Skip to content

Commit f79bb45

Browse files
committed
Fix packaged app launch and folder config
1 parent 3c61e8f commit f79bb45

4 files changed

Lines changed: 153 additions & 8 deletions

File tree

scripts/build_local.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ if [[ "$(uname -s)" == "Darwin" && -d "$PROJECT_ROOT/dist/$NAME.app" ]]; then
5959
plutil -replace CFBundleShortVersionString -string "$BUNDLE_VERSION" "$INFO_PLIST"
6060
plutil -replace CFBundleVersion -string "$VERSION" "$INFO_PLIST"
6161
plutil -replace CFBundleIconName -string "VideoMergingTool" "$INFO_PLIST"
62+
codesign --force --deep --sign - "$PROJECT_ROOT/dist/$NAME.app"
6263
rm -f "$DMG_PATH"
6364
rm -rf "$DMG_ROOT"
6465
mkdir -p "$DMG_ROOT"

tests/test_gui_folder_picker.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
from __future__ import annotations
22

33
import unittest
4+
from pathlib import Path
45
from unittest.mock import patch
56

6-
from videomerge.gui import _pick_folder
7+
from videomerge.gui import _load_gui_config, _pick_folder, _save_gui_config
78

89

910
class GuiFolderPickerTests(unittest.TestCase):
@@ -42,6 +43,48 @@ def test_macos_uses_osascript_folder_picker(self) -> None:
4243
pick_macos.assert_called_once_with("Select output folder")
4344
pick_tk.assert_not_called()
4445

46+
def test_config_save_excludes_selected_folders(self) -> None:
47+
with patch("videomerge.gui._config_path", return_value=Path("/tmp/vmt-test-config.json")) as config_path:
48+
path = config_path.return_value
49+
try:
50+
_save_gui_config(
51+
{
52+
"lang": "zh",
53+
"mode": "fast",
54+
"format": "mkv",
55+
"inputDir": "/private/source",
56+
"outputDir": "/private/output",
57+
"tempDir": "/private/temp",
58+
}
59+
)
60+
loaded = _load_gui_config()
61+
finally:
62+
path.unlink(missing_ok=True)
63+
64+
self.assertEqual(loaded["lang"], "zh")
65+
self.assertEqual(loaded["mode"], "fast")
66+
self.assertEqual(loaded["format"], "mkv")
67+
self.assertNotIn("inputDir", loaded)
68+
self.assertNotIn("outputDir", loaded)
69+
self.assertNotIn("tempDir", loaded)
70+
71+
def test_windows_picker_does_not_hide_files_with_folder_filter(self) -> None:
72+
commands = []
73+
74+
def fake_run(args, **kwargs): # type: ignore[no-untyped-def]
75+
commands.append(args[-1])
76+
return type("Result", (), {"returncode": 0, "stdout": "C:/Videos\n"})()
77+
78+
with patch("videomerge.gui.platform.system", return_value="Windows"), patch(
79+
"videomerge.gui.subprocess.run",
80+
side_effect=fake_run,
81+
):
82+
selected = _pick_folder("source")
83+
84+
self.assertEqual(selected, "C:/Videos")
85+
self.assertIn("All files (*.*)|*.*", commands[0])
86+
self.assertNotIn("*.folder", commands[0])
87+
4588

4689
if __name__ == "__main__":
4790
unittest.main()

videomerge/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
__all__ = ["__version__"]
22

3-
__version__ = "0.2.6-dev"
3+
__version__ = "0.2.7-dev"

videomerge/gui.py

Lines changed: 107 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,28 @@
2626
from . import __version__
2727

2828

