mirror of
https://git.srhtnin.garden/sarah/SaraSort.git
synced 2026-10-15 02:53:17 +02:00
Flow edit mode: edit arrow rules, delete directories
Each rule on an arrow can be edited (pattern, NewFileName, case-sensitive, overwrite); a rule shared with other inputs is split so only this arrow changes. Connecting an input with a pattern an output already has no longer renames it for the other inputs. Boxes get a delete button that removes the directory from SaraSortd (GlobalConf, other outputs' rules, its config and history files) but never the folder itself. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
60da6dd9ab
commit
918e1d68cf
3 files changed
+232
-37
No files matched your search
+109
-28
@@ -178,6 +178,11 @@ class FlowPage(ft.Column):
|
||||
ft.Row([
|
||||
ft.Icon(ft.Icons.FOLDER_OUTLINED if Exists else ft.Icons.FOLDER_OFF_OUTLINED, color=Color, size=16),
|
||||
ft.Text(Name, size=15, color=Palette.Text, no_wrap=True, overflow=ft.TextOverflow.ELLIPSIS, expand=True),
|
||||
*([ft.Container(
|
||||
ft.Icon(ft.Icons.DELETE_OUTLINE, color=Palette.Red, size=16),
|
||||
tooltip="Delete from SaraSortd (the folder stays)", border_radius=4,
|
||||
on_click=lambda e, PathText=Node.Path: self._DeleteDialog(PathText),
|
||||
)] if self.EditMode else []),
|
||||
], spacing=6),
|
||||
ft.Text(DisplayPath(os.path.dirname(Node.Path)), size=11, color=Palette.Overlay0, no_wrap=True,
|
||||
overflow=ft.TextOverflow.ELLIPSIS),
|
||||
@@ -325,7 +330,8 @@ class FlowPage(ft.Column):
|
||||
elif self.Selected:
|
||||
self.Hint.value = f"Now click the directory {os.path.basename(self.Selected)} should sort into (or it again to cancel)."
|
||||
else:
|
||||
self.Hint.value = "Edit mode: click a directory, then the one it should sort into. Click a label to change or remove an arrow."
|
||||
self.Hint.value = ("Edit mode: click a directory, then the one it should sort into. Click a label to edit "
|
||||
"or remove an arrow's rules; the bin on a box deletes it from SaraSortd.")
|
||||
self.Hint.color = Palette.Yellow if self.EditMode else Palette.Overlay0
|
||||
|
||||
def _ToggleEdit(self, e) -> None:
|
||||
@@ -404,28 +410,85 @@ class FlowPage(ft.Column):
|
||||
|
||||
def _EdgeDialog(self, FoundEdge: Edge) -> None:
|
||||
TheSetup = self.TheSetup
|
||||
Entries = (TheSetup.LoadDirConf(FoundEdge.Output, "Out") or {}).get("Files", [])
|
||||
Editors: list[tuple] = [] # (Rule, Entry, PatternField, NameField, CaseSwitch, OverwriteSwitch)
|
||||
Rows = []
|
||||
for Rule in FoundEdge.Rules:
|
||||
Scope = "only from this input" if Rule.OwnInputs else "from every input this output accepts"
|
||||
Row = [
|
||||
ft.Text(Rule.Pattern, color=Palette.Mauve, width=150, selectable=True),
|
||||
Entry = Entries[Rule.Index] if Rule.Index < len(Entries) else {}
|
||||
Others = [os.path.basename(Other) for Other in TheSetup.RuleInputs(FoundEdge.Output, Rule.Index)
|
||||
if Other != FoundEdge.Input]
|
||||
if not self.EditMode:
|
||||
Scope = f"also from {', '.join(Others)}" if Others else "only from this input"
|
||||
Rows.append(ft.Row([
|
||||
ft.Text(Rule.Pattern, color=Palette.Mauve, width=150, selectable=True),
|
||||
ft.Column([ft.Text(f"named {Rule.NewFileName or '~^'}", size=12), Note(Scope, 11)], spacing=0, expand=True),
|
||||
]))
|
||||
continue
|
||||
PatternField = ft.TextField(label="Pattern", value=Rule.Pattern, dense=True, width=220)
|
||||
CaseSwitch = ft.Switch(label="Case-sensitive", value=Entry.get("CaseSensitive", 0) == 1)
|
||||
OverwriteSwitch = ft.Switch(label="Overwrite", value=Entry.get("Overwrite", 1) == 1)
|
||||
Name = NameField(TheSetup, Rule.NewFileName or "~^", FoundEdge.Output, Entry)
|
||||
Editors.append((Rule, Entry, PatternField, Name, CaseSwitch, OverwriteSwitch))
|
||||
Rows.append(ft.Container(
|
||||
ft.Column([
|
||||
ft.Text(f"named {Rule.NewFileName or '~^'}", size=12),
|
||||
Note(Scope, 11),
|
||||
], spacing=0, expand=True),
|
||||
]
|
||||
if self.EditMode:
|
||||
Row.append(ft.IconButton(
|
||||
ft.Icons.DELETE_OUTLINE, icon_color=Palette.Red, tooltip="Stop sending this pattern here",
|
||||
on_click=lambda e, Index=Rule.Index: RemoveOne(Index),
|
||||
))
|
||||
Rows.append(ft.Row(Row))
|
||||
ft.Row([
|
||||
PatternField, CaseSwitch, OverwriteSwitch, ft.Container(expand=True),
|
||||
ft.IconButton(ft.Icons.DELETE_OUTLINE, icon_color=Palette.Red, tooltip="Stop sending this pattern here",
|
||||
on_click=lambda e, Index=Rule.Index: RemoveOne(Index)),
|
||||
], vertical_alignment=ft.CrossAxisAlignment.CENTER),
|
||||
Name,
|
||||
Note(f"Also used by {', '.join(Others)}: changing it here gives this arrow its own copy." if Others
|
||||
else "Only this arrow uses it.", 11),
|
||||
], spacing=6),
|
||||
padding=10, bgcolor=Palette.Base, border=ft.Border.all(1, Palette.Surface1), border_radius=6,
|
||||
))
|
||||
|
||||
def Reopen() -> None:
|
||||
"""After a change: show the same arrow again, if it's still there."""
|
||||
self.Refresh()
|
||||
Again = next((Other for Other in self.EdgeList
|
||||
if Other.Input == FoundEdge.Input and Other.Output == FoundEdge.Output), None)
|
||||
if Again:
|
||||
self._EdgeDialog(Again)
|
||||
|
||||
def RemoveOne(Index: int) -> None:
|
||||
self.App.Close()
|
||||
Message = TheSetup.RemoveRule(FoundEdge.Input, FoundEdge.Output, Index)
|
||||
if Message:
|
||||
self.App.Notify(Message, "warning")
|
||||
Reopen()
|
||||
|
||||
def SaveRules(e) -> None:
|
||||
Edits = []
|
||||
for Rule, Entry, PatternField, Name, CaseSwitch, OverwriteSwitch in Editors:
|
||||
Pattern = (PatternField.value or "").strip()
|
||||
if not Pattern:
|
||||
PatternField.error = "A pattern is needed, e.g. *.png"
|
||||
PatternField.update()
|
||||
return
|
||||
Wanted = {
|
||||
"Pattern": Pattern,
|
||||
"NewFileName": Name.value or "~^",
|
||||
"CaseSensitive": 1 if CaseSwitch.value else 0,
|
||||
"Overwrite": 1 if OverwriteSwitch.value else 0,
|
||||
}
|
||||
Changes = {Key: Value for Key, Value in Wanted.items() if Entry.get(Key) != Value}
|
||||
if Changes:
|
||||
Edits.append((Rule.Index, Changes))
|
||||
self.App.Close()
|
||||
Messages = []
|
||||
# Highest index first: a split inserts a copy, which would shift later entries.
|
||||
for Index, Changes in sorted(Edits, key=lambda Item: Item[0], reverse=True):
|
||||
try:
|
||||
Message = TheSetup.EditRule(FoundEdge.Input, FoundEdge.Output, Index, Changes)
|
||||
except OSError as Error:
|
||||
Message = f"Couldn't save: {Error}"
|
||||
if Message:
|
||||
Messages.append(Message)
|
||||
if Messages:
|
||||
self.App.Notify(" ".join(Messages), "warning")
|
||||
elif Edits:
|
||||
self.App.Notify("Saved; SaraSortd uses it on its next check.", "success")
|
||||
self.Refresh()
|
||||
|
||||
def RemoveAll() -> None:
|
||||
@@ -449,17 +512,21 @@ class FlowPage(ft.Column):
|
||||
ChipButton("Remove arrow", lambda e: (self.App.Close(), self.App.Confirm(
|
||||
"Remove arrow", f"Stop sorting from {DisplayPath(FoundEdge.Input)} into {DisplayPath(FoundEdge.Output)}?", RemoveAll,
|
||||
)), ft.Icons.LINK_OFF, Accent=Palette.Red),
|
||||
ChipButton("Cancel", lambda e: self.App.Close()),
|
||||
ChipButton("Save", SaveRules, ft.Icons.SAVE_OUTLINED),
|
||||
]
|
||||
Actions.append(ChipButton("Close", lambda e: self.App.Close()))
|
||||
else:
|
||||
Actions.append(ChipButton("Close", lambda e: self.App.Close()))
|
||||
ListHeight = (186 if self.EditMode else 48) * len(Rows) + 8
|
||||
self.App.ShowDialog(
|
||||
"Arrow",
|
||||
ft.Column([
|
||||
ft.Text(f"{DisplayPath(FoundEdge.Input)} -> {DisplayPath(FoundEdge.Output)}", color=Palette.Mauve, selectable=True),
|
||||
ft.Divider(height=1, color=Palette.Surface1),
|
||||
ft.Column(Rows, scroll=ft.ScrollMode.AUTO, height=min(360, 48 * len(Rows) + 8), spacing=4),
|
||||
ft.Column(Rows, scroll=ft.ScrollMode.AUTO, height=min(460, ListHeight), spacing=8),
|
||||
Note("SaraSortd tries outputs in OutputDir order and patterns top to bottom; the first match wins."),
|
||||
], tight=True, spacing=8),
|
||||
Actions, Width=620,
|
||||
Actions, Width=760 if self.EditMode else 620,
|
||||
)
|
||||
|
||||
def _SaveTemplateDialog(self, FoundEdge: Edge) -> None:
|
||||
@@ -506,18 +573,9 @@ class FlowPage(ft.Column):
|
||||
if len(set(TheSetup.ConfFile(PathText, K) for K in Kinds)) == 1:
|
||||
break
|
||||
|
||||
def Remove() -> None:
|
||||
if TheSetup.RemoveDir(PathText):
|
||||
Positions = LoadLayout()
|
||||
Positions.pop(PathText, None)
|
||||
SaveLayout(Positions)
|
||||
self._AfterChange(True)
|
||||
|
||||
Actions = ConfButtons + [ChipButton("Open folder", lambda e: self.App.OpenFolder(PathText), ft.Icons.FOLDER_OPEN_OUTLINED)]
|
||||
Actions.append(ChipButton("Remove", lambda e: (self.App.Close(), self.App.Confirm(
|
||||
"Remove directory",
|
||||
f"Take {DisplayPath(PathText)} out of SaraSortd? The folder, its files and its config stay on disk.", Remove,
|
||||
)), ft.Icons.DELETE_OUTLINE, Accent=Palette.Red))
|
||||
Actions.append(ChipButton("Delete", lambda e: (self.App.Close(), self._DeleteDialog(PathText)),
|
||||
ft.Icons.DELETE_OUTLINE, Accent=Palette.Red))
|
||||
Actions.append(ChipButton("Close", lambda e: self.App.Close()))
|
||||
self.App.ShowDialog(
|
||||
os.path.basename(PathText) or PathText,
|
||||
@@ -531,6 +589,29 @@ class FlowPage(ft.Column):
|
||||
Actions, Width=620,
|
||||
)
|
||||
|
||||
def _DeleteDialog(self, PathText: str) -> None:
|
||||
TheSetup = self.TheSetup
|
||||
ConfNames = sorted({TheSetup.ConfFile(PathText, Kind).name for Kind in ("In", "Out")})
|
||||
|
||||
def Delete() -> None:
|
||||
try:
|
||||
Changed = TheSetup.DeleteDirectory(PathText)
|
||||
except OSError as Error:
|
||||
self.App.Notify(f"Couldn't delete it: {Error}", "error")
|
||||
return
|
||||
Positions = LoadLayout()
|
||||
Positions.pop(PathText, None)
|
||||
SaveLayout(Positions)
|
||||
self._AfterChange(Changed)
|
||||
|
||||
self.App.Confirm(
|
||||
"Delete directory",
|
||||
f"Delete {DisplayPath(PathText)} from SaraSortd?\n\n"
|
||||
f"It's taken out of GlobalConf.toml and other outputs' rules, and its config "
|
||||
f"({', '.join(ConfNames)}) and history file are deleted. The folder and its files stay.",
|
||||
Delete, "Delete",
|
||||
)
|
||||
|
||||
def _AddDirectoryDialog(self, e) -> None:
|
||||
if not self.TheSetup:
|
||||
return
|
||||
|
||||
Reference in new issue
Block a user