mirror of
https://git.srhtnin.garden/sarah/SaraSort.git
synced 2026-10-14 18:48:11 +02:00
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]>
This commit is contained in:
commit
f7ed23edf9
22 files changed
+3399
No files matched your search
@@ -0,0 +1,168 @@
|
||||
"""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)
|
||||
Reference in new issue
Block a user