29+
CONFIG_FIELD_IDS = [
30+
"name",
31+
"format",
32+
"sortBy",
33+
"codec",
34+
"gpu",
35+
"audioCodec",
36+
"crf",
37+
"preset",
38+
"fpsPolicy",
39+
"resolutionPolicy",
40+
"padColor",
41+
"ffmpegPath",
42+
"ffprobePath",
43+
"recursive",
44+
"overwrite",
45+
"dryRun",
46+
"keepTemp",
47+
"autoDownloadDeps",
48+
]
49+
50+
2951
HTML = r"""<!doctype html>
3052
<html lang="en">
3153
<head>
@@ -569,7 +591,7 @@
569591
files: [],
570592
running: false,
571593
statusTimer: null,
572-
lang: localStorage.getItem("vmt_lang") || "en",
594+
lang: "en",
573595
deps: { status: "notChecked", message: "" }
574596
};
575597
const $ = (id) => document.getElementById(id);
@@ -627,6 +649,36 @@
627649
if (!response.ok) throw new Error(payload.error || payload.message || response.statusText);
628650
return payload;
629651
}
652+
function readConfig() {
653+
const values = { lang: state.lang, mode: state.mode };
654+
const ids = ["name", "format", "sortBy", "codec", "gpu", "audioCodec", "crf", "preset", "fpsPolicy", "resolutionPolicy", "padColor", "ffmpegPath", "ffprobePath", "recursive", "overwrite", "dryRun", "keepTemp", "autoDownloadDeps"];
655+
ids.forEach(id => {
656+
const node = $(id);
657+
values[id] = node.type === "checkbox" ? node.checked : node.value;
658+
});
659+
return values;
660+
}
661+
function applyConfig(config) {
662+
if (!config || typeof config !== "object") return;
663+
if (config.lang && messages[config.lang]) state.lang = config.lang;
664+
if (config.mode) state.mode = config.mode;
665+
Object.entries(config).forEach(([id, value]) => {
666+
const node = $(id);
667+
if (!node) return;
668+
if (node.type === "checkbox") node.checked = Boolean(value);
669+
else node.value = value;
670+
});
671+
}
672+
async function loadConfig() {
673+
try { applyConfig(await api("/config")); } catch (error) { log(`ERROR: ${error.message}`); }
674+
}
675+
let saveTimer = null;
676+
function scheduleSaveConfig() {
677+
if (saveTimer) clearTimeout(saveTimer);
678+
saveTimer = setTimeout(async () => {
679+
try { await api("/config", readConfig()); } catch (error) { log(`ERROR: ${error.message}`); }
680+
}, 250);
681+
}
630682
async function checkDeps() {
631683
state.deps = { status: "checking", message: "" };
632684
renderDepStatus();
@@ -791,6 +843,7 @@
791843
document.querySelectorAll(".mode-card").forEach(card => card.addEventListener("click", () => {
792844
state.mode = card.dataset.mode;
793845
renderModes();
846+
scheduleSaveConfig();
794847
}));
795848
$("selectSource").addEventListener("click", () => selectFolder("source"));
796849
$("selectOutput").addEventListener("click", () => selectFolder("output"));
@@ -799,9 +852,14 @@
799852
$("startMerge").addEventListener("click", merge);
800853
$("languageSelect").addEventListener("change", () => {
801854
state.lang = $("languageSelect").value;
802-
localStorage.setItem("vmt_lang", state.lang);
803855
applyLanguage();
804856
if (state.files.length) renderFiles(state.files);
857+
scheduleSaveConfig();
858+
});
859+
["name", "format", "sortBy", "codec", "gpu", "audioCodec", "crf", "preset", "fpsPolicy", "resolutionPolicy", "padColor", "ffmpegPath", "ffprobePath", "recursive", "overwrite", "dryRun", "keepTemp", "autoDownloadDeps"].forEach(id => {
860+
const node = $(id);
861+
node.addEventListener(node.type === "checkbox" ? "change" : "input", scheduleSaveConfig);
862+
node.addEventListener("change", scheduleSaveConfig);
805863
});
806864
$("ffmpegStatus").addEventListener("click", checkDeps);
807865
document.querySelectorAll(".info").forEach(icon => {
@@ -825,9 +883,12 @@
825883
$("tooltip").style.display = "none";
826884
});
827885
});
828-
applyLanguage();
829-
log(t("selectBegin"));
830-
checkDeps();
886+
(async () => {
887+
await loadConfig();
888+
applyLanguage();
889+
log(t("selectBegin"));
890+
checkDeps();
891+
})();
831892
</script>
832893
</body>
833894
</html>
@@ -951,6 +1012,9 @@ def do_GET(self) -> None:
9511012
if parsed.path == "/status":
9521013
self._send_json(state.snapshot())
9531014
return
1015+
if parsed.path == "/config":
1016+
self._send_json(_load_gui_config())
1017+
return
9541018
self._send_json({"error": "Not found"}, HTTPStatus.NOT_FOUND)
9551019

9561020
def do_POST(self) -> None:
@@ -965,6 +1029,10 @@ def do_POST(self) -> None:
9651029
if parsed.path == "/cancel":
9661030
self._cancel()
9671031
return
1032+
if parsed.path == "/config":
1033+
_save_gui_config(payload)
1034+
self._send_json({"ok": True})
1035+
return
9681036
self._send_json({"error": "Not found"}, HTTPStatus.NOT_FOUND)
9691037

9701038
def _deps(self) -> None:
@@ -1256,7 +1324,7 @@ def _pick_folder_windows(title: str) -> str:
12561324
$dialog.CheckFileExists = $false
12571325
$dialog.ValidateNames = $false
12581326
$dialog.FileName = 'Select this folder'
1259-
$dialog.Filter = 'Folders|*.folder'
1327+
$dialog.Filter = 'All files (*.*)|*.*'
12601328
if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {{
12611329
$selected = $dialog.FileName
12621330
if (Test-Path -LiteralPath $selected -PathType Container) {{
@@ -1283,6 +1351,39 @@ def _pick_folder_windows(title: str) -> str:
12831351
return ""
12841352

12851353

1354+
def _config_dir() -> Path:
1355+
system = platform.system()
1356+
if system == "Darwin":
1357+
return Path.home() / "Library" / "Application Support" / "VideoMergingTool"
1358+
if system == "Windows":
1359+
root = os.environ.get("APPDATA") or os.environ.get("LOCALAPPDATA")
1360+
return Path(root) / "VideoMergingTool" if root else Path.home() / "AppData" / "Roaming" / "VideoMergingTool"
1361+
return Path(os.environ.get("XDG_CONFIG_HOME") or Path.home() / ".config") / "VideoMergingTool"
1362+
1363+
1364+
def _config_path() -> Path:
1365+
return _config_dir() / "config.json"
1366+
1367+
1368+
def _load_gui_config() -> dict[str, object]:
1369+
path = _config_path()
1370+
if not path.exists():
1371+
return {}
1372+
try:
1373+
payload = json.loads(path.read_text(encoding="utf-8"))
1374+
except (OSError, json.JSONDecodeError):
1375+
return {}
1376+
return payload if isinstance(payload, dict) else {}
1377+
1378+
1379+
def _save_gui_config(payload: dict[str, object]) -> None:
1380+
allowed = set(CONFIG_FIELD_IDS) | {"lang", "mode"}
1381+
clean = {key: value for key, value in payload.items() if key in allowed}
1382+
path = _config_path()
1383+
path.parent.mkdir(parents=True, exist_ok=True)
1384+
path.write_text(json.dumps(clean, indent=2, ensure_ascii=False), encoding="utf-8")
1385+
1386+
12861387
def _pick_folder_tk(title: str) -> str:
12871388
try:
12881389
import tkinter as tk

0 commit comments

Comments
 (0)