diff --git a/.saratag/Core.py b/.saratag/Core.py index d74aadf..8d2229e 100755 --- a/.saratag/Core.py +++ b/.saratag/Core.py @@ -59,6 +59,9 @@ SaraTagRecordsPath = ApplicationDirectory / ".SaraTagRecords.toml" SaraTagHistoryPath = ApplicationDirectory / ".SaraTagHistory.toml" +# Empty means the default app; Windows rarely has mpv. +DefaultVideoPlayer = "" if sys.platform == "win32" else "mpv" + DefaultSaraTagConf = f'''# SaraTag's main portable configuration file. # Paths in this file are interpreted relative to the SaraTag directory # unless they are absolute. @@ -83,7 +86,7 @@ ShowInvalidEntries = true ShowImagePreviews = true [Media] -VideoPlayer = "mpv" +VideoPlayer = "{DefaultVideoPlayer}" [History] Enabled = true diff --git a/Books.py b/Books.py index 83d4759..1ac4d02 100644 --- a/Books.py +++ b/Books.py @@ -207,7 +207,7 @@ def RenameFile(SourcePath: Path, NewName: str) -> Path: """Renames a file and its Book together; undoes the file rename if the Book can't follow. Returns the new path.""" NewName = NewName.strip() - if not NewName or "/" in NewName or NewName in (".", ".."): + if not NewName or "/" in NewName or "\\" in NewName or NewName in (".", ".."): raise DataError("That isn't a valid file name.") if NewName.startswith("."): raise DataError("Names starting with a dot are hidden, and SaraTag skips hidden files.") diff --git a/Core.py b/Core.py index 6f091c7..25d2c10 100644 --- a/Core.py +++ b/Core.py @@ -23,9 +23,17 @@ AppVersion = "0.2.0-beta" BundledFontFamily = "Fixedsys Excelsior" ActiveFontFamily = BundledFontFamily # set by ApplyAppearance, read by BuildTheme +IsWindows = sys.platform == "win32" + +# Each system's own per-user folders, like SaraSuite's: ~/.config and +# ~/.local/share on Linux, %APPDATA% and %LOCALAPPDATA% on Windows. Home = Path.home() -ConfigHome = Home / ".config" -DataHome = Home / ".local" / "share" +if IsWindows: + ConfigHome = Path(os.environ.get("APPDATA") or Home / "AppData" / "Roaming") + DataHome = Path(os.environ.get("LOCALAPPDATA") or Home / "AppData" / "Local") +else: + ConfigHome = Path(os.environ.get("XDG_CONFIG_HOME") or Home / ".config") + DataHome = Path(os.environ.get("XDG_DATA_HOME") or Home / ".local" / "share") SharedConfigDir = ConfigHome / "SaraSuite" ThemesDir = SharedConfigDir / "Themes" @@ -222,6 +230,8 @@ def ChildEnvironment() -> dict[str, str]: Environment.pop("LD_LIBRARY_PATH", None) for Key in ("PYTHONHOME", "PYTHONPATH"): Environment.pop(Key, None) + if IsWindows: + Environment["PYTHONUTF8"] = "1" # every Sara program reads and writes UTF-8 return Environment @@ -301,8 +311,8 @@ def Heading(Text: str) -> ft.Text: def DisplayPath(PathText: str) -> str: - """/home/you/Pictures -> ~/Pictures, for labels.""" + """/home/you/Pictures -> ~/Pictures, for labels (C:\\Users\\you\\Pictures -> ~\\Pictures).""" HomeText = str(Home) - if PathText == HomeText or PathText.startswith(HomeText + "/"): + if PathText == HomeText or PathText.startswith(HomeText + os.sep): return "~" + PathText[len(HomeText):] return PathText diff --git a/Lexicons.py b/Lexicons.py index 163c650..5f4aff1 100644 --- a/Lexicons.py +++ b/Lexicons.py @@ -363,7 +363,7 @@ def KnownLexicons(Directories: list[Path]) -> list[Path]: def NewSharedLexicon(Name: str) -> Path: """An empty Lexicon in Lexicons/ for other Lexicons to inherit from.""" Name = Name.strip().removesuffix(".toml") - if not Name or "/" in Name or Name.startswith("."): + if not Name or "/" in Name or "\\" in Name or Name.startswith("."): raise DataError("That isn't a valid file name.") LexiconsDir.mkdir(parents=True, exist_ok=True) FilePath = LexiconsDir / f"{Name}.toml" diff --git a/Main.py b/Main.py index 8953b37..1f5ead9 100644 --- a/Main.py +++ b/Main.py @@ -4,7 +4,9 @@ The window and navigation; each page lives in its own *Page.py.""" import flet as ft from BrowsePage import BrowsePage -from Core import AppName, ApplyAppearance, AssetsDir, BuildTheme, BundledFontFamily, EffectiveSettings, LoadConfig, Palette +from Core import ( + AppName, ApplyAppearance, AssetsDir, BuildTheme, BundledFontFamily, EffectiveSettings, IsWindows, LoadConfig, Palette, +) from LexiconsPage import LexiconsPage from LibrariesPage import LibrariesPage from Library import TagCore @@ -40,6 +42,8 @@ def Main(Page: ft.Page) -> None: Page.bgcolor = Palette.Base Page.window.width = 1280 Page.window.height = 820 + if IsWindows: + Page.window.icon = str(AssetsDir / "Icon.ico") # taskbar/title bar (Linux: the desktop entry) Page.window.min_width = 900 Page.window.min_height = 560 diff --git a/Packaging/SaraSuite/Icon.png b/Packaging/SaraSuite/Icon.png new file mode 100644 index 0000000..1c39a33 Binary files /dev/null and b/Packaging/SaraSuite/Icon.png differ diff --git a/Packaging/SaraSuite/Manifest.toml b/Packaging/SaraSuite/Manifest.toml index 0b1e2dc..6b8fb54 100644 --- a/Packaging/SaraSuite/Manifest.toml +++ b/Packaging/SaraSuite/Manifest.toml @@ -1,5 +1,6 @@ # Kept in sync between SaraSuite/Programs/SaraTag/ and SaraTag/Packaging/SaraSuite/. Name = "SaraTag" +Platforms = ["Linux", "Windows"] Description = "Tag files with Books (sidecars) defined by Lexicons: browse, filter and sort by tag, edit tags on many files at once." [Version] @@ -20,18 +21,34 @@ RequirementsFile = "requirements.txt" Source = "Location.toml" Target = "{AppDir}/.saratag/Location.toml" # tells .saratag/Core.py where the data lives +# Linux: a launcher and a desktop entry. [[Templates]] +Platform = "Linux" Source = "Launch.sh" Target = "{AppDir}/Packaging/Launch.sh" Executable = true [[Templates]] +Platform = "Linux" Source = "SaraTag.desktop" Target = "{ApplicationsDir}/SaraTag.desktop" +# Windows: a Start Menu shortcut (pythonw.exe Main.py, from the app folder). +[[Shortcuts]] +Platform = "Windows" +Name = "SaraTag" +Script = "Main.py" +Icon = "assets/Icon.png" + # Made by SaraTag itself. The Lexicons are your tag definitions: only tick # "remove configs" if you don't need them any more. [[Leftovers]] Path = "{ConfigDir}" Kind = "Config" Note = "SaraTag's settings, Records, Libraries, history and Lexicons (Books next to your files are never touched)" + +# Shared with the other Flet programs; see Programs/FletIdentity.py. +[[Templates]] +Platform = "Linux" +Source = "../../FletIdentity.py" +Target = "{ProgramDir}/FletIdentity.py" diff --git a/Packaging/SaraSuite/Templates/Launch.sh b/Packaging/SaraSuite/Templates/Launch.sh index c606b43..70e1413 100644 --- a/Packaging/SaraSuite/Templates/Launch.sh +++ b/Packaging/SaraSuite/Templates/Launch.sh @@ -1,4 +1,8 @@ #!/bin/sh # Written by SaraSuite. Runs from the app folder so Flet finds assets/. +# A Flet client copy with the window app ID srhtnin.{Name}, so the desktop +# matches the window to {Name}.desktop (StartupWMClass) instead of showing it +# as "com.appveyor.flet". Empty if that fails: Flet then uses its own client. +export FLET_VIEW_PATH="$("{Python}" "{ProgramDir}/FletIdentity.py" "srhtnin.{Name}")" cd "{AppDir}" || exit 1 exec "{Python}" Main.py "$@" diff --git a/Packaging/SaraSuite/Templates/Location.toml b/Packaging/SaraSuite/Templates/Location.toml index 7d04cd3..ad2a9f1 100644 --- a/Packaging/SaraSuite/Templates/Location.toml +++ b/Packaging/SaraSuite/Templates/Location.toml @@ -1,3 +1,4 @@ # Written by SaraSuite. Where SaraTag keeps SaraTagConf.toml, its Records, # history and Lexicons (see .saratag/Core.py). -DataDir = "{ConfigDir}" +# A literal ('...') string: Windows paths are full of backslashes. +DataDir = '{ConfigDir}' diff --git a/Packaging/SaraSuite/Templates/SaraTag.desktop b/Packaging/SaraSuite/Templates/SaraTag.desktop index afcc970..a843cd0 100644 --- a/Packaging/SaraSuite/Templates/SaraTag.desktop +++ b/Packaging/SaraSuite/Templates/SaraTag.desktop @@ -5,5 +5,6 @@ Comment=Tag files with Books defined by Lexicons Exec={AppDir}/Packaging/Launch.sh Icon={AppDir}/assets/Icon.png Terminal=false -Categories=Utility;Graphics; +Categories=Utility; StartupNotify=true +StartupWMClass=srhtnin.{Name} diff --git a/Records.py b/Records.py index cddecb2..02cf692 100644 --- a/Records.py +++ b/Records.py @@ -253,7 +253,7 @@ def RenameRecordDirectory(Index: int, NewDirectoryName: str) -> Path: if TheRecord is None: raise DataError("That Record isn't in .SaraTagRecords.toml any more; reload.") NewDirectoryName = NewDirectoryName.strip() - if not NewDirectoryName or "/" in NewDirectoryName or NewDirectoryName.startswith("."): + if not NewDirectoryName or "/" in NewDirectoryName or "\\" in NewDirectoryName or NewDirectoryName.startswith("."): raise DataError("That isn't a valid directory name.") OldPath = TheRecord.Directory NewPath = OldPath.with_name(NewDirectoryName) diff --git a/Settings.py b/Settings.py index 3f96634..597a55f 100644 --- a/Settings.py +++ b/Settings.py @@ -12,7 +12,7 @@ import tomlkit from Core import ( AppConfig, AvailableThemes, BundledFontFamily, ChildEnvironment, ChipButtonStyle, ExportSettings, Home, - ImportSettings, LoadSharedSettings, Palette, SharedSettings, + ImportSettings, IsWindows, LoadSharedSettings, Palette, SharedSettings, ) from Library import ConfPath, ConfValue, DataDir, DataError, SaveConfValues from Widgets import App @@ -28,8 +28,40 @@ def _Note(Text: str) -> ft.Text: return ft.Text(Text, color=Palette.Overlay0, size=12) +def _WindowsFonts() -> set[str]: + """Font families from the system and per-user Fonts registry keys + ("Arial Bold (TrueType)" -> "Arial").""" + import re + import winreg + + Families: set[str] = set() + Key = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts" + for Root in (winreg.HKEY_LOCAL_MACHINE, winreg.HKEY_CURRENT_USER): + try: + with winreg.OpenKey(Root, Key) as Fonts: + Index = 0 + while True: + try: + Name = winreg.EnumValue(Fonts, Index)[0] + except OSError: + break + Index += 1 + Name = re.sub(r"\s*\(.*\)$", "", Name) + Name = re.sub(r"\s+(Bold|Italic|Light|Semibold|SemiBold|Black|Regular|Medium|Thin)\b.*$", "", Name) + if Name and "&" not in Name: + Families.add(Name.strip()) + except OSError: + continue + return Families + + def _Fonts() -> list[str]: - """Installed font families (fontconfig), with the bundled font first.""" + """Installed font families (fontconfig, or the registry on Windows), with + the bundled font first.""" + if IsWindows: + Families = _WindowsFonts() + Families.discard(BundledFontFamily) + return [BundledFontFamily] + sorted(Families, key=str.lower) try: Result = subprocess.run( ["fc-list", ":", "family"], capture_output=True, text=True, timeout=5, env=ChildEnvironment(), diff --git a/Widgets.py b/Widgets.py index ba9dc23..22c1b2b 100644 --- a/Widgets.py +++ b/Widgets.py @@ -12,7 +12,7 @@ from typing import Callable, Optional import flet as ft -from Core import AppConfig, ChildEnvironment, ChipButton, Note, NotificationStyle, Palette +from Core import AppConfig, ChildEnvironment, ChipButton, IsWindows, Note, NotificationStyle, Palette from Library import ConfValue @@ -72,7 +72,7 @@ class App: else to the default app.""" from Query import VideoExtensions - Command = ["xdg-open", str(TargetPath)] + Command = None if IsWindows else ["xdg-open", str(TargetPath)] # Windows: os.startfile below if TargetPath.suffix.lower() in VideoExtensions: Player = str(ConfValue("Media", "VideoPlayer", "") or "").strip() if Player and shutil.which(Player.split()[0]): @@ -80,8 +80,11 @@ class App: elif Player: self.Notify(f"Video player '{Player}' isn't installed; using the default app.", "warning") try: - subprocess.Popen(Command, env=ChildEnvironment(), start_new_session=True, - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + if Command is None: + os.startfile(str(TargetPath)) # the default app + else: + subprocess.Popen(Command, env=ChildEnvironment(), start_new_session=True, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) except OSError as Error: self.Notify(f"Couldn't open {TargetPath.name}: {Error}", "error") diff --git a/assets/Icon.ico b/assets/Icon.ico new file mode 100644 index 0000000..ec5faed Binary files /dev/null and b/assets/Icon.ico differ