mirror of
https://git.srhtnin.garden/sarah/SaraSort.git
synced 2026-10-13 18:28:18 +02:00
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]>
169 lines
9.7 KiB
Python
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)
|