Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/alfagun74_scene-renamer.json
T

1 line
39 KiB
JSON

{"repo": "alfagun74/scene-renamer", "files": {"scene_renamer/ui/settings.py": {"6f07e1b13a7650db3830bde1bfbc92ebafa5180c": {"t": 1785746921.079303, "n": 11444, "h": "67b84e0ad8a01be0", "c": "\"\"\"Settings view — API, destinations, advanced options. Auto-saves on change.\"\"\"\n\nfrom __future__ import annotations\n\nimport flet as ft\n\nfrom scene_renamer.config import ConfigManager, DEFAULT_CONFIG\nfrom scene_renamer.models import AppState\nfrom scene_renamer.ui.theme import (\n CLR_TEXT, CLR_TEXT_SECONDARY, CLR_TEXT_DIM, CLR_SECONDARY,\n CLR_BORDER, CLR_SUCCESS,\n SPC_XS, SPC_SM, SPC_MD, SPC_LG, SPC_XL,\n)\n\n\nclass SettingsView:\n \"\"\"Application settings page — auto-saves on every change.\"\"\"\n\n def __init__(self, page: ft.Page, config: ConfigManager, state: AppState, movies_picker: ft.FilePicker, series_picker: ft.FilePicker) -> None:\n self.page = page\n self.config = config\n self.state = state\n self._movies_picker = movies_picker\n self._series_picker = series_picker\n self._saved_text = ft.Text(\"\", size=11, color=CLR_SUCCESS)\n\n # -- API section --\n self._api_key_field = ft.TextField(\n value=config.api_key,\n label=\"API Key\",\n hint_text=\"sk-... (OpenAI-compatible)\",\n password=True,\n can_reveal_password=True,\n on_change=lambda _: self._auto_save(),\n )\n self._api_url_field = ft.TextField(\n value=config.api_url,\n label=\"API Base URL\",\n hint_text=\"/chat/completions is appended automatically\",\n on_change=lambda _: self._auto_save(),\n )\n self._model_field = ft.TextField(\n value=config.model,\n label=\"Model\",\n hint_text=\"e.g. deepseek-chat, gpt-4o-mini, claude-3-5-sonnet\",\n on_change=lambda _: self._auto_save(),\n )\n\n # -- Paths section --\n self._movies_root_field = ft.TextField(\n value=config.movies_root,\n label=\"Movies destination\",\n hint_text=\"D:\\\\Movies\",\n on_change=lambda _: self._auto_save(),\n )\n self._series_root_field = ft.TextField(\n value=config.series_root,\n label=\"Series destination\",\n hint_text=\"D:\\\\Series\",\n on_change=lambda _: self._auto_save(),\n )\n self._batch_size_field = ft.TextField(\n value=str(config.batch_size),\n label=\"Batch size (files per API request)\",\n hint_text=\"200\",\n width=200,\n keyboard_type=ft.KeyboardType.NUMBER,\n input_filter=ft.InputFilter(allow=True, regex_string=r\"[0-9]*\", replacement_string=\"\"),\n on_change=lambda _: self._auto_save(),\n )\n\n # -- Advanced section --\n self._video_extensions_field = ft.TextField(\n value=\", \".join(config.video_extensions),\n label=\"Video extensions\",\n hint_text=\".mkv, .mp4, .avi, ...\",\n multiline=True, min_lines=2, max_lines=4,\n on_change=lambda _: self._auto_save(),\n )\n self._skip_patterns_field = ft.TextField(\n value=\", \".join(config.skip_patterns),\n label=\"Filename patterns to skip\",\n hint_text=\"*sample*, *.nfo, ...\",\n multiline=True, min_lines=3, max_lines=6,\n on_change=lambda _: self._auto_save(),\n )\n self._skip_dirs_field = ft.TextField(\n value=\", \".join(config.skip_dirs),\n label=\"Directories to skip\",\n hint_text=\"Sample, Proof, Subs, ...\",\n on_change=lambda _: self._auto_save(),\n )\n self._dry_run_switch = ft.Switch(\n value=config.dry_run,\n label=\"Dry run mode (preview only, no files moved)\",\n label_position=ft.LabelPosition.RIGHT,\n on_change=lambda _: self._auto_save(),\n )\n\n self._view: ft.Column | None = None\n\n # ================================================================\n # BUILD\n # ================================================================\n\n def build(self) -> ft.Column:\n self._view = ft.Column(\n [\n ft.Text(\"Settings\", size=20, weight=ft.FontWeight.W_600, color=CLR_TEXT),\n ft.Text(\n \"All changes are saved automatically. Set up your API connection and destinations below.\",\n size=13, color=CLR_TEXT_SECONDARY,\n ),\n ft.Divider(height=SPC_MD),\n\n # -- API --\n ft.Text(\"API Connection\", size=13, weight=ft.FontWeight.W_600, color=CLR_SECONDARY),\n ft.Text(\n \"Works with any OpenAI Chat Completions-compatible API: DeepSeek, OpenAI, Groq, Ollama, etc.\",\n size=12, color=CLR_TEXT_DIM,\n ),\n self._api_key_field,\n ft.Row([self._api_url_field, self._model_field], spacing=SPC_SM),\n ft.Divider(height=SPC_MD),\n\n # -- Paths --\n ft.Text(\"Destination Paths\", size=13, weight=ft.FontWeight.W_600, color=CLR_SECONDARY),\n ft.Text(\n \"Movies and TV series are moved here after classification. \"\n \"TV shows are organized into Show / Season NN / SxxExx folders automatically.\",\n size=12, color=CLR_TEXT_DIM,\n ),\n ft.Row([\n self._movies_root_field,\n ft.IconButton(\n icon=ft.Icons.FOLDER_OPEN, tooltip=\"Browse movies destination\",\n on_click=lambda _: self.page.run_task(\n self._pick_dir, self._movies_picker, self._movies_root_field, \"movies_root\"\n ),\n ),\n ], spacing=SPC_SM),\n ft.Row([\n self._series_root_field,\n ft.IconButton(\n icon=ft.Icons.FOLDER_OPEN, tooltip=\"Browse series destination\",\n on_click=lambda _: self.page.run_task(\n self._pick_dir, self._series_picker, self._series_root_field, \"series_root\"\n ),\n ),\n ], spacing=SPC_SM),\n self._batch_size_field,\n ft.Divider(height=SPC_MD),\n\n # -- Advanced --\n ft.Text(\"Advanced\", size=13, weight=ft.FontWeight.W_600, color=CLR_SECONDARY),\n ft.Text(\"Fine-tune scanning behavior and safety options.\", size=12, color=CLR_TEXT_DIM),\n self._dry_run_switch,\n ft.Text(\n \"When enabled, everything runs normally but files are never actually moved. \"\n \"Use this to preview results safely.\",\n size=11, color=CLR_TEXT_DIM,\n ),\n self._video_extensions_field,\n self._skip_patterns_field,\n self._skip_dirs_field,\n ft.Divider(height=SPC_MD),\n\n # -- Actions --\n ft.Row([\n ft.OutlinedButton(\n content=ft.Text(\"Reset to Defaults\"),\n icon=ft.Icons.RESTORE,\n on_click=self._on_reset,\n ),\n self._saved_text,\n ], spacing=SPC_SM),\n ],\n scroll=ft.ScrollMode.AUTO,\n expand=True,\n spacing=SPC_SM,\n )\n return self._view\n\n # ================================================================\n # LIFECYCLE\n # ================================================================\n\n def on_activate(self) -> None:\n \"\"\"Refresh field values when navigated to.\"\"\"\n self._api_key_field.value = self.config.api_key\n self._api_url_field.value = self.config.api_url\n self._model_field.value = self.config.model\n self._movies_root_field.value = self.config.movies_root\n self._series_root_field.value = self.config.series_root\n self._batch_size_field.value = str(self.config.batch_size)\n self._video_extensions_field.value = \", \".join(self.config.video_extensions)\n self._skip_patterns_field.value = \", \".join(self.config.skip_patterns)\n self._skip_dirs_field.value = \", \".join(self.config.skip_dirs)\n self._dry_run_switch.value = self.config.dry_run\n\n # ================================================================\n # HANDLERS\n # ================================================================\n\n async def _pick_dir(self, picker: ft.FilePicker, field: ft.TextField, config_key: str) -> None:\n path = await picker.get_directory_path()\n if path:\n field.value = path\n field.update()\n setattr(self.config, config_key, path)\n self.config.save()\n self._flash_saved()\n\n def _auto_save(self) -> None:\n \"\"\"Persist all fields to config immediately.\"\"\"\n self.config.api_key = self._api_key_field.value or \"\"\n\n url = self._api_url_field.value or \"\"\n self.config._data[\"api_url\"] = url if url else DEFAULT_CONFIG[\"api_url\"]\n\n model = self._model_field.value or \"\"\n if model:\n self.config._data[\"model\"] = model\n\n movies = self._movies_root_field.value or \"\"\n if movies:\n self.config.movies_root = movies\n\n series = self._series_root_field.value or \"\"\n if series:\n self.config.series_root = series\n\n try:\n self.config.batch_size = int(self._batch_size_field.value or \"200\")\n except ValueError:\n self.config.batch_size = DEFAULT_CONFIG[\"batch_size\"]\n\n self.config._data[\"dry_run\"] = self._dry_run_switch.value\n\n exts = [x.strip() for x in self._video_extensions_field.value.split(\",\") if x.strip()]\n self.config._data[\"video_extensions\"] = exts or DEFAULT_CONFIG[\"video_extensions\"]\n\n pats = [p.strip() for p in self._skip_patterns_field.value.split(\",\") if p.strip()]\n self.config._data[\"skip_patterns\"] = pats or DEFAULT_CONFIG[\"skip_patterns\"]\n\n dirs = [d.strip() for d in self._skip_dirs_field.value.split(\",\") if d.strip()]\n self.config._data[\"skip_dirs\"] = dirs or DEFAULT_CONFIG[\"skip_dirs\"]\n\n self.config.save()\n self._flash_saved()\n\n def _on_reset(self, e: ft.ControlEvent) -> None:\n self.config.reset_to_defaults()\n self.config.save()\n self._api_key_field.value = \"\"\n self._api_url_field.value = DEFAULT_CONFIG[\"api_url\"]\n self._model_field.value = DEFAULT_CONFIG[\"model\"]\n self._movies_root_field.value = DEFAULT_CONFIG[\"movies_root\"]\n self._series_root_field.value = DEFAULT_CONFIG[\"series_root\"]\n self._batch_size_field.value = str(DEFAULT_CONFIG[\"batch_size\"])\n self._video_extensions_field.value = \", \".join(DEFAULT_CONFIG[\"video_extensions\"])\n self._skip_patterns_field.value = \", \".join(DEFAULT_CONFIG[\"skip_patterns\"])\n self._skip_dirs_field.value = \", \".join(DEFAULT_CONFIG[\"skip_dirs\"])\n self._dry_run_switch.value = DEFAULT_CONFIG[\"dry_run\"]\n if self._view:\n self._view.update()\n self._flash_saved()\n\n # ================================================================\n # HELPERS\n # ================================================================\n\n def _flash_saved(self) -> None:\n self._saved_text.value = \"Saved\"\n self._saved_text.update()\n"}, "ff41a3bb938c38243615033163c719d942642039": {"t": 1785769029.641597, "n": 13012, "h": "5afae64c1b93357c", "c": "\"\"\"Settings view — API, destinations, advanced options. Auto-saves on change.\"\"\"\n\nfrom __future__ import annotations\n\nimport flet as ft\n\nfrom scene_renamer.config import ConfigManager, DEFAULT_CONFIG\nfrom scene_renamer.models import AppState\nfrom scene_renamer.ui.theme import (\n CLR_TEXT, CLR_TEXT_SECONDARY, CLR_TEXT_DIM, CLR_SECONDARY,\n CLR_BORDER, CLR_SUCCESS,\n SPC_XS, SPC_SM, SPC_MD, SPC_LG, SPC_XL,\n)\n\n_MODEL_PRESETS = [\n \"deepseek-chat\",\n \"deepseek-reasoner\",\n \"gpt-4o-mini\",\n \"gpt-4o\",\n \"claude-3-5-sonnet\",\n]\n\n\nclass SettingsView:\n \"\"\"Application settings page — auto-saves on every change.\"\"\"\n\n def __init__(self, page: ft.Page, config: ConfigManager, state: AppState) -> None:\n self.page = page\n self.config = config\n self.state = state\n self._saved_text = ft.Text(\"\", size=11, color=CLR_SUCCESS)\n\n # -- API section --\n self._api_key_field = ft.TextField(\n value=config.api_key,\n label=\"API Key\",\n hint_text=\"sk-... (OpenAI-compatible)\",\n password=True,\n can_reveal_password=True,\n on_change=lambda _: self._auto_save(),\n )\n self._api_url_field = ft.TextField(\n value=config.api_url,\n label=\"API Base URL\",\n hint_text=\"/chat/completions is appended automatically\",\n on_change=lambda _: self._auto_save(),\n )\n self._model_dropdown = ft.Dropdown(\n value=self._resolve_model_value(config.model),\n label=\"Model\",\n width=280,\n options=[ft.dropdown.Option(m, m) for m in _MODEL_PRESETS]\n + [ft.dropdown.Option(\"__custom__\", \"Custom...\")],\n on_select=self._on_model_changed,\n )\n self._model_custom_field = ft.TextField(\n value=config.model if config.model not in _MODEL_PRESETS else \"\",\n label=\"Custom model name\",\n hint_text=\"e.g. llama-3-70b\",\n visible=config.model not in _MODEL_PRESETS,\n on_change=lambda _: self._auto_save(),\n )\n\n # -- Paths section --\n self._movies_root_field = ft.TextField(\n value=config.movies_root,\n label=\"Movies destination\",\n hint_text=\"D:\\\\Movies\",\n on_change=lambda _: self._auto_save(),\n )\n self._series_root_field = ft.TextField(\n value=config.series_root,\n label=\"Series destination\",\n hint_text=\"D:\\\\Series\",\n on_change=lambda _: self._auto_save(),\n )\n self._batch_size_field = ft.TextField(\n value=str(config.batch_size),\n label=\"Batch size (files per API request)\",\n hint_text=\"200\",\n width=200,\n keyboard_type=ft.KeyboardType.NUMBER,\n input_filter=ft.InputFilter(allow=True, regex_string=r\"[0-9]*\", replacement_string=\"\"),\n on_change=lambda _: self._auto_save(),\n )\n\n # -- Advanced section --\n self._video_extensions_field = ft.TextField(\n value=\", \".join(config.video_extensions),\n label=\"Video extensions\",\n hint_text=\".mkv, .mp4, .avi, ...\",\n multiline=True, min_lines=2, max_lines=4,\n on_change=lambda _: self._auto_save(),\n )\n self._skip_patterns_field = ft.TextField(\n value=\", \".join(config.skip_patterns),\n label=\"Filename patterns to skip\",\n hint_text=\"*sample*, *.nfo, ...\",\n multiline=True, min_lines=3, max_lines=6,\n on_change=lambda _: self._auto_save(),\n )\n self._skip_dirs_field = ft.TextField(\n value=\", \".join(config.skip_dirs),\n label=\"Directories to skip\",\n hint_text=\"Sample, Proof, Subs, ...\",\n on_change=lambda _: self._auto_save(),\n )\n self._dry_run_switch = ft.Switch(\n value=config.dry_run,\n label=\"Dry run mode (preview only, no files moved)\",\n label_position=ft.LabelPosition.RIGHT,\n on_change=lambda _: self._auto_save(),\n )\n\n self._view: ft.Column | None = None\n\n # File pickers\n self._movies_picker = ft.FilePicker()\n self._series_picker = ft.FilePicker()\n\n # ================================================================\n # BUILD\n # ================================================================\n\n def build(self) -> ft.Column:\n self._view = ft.Column(\n [\n ft.Text(\"Settings\", size=20, weight=ft.FontWeight.W_600, color=CLR_TEXT),\n ft.Text(\n \"All changes are saved automatically. Set up your API connection and destinations below.\",\n size=13, color=CLR_TEXT_SECONDARY,\n ),\n ft.Divider(height=SPC_MD),\n\n # -- API --\n ft.Text(\"API Connection\", size=13, weight=ft.FontWeight.W_600, color=CLR_SECONDARY),\n ft.Text(\n \"Works with any OpenAI Chat Completions-compatible API: DeepSeek, OpenAI, Groq, Ollama, etc.\",\n size=12, color=CLR_TEXT_DIM,\n ),\n self._api_key_field,\n ft.Row([self._api_url_field, self._model_dropdown], spacing=SPC_SM),\n self._model_custom_field,\n ft.Divider(height=SPC_MD),\n\n # -- Paths --\n ft.Text(\"Destination Paths\", size=13, weight=ft.FontWeight.W_600, color=CLR_SECONDARY),\n ft.Text(\n \"Movies and TV series are moved here after classification. \"\n \"TV shows are organized into Show / Season NN / SxxExx folders automatically.\",\n size=12, color=CLR_TEXT_DIM,\n ),\n ft.Row([\n self._movies_root_field,\n ft.IconButton(\n icon=ft.Icons.FOLDER_OPEN, tooltip=\"Browse movies destination\",\n on_click=lambda _: self.page.run_task(\n self._pick_dir, self._movies_picker, self._movies_root_field, \"movies_root\"\n ),\n ),\n ], spacing=SPC_SM),\n ft.Row([\n self._series_root_field,\n ft.IconButton(\n icon=ft.Icons.FOLDER_OPEN, tooltip=\"Browse series destination\",\n on_click=lambda _: self.page.run_task(\n self._pick_dir, self._series_picker, self._series_root_field, \"series_root\"\n ),\n ),\n ], spacing=SPC_SM),\n self._batch_size_field,\n ft.Divider(height=SPC_MD),\n\n # -- Advanced --\n ft.Text(\"Advanced\", size=13, weight=ft.FontWeight.W_600, color=CLR_SECONDARY),\n ft.Text(\"Fine-tune scanning behavior and safety options.\", size=12, color=CLR_TEXT_DIM),\n self._dry_run_switch,\n ft.Text(\n \"When enabled, everything runs normally but files are never actually moved. \"\n \"Use this to preview results safely.\",\n size=11, color=CLR_TEXT_DIM,\n ),\n self._video_extensions_field,\n self._skip_patterns_field,\n self._skip_dirs_field,\n ft.Divider(height=SPC_MD),\n\n # -- Actions --\n ft.Row([\n ft.OutlinedButton(\n content=ft.Text(\"Reset to Defaults\"),\n icon=ft.Icons.RESTORE,\n on_click=self._on_reset,\n ),\n self._saved_text,\n ], spacing=SPC_SM),\n ],\n scroll=ft.ScrollMode.AUTO,\n expand=True,\n spacing=SPC_SM,\n )\n return self._view\n\n # ================================================================\n # LIFECYCLE\n # ================================================================\n\n def on_activate(self) -> None:\n \"\"\"Refresh field values when navigated to.\"\"\"\n self._api_key_field.value = self.config.api_key\n self._api_url_field.value = self.config.api_url\n self._model_dropdown.value = self._resolve_model_value(self.config.model)\n self._model_custom_field.value = self.config.model if self.config.model not in _MODEL_PRESETS else \"\"\n self._model_custom_field.visible = self.config.model not in _MODEL_PRESETS\n self._movies_root_field.value = self.config.movies_root\n self._series_root_field.value = self.config.series_root\n self._batch_size_field.value = str(self.config.batch_size)\n self._video_extensions_field.value = \", \".join(self.config.video_extensions)\n self._skip_patterns_field.value = \", \".join(self.config.skip_patterns)\n self._skip_dirs_field.value = \", \".join(self.config.skip_dirs)\n self._dry_run_switch.value = self.config.dry_run\n\n # ================================================================\n # HANDLERS\n # ================================================================\n\n async def _pick_dir(self, picker: ft.FilePicker, field: ft.TextField, config_key: str) -> None:\n path = await picker.get_directory_path()\n if path:\n field.value = path\n field.update()\n setattr(self.config, config_key, path)\n self.config.save()\n self._flash_saved()\n\n def _on_model_changed(self, e: ft.ControlEvent) -> None:\n val = self._model_dropdown.value or \"\"\n if val == \"__custom__\":\n self._model_custom_field.visible = True\n self._model_custom_field.update()\n else:\n self._model_custom_field.visible = False\n self._model_custom_field.update()\n self._auto_save()\n\n def _auto_save(self) -> None:\n \"\"\"Persist all fields to config immediately.\"\"\"\n self.config.api_key = self._api_key_field.value or \"\"\n\n url = self._api_url_field.value or \"\"\n self.config._data[\"api_url\"] = url if url else DEFAULT_CONFIG[\"api_url\"]\n\n model = self._model_dropdown.value or \"\"\n if model == \"__custom__\":\n model = self._model_custom_field.value or DEFAULT_CONFIG[\"model\"]\n if model and model != \"__custom__\":\n self.config._data[\"model\"] = model\n\n movies = self._movies_root_field.value or \"\"\n if movies:\n self.config.movies_root = movies\n\n series = self._series_root_field.value or \"\"\n if series:\n self.config.series_root = series\n\n try:\n self.config.batch_size = int(self._batch_size_field.value or \"200\")\n except ValueError:\n self.config.batch_size = DEFAULT_CONFIG[\"batch_size\"]\n\n self.config._data[\"dry_run\"] = self._dry_run_switch.value\n\n exts = [x.strip() for x in self._video_extensions_field.value.split(\",\") if x.strip()]\n self.config._data[\"video_extensions\"] = exts or DEFAULT_CONFIG[\"video_extensions\"]\n\n pats = [p.strip() for p in self._skip_patterns_field.value.split(\",\") if p.strip()]\n self.config._data[\"skip_patterns\"] = pats or DEFAULT_CONFIG[\"skip_patterns\"]\n\n dirs = [d.strip() for d in self._skip_dirs_field.value.split(\",\") if d.strip()]\n self.config._data[\"skip_dirs\"] = dirs or DEFAULT_CONFIG[\"skip_dirs\"]\n\n self.config.save()\n self._flash_saved()\n\n def _on_reset(self, e: ft.ControlEvent) -> None:\n self.config.reset_to_defaults()\n self.config.save()\n self._api_key_field.value = \"\"\n self._api_url_field.value = DEFAULT_CONFIG[\"api_url\"]\n self._model_dropdown.value = DEFAULT_CONFIG[\"model\"]\n self._model_custom_field.value = \"\"\n self._model_custom_field.visible = False\n self._movies_root_field.value = DEFAULT_CONFIG[\"movies_root\"]\n self._series_root_field.value = DEFAULT_CONFIG[\"series_root\"]\n self._batch_size_field.value = str(DEFAULT_CONFIG[\"batch_size\"])\n self._video_extensions_field.value = \", \".join(DEFAULT_CONFIG[\"video_extensions\"])\n self._skip_patterns_field.value = \", \".join(DEFAULT_CONFIG[\"skip_patterns\"])\n self._skip_dirs_field.value = \", \".join(DEFAULT_CONFIG[\"skip_dirs\"])\n self._dry_run_switch.value = DEFAULT_CONFIG[\"dry_run\"]\n if self._view:\n self._view.update()\n self._flash_saved()\n\n # ================================================================\n # HELPERS\n # ================================================================\n\n @staticmethod\n def _resolve_model_value(model: str) -> str:\n if model in _MODEL_PRESETS:\n return model\n return \"__custom__\"\n\n def _flash_saved(self) -> None:\n self._saved_text.value = \"Saved\"\n self._saved_text.update()\n"}, "92dd8c8b5e56919965b4defd5c68f48c851f6921": {"t": 1785769030.930426, "n": 13012, "h": "83e0394f26d1bc98", "c": "\"\"\"Settings view — API, destinations, advanced options. Auto-saves on change.\"\"\"\n\nfrom __future__ import annotations\n\nimport flet as ft\n\nfrom scene_renamer.config import ConfigManager, DEFAULT_CONFIG\nfrom scene_renamer.models import AppState\nfrom scene_renamer.ui.theme import (\n CLR_TEXT, CLR_TEXT_SECONDARY, CLR_TEXT_DIM, CLR_SECONDARY,\n CLR_BORDER, CLR_SUCCESS,\n SPC_XS, SPC_SM, SPC_MD, SPC_LG, SPC_XL,\n)\n\n_MODEL_PRESETS = [\n \"deepseek-chat\",\n \"deepseek-reasoner\",\n \"gpt-4o-mini\",\n \"gpt-4o\",\n \"claude-3-5-sonnet\",\n]\n\n\nclass SettingsView:\n \"\"\"Application settings page — auto-saves on every change.\"\"\"\n\n def __init__(self, page: ft.Page, config: ConfigManager, state: AppState) -> None:\n self.page = page\n self.config = config\n self.state = state\n self._saved_text = ft.Text(\"\", size=11, color=CLR_SUCCESS)\n\n # -- API section --\n self._api_key_field = ft.TextField(\n value=config.api_key,\n label=\"API Key\",\n hint_text=\"sk-... (OpenAI-compatible)\",\n password=True,\n can_reveal_password=True,\n on_change=lambda _: self._auto_save(),\n )\n self._api_url_field = ft.TextField(\n value=config.api_url,\n label=\"API Base URL\",\n hint_text=\"/chat/completions is appended automatically\",\n on_change=lambda _: self._auto_save(),\n )\n self._model_dropdown = ft.Dropdown(\n value=self._resolve_model_value(config.model),\n label=\"Model\",\n width=280,\n options=[ft.dropdown.Option(m, m) for m in _MODEL_PRESETS]\n + [ft.dropdown.Option(\"__custom__\", \"Custom...\")],\n on_change=self._on_model_changed,\n )\n self._model_custom_field = ft.TextField(\n value=config.model if config.model not in _MODEL_PRESETS else \"\",\n label=\"Custom model name\",\n hint_text=\"e.g. llama-3-70b\",\n visible=config.model not in _MODEL_PRESETS,\n on_change=lambda _: self._auto_save(),\n )\n\n # -- Paths section --\n self._movies_root_field = ft.TextField(\n value=config.movies_root,\n label=\"Movies destination\",\n hint_text=\"D:\\\\Movies\",\n on_change=lambda _: self._auto_save(),\n )\n self._series_root_field = ft.TextField(\n value=config.series_root,\n label=\"Series destination\",\n hint_text=\"D:\\\\Series\",\n on_change=lambda _: self._auto_save(),\n )\n self._batch_size_field = ft.TextField(\n value=str(config.batch_size),\n label=\"Batch size (files per API request)\",\n hint_text=\"200\",\n width=200,\n keyboard_type=ft.KeyboardType.NUMBER,\n input_filter=ft.InputFilter(allow=True, regex_string=r\"[0-9]*\", replacement_string=\"\"),\n on_change=lambda _: self._auto_save(),\n )\n\n # -- Advanced section --\n self._video_extensions_field = ft.TextField(\n value=\", \".join(config.video_extensions),\n label=\"Video extensions\",\n hint_text=\".mkv, .mp4, .avi, ...\",\n multiline=True, min_lines=2, max_lines=4,\n on_change=lambda _: self._auto_save(),\n )\n self._skip_patterns_field = ft.TextField(\n value=\", \".join(config.skip_patterns),\n label=\"Filename patterns to skip\",\n hint_text=\"*sample*, *.nfo, ...\",\n multiline=True, min_lines=3, max_lines=6,\n on_change=lambda _: self._auto_save(),\n )\n self._skip_dirs_field = ft.TextField(\n value=\", \".join(config.skip_dirs),\n label=\"Directories to skip\",\n hint_text=\"Sample, Proof, Subs, ...\",\n on_change=lambda _: self._auto_save(),\n )\n self._dry_run_switch = ft.Switch(\n value=config.dry_run,\n label=\"Dry run mode (preview only, no files moved)\",\n label_position=ft.LabelPosition.RIGHT,\n on_change=lambda _: self._auto_save(),\n )\n\n self._view: ft.Column | None = None\n\n # File pickers\n self._movies_picker = ft.FilePicker()\n self._series_picker = ft.FilePicker()\n\n # ================================================================\n # BUILD\n # ================================================================\n\n def build(self) -> ft.Column:\n self._view = ft.Column(\n [\n ft.Text(\"Settings\", size=20, weight=ft.FontWeight.W_600, color=CLR_TEXT),\n ft.Text(\n \"All changes are saved automatically. Set up your API connection and destinations below.\",\n size=13, color=CLR_TEXT_SECONDARY,\n ),\n ft.Divider(height=SPC_MD),\n\n # -- API --\n ft.Text(\"API Connection\", size=13, weight=ft.FontWeight.W_600, color=CLR_SECONDARY),\n ft.Text(\n \"Works with any OpenAI Chat Completions-compatible API: DeepSeek, OpenAI, Groq, Ollama, etc.\",\n size=12, color=CLR_TEXT_DIM,\n ),\n self._api_key_field,\n ft.Row([self._api_url_field, self._model_dropdown], spacing=SPC_SM),\n self._model_custom_field,\n ft.Divider(height=SPC_MD),\n\n # -- Paths --\n ft.Text(\"Destination Paths\", size=13, weight=ft.FontWeight.W_600, color=CLR_SECONDARY),\n ft.Text(\n \"Movies and TV series are moved here after classification. \"\n \"TV shows are organized into Show / Season NN / SxxExx folders automatically.\",\n size=12, color=CLR_TEXT_DIM,\n ),\n ft.Row([\n self._movies_root_field,\n ft.IconButton(\n icon=ft.Icons.FOLDER_OPEN, tooltip=\"Browse movies destination\",\n on_click=lambda _: self.page.run_task(\n self._pick_dir, self._movies_picker, self._movies_root_field, \"movies_root\"\n ),\n ),\n ], spacing=SPC_SM),\n ft.Row([\n self._series_root_field,\n ft.IconButton(\n icon=ft.Icons.FOLDER_OPEN, tooltip=\"Browse series destination\",\n on_click=lambda _: self.page.run_task(\n self._pick_dir, self._series_picker, self._series_root_field, \"series_root\"\n ),\n ),\n ], spacing=SPC_SM),\n self._batch_size_field,\n ft.Divider(height=SPC_MD),\n\n # -- Advanced --\n ft.Text(\"Advanced\", size=13, weight=ft.FontWeight.W_600, color=CLR_SECONDARY),\n ft.Text(\"Fine-tune scanning behavior and safety options.\", size=12, color=CLR_TEXT_DIM),\n self._dry_run_switch,\n ft.Text(\n \"When enabled, everything runs normally but files are never actually moved. \"\n \"Use this to preview results safely.\",\n size=11, color=CLR_TEXT_DIM,\n ),\n self._video_extensions_field,\n self._skip_patterns_field,\n self._skip_dirs_field,\n ft.Divider(height=SPC_MD),\n\n # -- Actions --\n ft.Row([\n ft.OutlinedButton(\n content=ft.Text(\"Reset to Defaults\"),\n icon=ft.Icons.RESTORE,\n on_click=self._on_reset,\n ),\n self._saved_text,\n ], spacing=SPC_SM),\n ],\n scroll=ft.ScrollMode.AUTO,\n expand=True,\n spacing=SPC_SM,\n )\n return self._view\n\n # ================================================================\n # LIFECYCLE\n # ================================================================\n\n def on_activate(self) -> None:\n \"\"\"Refresh field values when navigated to.\"\"\"\n self._api_key_field.value = self.config.api_key\n self._api_url_field.value = self.config.api_url\n self._model_dropdown.value = self._resolve_model_value(self.config.model)\n self._model_custom_field.value = self.config.model if self.config.model not in _MODEL_PRESETS else \"\"\n self._model_custom_field.visible = self.config.model not in _MODEL_PRESETS\n self._movies_root_field.value = self.config.movies_root\n self._series_root_field.value = self.config.series_root\n self._batch_size_field.value = str(self.config.batch_size)\n self._video_extensions_field.value = \", \".join(self.config.video_extensions)\n self._skip_patterns_field.value = \", \".join(self.config.skip_patterns)\n self._skip_dirs_field.value = \", \".join(self.config.skip_dirs)\n self._dry_run_switch.value = self.config.dry_run\n\n # ================================================================\n # HANDLERS\n # ================================================================\n\n async def _pick_dir(self, picker: ft.FilePicker, field: ft.TextField, config_key: str) -> None:\n path = await picker.get_directory_path()\n if path:\n field.value = path\n field.update()\n setattr(self.config, config_key, path)\n self.config.save()\n self._flash_saved()\n\n def _on_model_changed(self, e: ft.ControlEvent) -> None:\n val = self._model_dropdown.value or \"\"\n if val == \"__custom__\":\n self._model_custom_field.visible = True\n self._model_custom_field.update()\n else:\n self._model_custom_field.visible = False\n self._model_custom_field.update()\n self._auto_save()\n\n def _auto_save(self) -> None:\n \"\"\"Persist all fields to config immediately.\"\"\"\n self.config.api_key = self._api_key_field.value or \"\"\n\n url = self._api_url_field.value or \"\"\n self.config._data[\"api_url\"] = url if url else DEFAULT_CONFIG[\"api_url\"]\n\n model = self._model_dropdown.value or \"\"\n if model == \"__custom__\":\n model = self._model_custom_field.value or DEFAULT_CONFIG[\"model\"]\n if model and model != \"__custom__\":\n self.config._data[\"model\"] = model\n\n movies = self._movies_root_field.value or \"\"\n if movies:\n self.config.movies_root = movies\n\n series = self._series_root_field.value or \"\"\n if series:\n self.config.series_root = series\n\n try:\n self.config.batch_size = int(self._batch_size_field.value or \"200\")\n except ValueError:\n self.config.batch_size = DEFAULT_CONFIG[\"batch_size\"]\n\n self.config._data[\"dry_run\"] = self._dry_run_switch.value\n\n exts = [x.strip() for x in self._video_extensions_field.value.split(\",\") if x.strip()]\n self.config._data[\"video_extensions\"] = exts or DEFAULT_CONFIG[\"video_extensions\"]\n\n pats = [p.strip() for p in self._skip_patterns_field.value.split(\",\") if p.strip()]\n self.config._data[\"skip_patterns\"] = pats or DEFAULT_CONFIG[\"skip_patterns\"]\n\n dirs = [d.strip() for d in self._skip_dirs_field.value.split(\",\") if d.strip()]\n self.config._data[\"skip_dirs\"] = dirs or DEFAULT_CONFIG[\"skip_dirs\"]\n\n self.config.save()\n self._flash_saved()\n\n def _on_reset(self, e: ft.ControlEvent) -> None:\n self.config.reset_to_defaults()\n self.config.save()\n self._api_key_field.value = \"\"\n self._api_url_field.value = DEFAULT_CONFIG[\"api_url\"]\n self._model_dropdown.value = DEFAULT_CONFIG[\"model\"]\n self._model_custom_field.value = \"\"\n self._model_custom_field.visible = False\n self._movies_root_field.value = DEFAULT_CONFIG[\"movies_root\"]\n self._series_root_field.value = DEFAULT_CONFIG[\"series_root\"]\n self._batch_size_field.value = str(DEFAULT_CONFIG[\"batch_size\"])\n self._video_extensions_field.value = \", \".join(DEFAULT_CONFIG[\"video_extensions\"])\n self._skip_patterns_field.value = \", \".join(DEFAULT_CONFIG[\"skip_patterns\"])\n self._skip_dirs_field.value = \", \".join(DEFAULT_CONFIG[\"skip_dirs\"])\n self._dry_run_switch.value = DEFAULT_CONFIG[\"dry_run\"]\n if self._view:\n self._view.update()\n self._flash_saved()\n\n # ================================================================\n # HELPERS\n # ================================================================\n\n @staticmethod\n def _resolve_model_value(model: str) -> str:\n if model in _MODEL_PRESETS:\n return model\n return \"__custom__\"\n\n def _flash_saved(self) -> None:\n self._saved_text.value = \"Saved\"\n self._saved_text.update()\n"}}}}