"""Ready-made connections to install between an input and an output you pick, plus your own templates (saved from an arrow, or made here).""" import os import flet as ft from Core import ChipButton, DisplayPath, Heading, Note, Palette from Templates import AllTemplates, DeleteUserTemplate, SaveUserTemplate, SortTemplate from Widgets import App, DirField, ExpandPath, NameField CardWidth = 300 class TemplatesPage(ft.Column): def __init__(self, TheApp: App): super().__init__(expand=True, spacing=0) self.App = TheApp self.Grid = ft.Row(wrap=True, spacing=12, run_spacing=12, vertical_alignment=ft.CrossAxisAlignment.START) self.controls = [ ft.Container( ft.Row([ Heading("Templates"), ft.Container(expand=True), ChipButton("New template", lambda e: self._EditDialog(None), ft.Icons.ADD), ]), padding=ft.Padding.only(left=16, right=16, top=12, bottom=4), ), ft.Container(Note("Pick one, choose where files come from and where they go. " "Your own templates live in ~/.config/SaraSuite/SaraSort/Templates/."), padding=ft.Padding.only(left=16, bottom=8)), ft.Divider(height=1, color=Palette.Surface1), ft.Container(ft.Column([self.Grid], scroll=ft.ScrollMode.AUTO, expand=True), padding=16, expand=True), ] def Refresh(self) -> None: self.Grid.controls = [self._Card(Template) for Template in AllTemplates()] self.update() def _Card(self, Template: SortTemplate) -> ft.Container: Icon = getattr(ft.Icons, Template.Icon, ft.Icons.FOLDER_COPY_OUTLINED) Patterns = ", ".join(Template.Patterns) Extras = [] if Template.OutputSettings.get("Unzip"): Extras.append("unpacks archives") if Template.OutputSettings.get("DeleteOrg") == 0: Extras.append("copies") if Template.InputSettings.get("SortDirs"): Extras.append("sorts folders") if Template.NewFileName: Extras.append(f"renames to {Template.NewFileName}") Buttons = [ChipButton("Install", lambda e: self._InstallDialog(Template), ft.Icons.DOWNLOAD_OUTLINED)] if not Template.BuiltIn: Buttons += [ ft.IconButton(ft.Icons.EDIT_OUTLINED, tooltip="Edit", on_click=lambda e: self._EditDialog(Template)), ft.IconButton(ft.Icons.DELETE_OUTLINE, icon_color=Palette.Red, tooltip="Delete template", on_click=lambda e: self.App.Confirm("Delete template", f"Delete \"{Template.Name}\"?", lambda: (DeleteUserTemplate(Template), self.Refresh()), "Delete")), ] return ft.Container( ft.Column([ ft.Row([ft.Icon(Icon, color=Palette.Mauve), ft.Text(Template.Name, size=16, expand=True), Note("built-in" if Template.BuiltIn else "yours", 11)]), ft.Text(Template.Description or " ", size=12), ft.Text(Patterns, size=12, color=Palette.Mauve, max_lines=2, overflow=ft.TextOverflow.ELLIPSIS, tooltip=Patterns), Note(", ".join(Extras), 11) if Extras else ft.Container(height=0), ft.Row(Buttons), ], spacing=8), width=CardWidth, padding=14, bgcolor=Palette.Mantle, border=ft.Border.all(1, Palette.Surface1), border_radius=6, ) def _InstallDialog(self, Template: SortTemplate) -> None: TheSetup, Error = self.App.LoadSetup() if TheSetup is None: self.App.Notify(Error, "error") return Root = TheSetup.RootDir() or os.path.expanduser("~") DefaultInput = next(iter(TheSetup.Inputs), f"{Root}/Input") DefaultOutput = ExpandPath(Template.SuggestedOutput) if Template.SuggestedOutput else f"{Root}/{Template.Name.split(' ')[0]}" InputRow, InputField = DirField(self.App, "Input (files come from)", DefaultInput) OutputRow, OutputField = DirField(self.App, "Output (files go to)", DefaultOutput) Inputs = list(dict.fromkeys(TheSetup.Inputs)) Quick = ft.Row([ ft.Container(ft.Text(DisplayPath(PathText), size=11, color=Palette.Red), padding=ft.Padding.symmetric(horizontal=6, vertical=2), border=ft.Border.all(1, Palette.Surface1), border_radius=4, on_click=lambda e, PathText=PathText: (setattr(InputField, "value", PathText), InputField.update())) for PathText in Inputs ], wrap=True, spacing=4) PatternField = ft.TextField(label="Patterns", value=", ".join(Template.Patterns), dense=True, multiline=True, max_lines=3) Defaults = TheSetup.TemplateFileDefaults() Name = NameField(TheSetup, Template.NewFileName or str(Defaults.get("NewFileName", "~^")), DefaultOutput, Defaults) Move = ft.Switch(label="Move files (off: copy them)", value=Template.OutputSettings.get("DeleteOrg", 1) == 1) def Install(e) -> None: InputPath, OutputPath = ExpandPath(InputField.value or ""), ExpandPath(OutputField.value or "") Patterns = [Part.strip() for Part in (PatternField.value or "").split(",") if Part.strip()] if not InputPath or not OutputPath or not Patterns: self.App.Notify("Pick an input, an output and at least one pattern.", "warning") return if InputPath == OutputPath: self.App.Notify("Input and output are the same directory.", "warning") return self.App.Close() OutputSettings = dict(Template.OutputSettings) OutputSettings["DeleteOrg"] = 1 if Move.value else 0 Overrides = {} if Name.value and Name.value != Defaults.get("NewFileName"): Overrides["NewFileName"] = Name.value try: Changed = TheSetup.Connect(InputPath, OutputPath, Patterns, Overrides, OutputSettings, dict(Template.InputSettings)) except Exception as Error: self.App.Notify(f"Couldn't install: {Error}", "error") return if Changed: self.App.AfterGlobalChange() else: self.App.Notify(f"Installed {Template.Name}.", "success") self.App.Navigate("Flow") Notes = [] if Template.InputSettings.get("SortDirs"): Notes.append(Note("Turns on SortDirs for the input: folders dropped there are zipped and sorted too.")) if Template.OutputSettings.get("Unzip"): Notes.append(Note(f"The output unpacks {', '.join(Template.OutputSettings['Unzip'])} files and removes the archive.")) self.App.ShowDialog(f"Install: {Template.Name}", ft.Column([ InputRow, Quick, OutputRow, PatternField, Name, Move, *Notes, ], tight=True, spacing=10, scroll=ft.ScrollMode.AUTO), [ ChipButton("Cancel", lambda e: self.App.Close()), ChipButton("Install", Install), ], Width=640) def _EditDialog(self, Template: SortTemplate | None) -> None: TheSetup, _ = self.App.LoadSetup() NameInput = ft.TextField(label="Name", value=Template.Name if Template else "", dense=True, autofocus=True) Description = ft.TextField(label="Description", value=Template.Description if Template else "", dense=True) PatternField = ft.TextField(label="Patterns, separated by commas", value=", ".join(Template.Patterns) if Template else "", dense=True, multiline=True, max_lines=3) Suggested = ft.TextField(label="Suggested output (optional)", value=Template.SuggestedOutput if Template else "", dense=True) NewName = NameField(TheSetup, Template.NewFileName if Template else "", "/Output", Label="NewFileName (empty: the default)") Move = ft.Switch(label="Move files (off: copy them)", value=(Template.OutputSettings.get("DeleteOrg", 1) if Template else 1) == 1) Unzip = ft.TextField(label="Unzip these types (optional, commas)", dense=True, value=", ".join(Template.OutputSettings.get("Unzip", [])) if Template else "") SortDirs = ft.Switch(label="Input also sorts folders (SortDirs)", value=bool(Template and Template.InputSettings.get("SortDirs"))) def Save(e) -> None: if not (NameInput.value or "").strip(): return self.App.Close() OutputSettings = dict(Template.OutputSettings) if Template else {} OutputSettings["DeleteOrg"] = 1 if Move.value else 0 UnzipTypes = [Part.strip() for Part in (Unzip.value or "").split(",") if Part.strip()] if UnzipTypes: OutputSettings["Unzip"] = UnzipTypes else: OutputSettings.pop("Unzip", None) New = SortTemplate( NameInput.value.strip(), Description.value or "", Template.Icon if Template else "BOOKMARK_OUTLINED", [Part.strip() for Part in (PatternField.value or "").split(",") if Part.strip()], NewName.value, Suggested.value or "", OutputSettings, {"SortDirs": 1} if SortDirs.value else {}, ) if Template and Template.FilePath: DeleteUserTemplate(Template) SaveUserTemplate(New) self.Refresh() self.App.ShowDialog("Edit template" if Template else "New template", ft.Column([ NameInput, Description, PatternField, Suggested, NewName, Move, Unzip, SortDirs, ], tight=True, spacing=10, scroll=ft.ScrollMode.AUTO), [ ChipButton("Cancel", lambda e: self.App.Close()), ChipButton("Save", Save), ], Width=600)