mirror of
https://git.srhtnin.garden/sarah/SaraTag.git
synced 2026-10-13 18:28:16 +02:00
Windows support
This commit is contained in:
1 parent
9757518d8e
commit
8eb9943a30
14 files changed
+92
-17
No files matched your search
+4
-1
@@ -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
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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
|
||||
+1
-1
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
@@ -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"
|
||||
@@ -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 "$@"
|
||||
@@ -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}'
|
||||
@@ -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}
|
||||
+1
-1
@@ -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)
|
||||
|
||||
+34
-2
@@ -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(),
|
||||
|
||||
+7
-4
@@ -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")
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 7.0 KiB |
Reference in new issue
Block a user