Files
SaraSort/TemplatesPage.py
T
sarahandClaude Opus 5.5 f7ed23edf9 SaraSort: Flet GUI for configuring SaraSortd
Flowchart of inputs/outputs with editable arrows, service page (status,
controls, autostart, version, logs), config editor (form + raw), templates,
tools (warnings, filename tester, failed files) and settings with
SaraFem-compatible export/import. AppImage build and a SaraSuite manifest
in Packaging/.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-07 22:23:36 +02:00

169 lines
9.7 KiB
Python

"""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)