From a7cb289c982c56b3d00461526df3311ba97ca43c Mon Sep 17 00:00:00 2001 From: Akaza Renn <8340896+AkazaRenn@users.noreply.github.com> Date: Mon, 17 Jun 2024 21:03:10 -0400 Subject: [PATCH 01/19] Enable sync libraries in the backend --- py_modules/plugin_config.py | 9 ++++++++- py_modules/rclone_sync_manager.py | 17 +++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/py_modules/plugin_config.py b/py_modules/plugin_config.py index 2c12cc1..98f95f6 100644 --- a/py_modules/plugin_config.py +++ b/py_modules/plugin_config.py @@ -14,7 +14,7 @@ cfg_syncpath_filter_file = config_dir / "sync_paths_filter.txt" cfg_property_file = config_dir / "plugin.properties" -def get_config(): +def get_config(): """ Reads and parses the plugin configuration file. @@ -108,3 +108,10 @@ def migrate(): set_config("sync_on_game_exit", "true") if not any(e[0] == "toast_auto_sync" for e in config): set_config("toast_auto_sync", "true") + if not any(e[0] == "library_sync" for e in config): + set_config("library_sync", { + "Documents": {"enabled": False, "destination": "Documents"}, + "Music": {"enabled": False, "destination": "Music"}, + "Pictures": {"enabled": False, "destination": "Pictures"}, + "Videos": {"enabled": False, "destination": "Videos"}, + }) diff --git a/py_modules/rclone_sync_manager.py b/py_modules/rclone_sync_manager.py index 868bb81..492815e 100644 --- a/py_modules/rclone_sync_manager.py +++ b/py_modules/rclone_sync_manager.py @@ -5,6 +5,7 @@ import plugin_config import os import os.path +from pathlib import Path class RcloneSyncManager: @@ -21,6 +22,16 @@ async def delete_lock_files(self): await create_subprocess_exec(*cmd) async def sync_now(self, winner: str, resync: bool): + destination_path = plugin_config.get_config_item( + "destination_directory", "decky-cloud-save") + self.sync_now_internal(["/", f"backend:{destination_path}", "--filter-from", + plugin_config.cfg_syncpath_filter_file], winner, resync) + + for k, v in plugin_config.get_config_item("library_sync", {}).items(): + if v.get("enabled", False): + self.sync_now_internal([str(Path.home() / k), f"backend:{v.get('destination', f'decky-cloud-save/{k}')}"], winner, resync) + + async def sync_now_internal(self, path_args: list, winner: str, resync: bool): """ Initiates a synchronization process using rclone. @@ -31,8 +42,6 @@ async def sync_now(self, winner: str, resync: bool): """ bisync_enabled = plugin_config.get_config_item( "bisync_enabled", "false") == "true" - destination_path = plugin_config.get_config_item( - "destination_directory", "decky-cloud-save") args = [] if bisync_enabled: @@ -42,8 +51,8 @@ async def sync_now(self, winner: str, resync: bool): args.extend(["copy"]) decky_plugin.logger.debug("Using copy") - args.extend(["/", f"backend:{destination_path}", "--filter-from", - plugin_config.cfg_syncpath_filter_file, "--copy-links"]) + args.extend(path_args) + args.extend(["--copy-links"]) if bisync_enabled: if resync: args.extend(["--resync-mode", winner, "--resync"]) From 80273c2e599f40ccbbda4af27a1555aa5d475e6d Mon Sep 17 00:00:00 2001 From: Akaza Renn <8340896+AkazaRenn@users.noreply.github.com> Date: Tue, 18 Jun 2024 20:26:07 -0400 Subject: [PATCH 02/19] Update settings format --- py_modules/plugin_config.py | 29 ++++++++++++++++++++++------- py_modules/rclone_sync_manager.py | 9 ++++----- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/py_modules/plugin_config.py b/py_modules/plugin_config.py index 98f95f6..ce84a4e 100644 --- a/py_modules/plugin_config.py +++ b/py_modules/plugin_config.py @@ -1,6 +1,7 @@ import os from pathlib import Path import decky_plugin +from settings import SettingsManager as settings_manager # Plugin directories and files plugin_dir = Path(decky_plugin.DECKY_PLUGIN_DIR) @@ -14,6 +15,8 @@ cfg_syncpath_filter_file = config_dir / "sync_paths_filter.txt" cfg_property_file = config_dir / "plugin.properties" +library_sync_config = settings_manager(name="library_sync", settings_directory=decky_plugin.DECKY_PLUGIN_SETTINGS_DIR) + def get_config(): """ Reads and parses the plugin configuration file. @@ -82,6 +85,19 @@ def regenerate_filter_file(): f.write("\n") f.write("- **\n") +def get_library_sync_config(key: str = None): + """ + Retrieves the library sync configuration. + + Returns: + dict: The library sync configuration. + """ + library_sync_config.read() + if key: + return library_sync_config.get(key, {"enabled": False, "destination": "key"}) + else: + return library_sync_config + def migrate(): """ Performs migration tasks if necessary, like creating directories and files, and setting default configurations. @@ -108,10 +124,9 @@ def migrate(): set_config("sync_on_game_exit", "true") if not any(e[0] == "toast_auto_sync" for e in config): set_config("toast_auto_sync", "true") - if not any(e[0] == "library_sync" for e in config): - set_config("library_sync", { - "Documents": {"enabled": False, "destination": "Documents"}, - "Music": {"enabled": False, "destination": "Music"}, - "Pictures": {"enabled": False, "destination": "Pictures"}, - "Videos": {"enabled": False, "destination": "Videos"}, - }) + + if not get_library_sync_config().settings: + get_library_sync_config().setSetting("Documents", {"enabled": False, "destination": "Documents"}) + get_library_sync_config().setSetting("Music", {"enabled": False, "destination": "Music"}) + get_library_sync_config().setSetting("Pictures", {"enabled": False, "destination": "Pictures"}) + get_library_sync_config().setSetting("Videos", {"enabled": False, "destination": "Videos"}) diff --git a/py_modules/rclone_sync_manager.py b/py_modules/rclone_sync_manager.py index 492815e..c4231b5 100644 --- a/py_modules/rclone_sync_manager.py +++ b/py_modules/rclone_sync_manager.py @@ -22,14 +22,13 @@ async def delete_lock_files(self): await create_subprocess_exec(*cmd) async def sync_now(self, winner: str, resync: bool): - destination_path = plugin_config.get_config_item( - "destination_directory", "decky-cloud-save") - self.sync_now_internal(["/", f"backend:{destination_path}", "--filter-from", + destination_path = plugin_config.get_config_item("destination_directory", "decky-cloud-save") + await self.sync_now_internal(["/", f"backend:{destination_path}", "--filter-from", plugin_config.cfg_syncpath_filter_file], winner, resync) - for k, v in plugin_config.get_config_item("library_sync", {}).items(): + for k, v in plugin_config.get_library_sync_config().settings.items(): if v.get("enabled", False): - self.sync_now_internal([str(Path.home() / k), f"backend:{v.get('destination', f'decky-cloud-save/{k}')}"], winner, resync) + await self.sync_now_internal([str(Path.home() / k), f"backend:{v.get('destination', f'deck-libraries/{k}')}"], winner, resync) async def sync_now_internal(self, path_args: list, winner: str, resync: bool): """ From 5486fca47de4df39d086ca05f743609792e0f13e Mon Sep 17 00:00:00 2001 From: Akaza Renn <8340896+AkazaRenn@users.noreply.github.com> Date: Tue, 18 Jun 2024 20:30:19 -0400 Subject: [PATCH 03/19] Remove unrelated change --- assets/languages/zh.json | 53 --------------------------------------- src/helpers/translator.ts | 9 +++---- 2 files changed, 3 insertions(+), 59 deletions(-) delete mode 100644 assets/languages/zh.json diff --git a/assets/languages/zh.json b/assets/languages/zh.json deleted file mode 100644 index 90aac13..0000000 --- a/assets/languages/zh.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "sync": "同步", - "sync.now": "立即同步", - "resync.now": "立即重新同步", - "provider.not.configured": "未配置雲存儲提供商。請在「雲存儲提供商」中配置", - "sync.start.stop": "在遊戲開始和結束時同步", - "toast.auto.sync": "自動同步後彈出通知", - "configuration": "配置", - "sync.paths": "同步路徑", - "cloud.provider": "雲存儲提供商", - "experimental.use.risk": "實驗性功能", - "bidirectional.sync": "双向同步", - "you.mad": "你瘋了嗎??", - "warning.root": "為了你的個人安全,你無法同步整個檔案系統", - "confirm.add": "確認添加", - "confirm.add.path": "路徑 {{path}} 對應 {{count}} 個檔案。繼續?", - "select.path": "選擇要同步的路徑", - "file": "檔案", - "folder": "資料夾", - "folder.exclude": "資料夾 (排除子資料夾)", - "add.to.sync": "將路徑添加到同步", - "exclude.from.sync": "將路徑從同步中排除", - "confirm.remove": "確認移除", - "removing.path": "移除路徑 {{path}}。繼續?", - "includes.vs.exclude": "包含或排除", - "help.exclude": "自 v1.2.0 起, 特定資料夾可被從同步中排除。\n\n同步時,本插件會先檢查排除列表以確認某檔案(或資料夾)是否被排除,其後它才會檢查包含列表內的文件。\n\n舉例,若資料夾 /a/** 在包含列表內, 但檔案 /a/b 在排除列表內,則除 b 以外的檔案將會被備份。", - "include.or.exclude.subf": "包含或排除子資料夾", - "help.include.or.exclude.subf": "有時插件只需要備份資料夾本身而不需要繼續搜尋子資料夾。在這種情況下,你可以使用「資料夾 (排除子資料夾)」選項。\n\n此種情況十分罕見,多數時期應使用「資料夾」選項。", - "bug.file.picker": "臭蟲:檔案選擇器無限期載入", - "help.file.picker.fail": "全新安裝後,檔案選擇器可能無法載入。重新啟動 Steam 即可解決。\n詳情請參考討論區", - "cloud.save.path": "雲存儲路徑", - "configure.provider": "配置雲存儲提供商", - "currently.using": "使用中", - "click.providers": "點擊以下提供商之一來配置備份目的路徑", - "other.providers": "添加其他提供商", - "manually.desktop": "除上列兩家提供商外,也可以配置其他提供商,但該配置只能在桌面模式下完成。\n\n針對某些提供商(例如 Google 雲端硬碟)的安裝腳本已經就緒,請直接運行插件安裝目錄下的安裝腳本(預設為:/home/deck/homebrew/plugins/decky-cloud-save/quickstart/)。\n\n針對其他提供商,請參閱 README.md。", - "other.advanced": "其他(進階)", - "includes": "包含", - "excludes": "排除", - "waiting.previous": "正等待前一次同步", - "synchronizing.savedata": "同步儲存資料(savedata)", - "sync.failed": "出錯。點擊此處以查看該錯誤。", - "sync.completed": "同步完成,共耗時 {{time}} 秒", - "sync.conflict": "同步時遭遇衝突。", - "rclone.error.logs": "日誌同步出錯", - "resynchronizing.savedata": "重新同步儲存資料(savedata),請稍候", - "log.files": "日誌檔案", - "app.logs": "插件日誌", - "sync.logs": "同步日誌", - "no.available.logs": "無可用日誌", - "deleting.locks.sync": "刪除鎖定並同步", - "delete.locks": "刪除鎖定檔案(locks)" -} \ No newline at end of file diff --git a/src/helpers/translator.ts b/src/helpers/translator.ts index fe6c73c..57a690c 100644 --- a/src/helpers/translator.ts +++ b/src/helpers/translator.ts @@ -3,7 +3,6 @@ import spanish from "../../assets/languages/es.json"; import french from "../../assets/languages/fr.json"; import portuguese from "../../assets/languages/pt.json"; import german from "../../assets/languages/de.json"; -import chinese from "../../assets/languages/zh.json"; import { Logger } from "./logger"; /** @@ -15,8 +14,7 @@ enum Language { latam, french, portuguese, - german, - chinese + german } /** @@ -29,7 +27,7 @@ export class Translator { */ private constructor(){ } - + /** * An object that maps languages to their respective dictionaries. */ @@ -39,8 +37,7 @@ export class Translator { [Language.latam]: spanish, [Language.french]: french, [Language.portuguese]: portuguese, - [Language.german]: german, - [Language.chinese]: chinese + [Language.german]: german }; /** From f7abd39e97eca44a9459cce882aed9b3e4ee2c72 Mon Sep 17 00:00:00 2001 From: Akaza Renn <8340896+AkazaRenn@users.noreply.github.com> Date: Tue, 18 Jun 2024 22:23:56 -0400 Subject: [PATCH 04/19] Dummy UI, can get config from backend --- assets/languages/en.json | 8 +++- main.py | 10 +++++ py_modules/plugin_config.py | 32 ++++++++++++--- src/helpers/state.ts | 32 ++++++++++++--- src/pages/ConfigurePathsPage.tsx | 68 +++++++++++++++++++++++++++++++- 5 files changed, 136 insertions(+), 14 deletions(-) diff --git a/assets/languages/en.json b/assets/languages/en.json index 576bbae..813d76c 100644 --- a/assets/languages/en.json +++ b/assets/languages/en.json @@ -49,5 +49,11 @@ "sync.logs": "Sync logs", "no.available.logs": "No available logs", "deleting.locks.sync": "Deleting locks and syncing", - "delete.locks": "Delete locks" + "delete.locks": "Delete locks", + "library.sync": "Library Sync", + "documents": "Documents", + "music": "Music", + "pictures": "Pictures", + "videos": "Videos", + "enabled": "Enabled" } \ No newline at end of file diff --git a/main.py b/main.py index 07bb0fd..21eda1c 100644 --- a/main.py +++ b/main.py @@ -93,6 +93,16 @@ async def set_config(self, key: str, value: str): decky_plugin.logger.debug("Executing: set_config(%s, %s)", key, value) plugin_config.set_config(key, value) + async def get_library_sync_config(self, key: str = None): + decky_plugin.logger.debug( + f"Executing: get_library_sync_config({key})") + return plugin_config.get_library_sync_config(key) + + async def set_library_sync_config(self, key: str, enabled: bool = None, destination: str = None): + decky_plugin.logger.debug( + f"Executing: set_library_sync_config({key}:{{enabled:{enabled},destination:{destination}}})") + plugin_config.set_library_sync_config(key, enabled, destination) + # Logger async def log(self, level: str, msg: str) -> int: diff --git a/py_modules/plugin_config.py b/py_modules/plugin_config.py index ce84a4e..d40f28a 100644 --- a/py_modules/plugin_config.py +++ b/py_modules/plugin_config.py @@ -89,6 +89,9 @@ def get_library_sync_config(key: str = None): """ Retrieves the library sync configuration. + Parameters: + key (str, optional): The key of the value to retrieve. If not provided, the entire configuration will be returned. + Returns: dict: The library sync configuration. """ @@ -96,7 +99,24 @@ def get_library_sync_config(key: str = None): if key: return library_sync_config.get(key, {"enabled": False, "destination": "key"}) else: - return library_sync_config + return library_sync_config.settings + +def set_library_sync_config(key: str, enabled: bool = None, destination: str = None): + """ + Sets the library sync configuration. + + Parameters: + key (str): The key to set. + enabled (bool): Whether the key is enabled. + destination (str): The destination of the key. + """ + library_sync_config.read() + if enabled == None: + enabled = library_sync_config.get(key, {}).get("enabled", False) + if destination == None: + destination = library_sync_config.get(key, {}).get("destination", f"deck-libraries/{key}") + + library_sync_config.setSetting(key, {"enabled": enabled, "destination": destination}) def migrate(): """ @@ -125,8 +145,8 @@ def migrate(): if not any(e[0] == "toast_auto_sync" for e in config): set_config("toast_auto_sync", "true") - if not get_library_sync_config().settings: - get_library_sync_config().setSetting("Documents", {"enabled": False, "destination": "Documents"}) - get_library_sync_config().setSetting("Music", {"enabled": False, "destination": "Music"}) - get_library_sync_config().setSetting("Pictures", {"enabled": False, "destination": "Pictures"}) - get_library_sync_config().setSetting("Videos", {"enabled": False, "destination": "Videos"}) + if not get_library_sync_config(): + set_library_sync_config("Documents") + set_library_sync_config("Music") + set_library_sync_config("Pictures") + set_library_sync_config("Videos") diff --git a/src/helpers/state.ts b/src/helpers/state.ts index 84bf33d..7daa941 100644 --- a/src/helpers/state.ts +++ b/src/helpers/state.ts @@ -9,7 +9,20 @@ type State = { experimental_menu: string; toast_auto_sync: string; destination_directory: string; - playing: string + playing: string; + documents_sync: LibrarySyncState; + pictures_sync: LibrarySyncState; + videos_sync: LibrarySyncState; + music_sync: LibrarySyncState; +}; + +interface LibrarySyncState { + enabled: boolean; + destination: string; +}; + +type LibrarySyncDict = { + [key: string]: LibrarySyncState; }; class AppState { @@ -22,7 +35,11 @@ class AppState { experimental_menu: "false", toast_auto_sync: "true", destination_directory: "decky-cloud-save", - playing: "false" + playing: "false", + documents_sync: { enabled: false, destination: "deck-libraries/Documents" }, + pictures_sync: { enabled: false, destination: "deck-libraries/Pictures" }, + videos_sync: { enabled: false, destination: "deck-libraries/Videos" }, + music_sync: { enabled: false, destination: "deck-libraries/Music" }, }; private _serverApi: ServerAPI = null!; @@ -53,15 +70,18 @@ class AppState { } else { Logger.error(data); } + + const libSyncData = await serverApi.callPluginMethod<{}, LibrarySyncDict>("get_library_sync_config", {}); + console.log(libSyncData); } - public setState = (key: keyof State, value: string, persist = false) => { + public setState = (key: keyof State, value: string | LibrarySyncState, persist = false) => { this._currentState = { ...this.currentState, [key]: value }; Logger.debug("Setting '" + key + "' to '" + value + "' with persistence: " + persist); if (persist) { - this.serverApi.callPluginMethod<{ key: string; value: string }, null>("set_config", { key, value }).then(e => Logger.debug(e)); + this.serverApi.callPluginMethod<{ key: string; value: string | LibrarySyncState }, null>("set_config", { key, value }).then(e => Logger.debug(e)); } this._subscribers.forEach((e) => e.callback(this.currentState)); @@ -97,9 +117,9 @@ export class ApplicationState { private constructor(){ } - + private static appState = new AppState(); - + public static async initialize(serverApi: ServerAPI) { await this.appState.initialize(serverApi); } diff --git a/src/pages/ConfigurePathsPage.tsx b/src/pages/ConfigurePathsPage.tsx index 2181139..fe6bbcb 100644 --- a/src/pages/ConfigurePathsPage.tsx +++ b/src/pages/ConfigurePathsPage.tsx @@ -1,4 +1,4 @@ -import { PanelSectionRow, PanelSection, TextField } from "decky-frontend-lib"; +import { PanelSectionRow, PanelSection, TextField, ToggleField } from "decky-frontend-lib"; import { useEffect, useState } from "react"; import { PageProps } from "../helpers/types"; import AddNewPathButton from "../components/AddNewPathButton"; @@ -24,6 +24,9 @@ export default function ConfigurePathsPage({ serverApi }: PageProps<{}>) { useEffect(() => onPathsUpdated(), []); return ( + +
+ ) { ))} + + + + +
+
+ +
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+
); } From 40df626fc5bf9b3ba1109d3caef9740283044893 Mon Sep 17 00:00:00 2001 From: Renn <8340896+AkazaRenn@users.noreply.github.com> Date: Tue, 18 Jun 2024 23:35:00 -0400 Subject: [PATCH 05/19] fix getting config dict --- py_modules/rclone_sync_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py_modules/rclone_sync_manager.py b/py_modules/rclone_sync_manager.py index c4231b5..3e7ddfe 100644 --- a/py_modules/rclone_sync_manager.py +++ b/py_modules/rclone_sync_manager.py @@ -26,7 +26,7 @@ async def sync_now(self, winner: str, resync: bool): await self.sync_now_internal(["/", f"backend:{destination_path}", "--filter-from", plugin_config.cfg_syncpath_filter_file], winner, resync) - for k, v in plugin_config.get_library_sync_config().settings.items(): + for k, v in plugin_config.get_library_sync_config().items(): if v.get("enabled", False): await self.sync_now_internal([str(Path.home() / k), f"backend:{v.get('destination', f'deck-libraries/{k}')}"], winner, resync) From ce859bc72a026174b133fa5e857b771c87b5cc7f Mon Sep 17 00:00:00 2001 From: Akaza Renn <8340896+AkazaRenn@users.noreply.github.com> Date: Wed, 19 Jun 2024 21:32:20 -0400 Subject: [PATCH 06/19] Link UI toggles/inputs with backend --- main.py | 6 +- py_modules/plugin_config.py | 12 +-- py_modules/rclone_sync_manager.py | 23 +++-- src/helpers/libSyncState.ts | 134 ++++++++++++++++++++++++++++++ src/helpers/state.ts | 32 ++----- src/index.tsx | 14 ++-- src/pages/ConfigurePathsPage.tsx | 94 +++++++++------------ 7 files changed, 212 insertions(+), 103 deletions(-) create mode 100644 src/helpers/libSyncState.ts diff --git a/main.py b/main.py index 21eda1c..8ccb771 100644 --- a/main.py +++ b/main.py @@ -98,10 +98,10 @@ async def get_library_sync_config(self, key: str = None): f"Executing: get_library_sync_config({key})") return plugin_config.get_library_sync_config(key) - async def set_library_sync_config(self, key: str, enabled: bool = None, destination: str = None): + async def set_library_sync_config(self, key: str, enabled, bisync, destination): decky_plugin.logger.debug( - f"Executing: set_library_sync_config({key}:{{enabled:{enabled},destination:{destination}}})") - plugin_config.set_library_sync_config(key, enabled, destination) + f"Executing: set_library_sync_config({key}:{{enabled:{enabled},bisync:{bisync},destination:{destination}}})") + plugin_config.set_library_sync_config(key=key, enabled=enabled, bisync=bisync, destination=destination) # Logger diff --git a/py_modules/plugin_config.py b/py_modules/plugin_config.py index d40f28a..c1e74e3 100644 --- a/py_modules/plugin_config.py +++ b/py_modules/plugin_config.py @@ -97,11 +97,11 @@ def get_library_sync_config(key: str = None): """ library_sync_config.read() if key: - return library_sync_config.get(key, {"enabled": False, "destination": "key"}) + return library_sync_config.getSetting(key, {"enabled": False, "bisync": False, "destination": f"deck-libraries/{key}"}) else: return library_sync_config.settings -def set_library_sync_config(key: str, enabled: bool = None, destination: str = None): +def set_library_sync_config(key: str, enabled: bool = None, bisync: bool = None, destination: str = None): """ Sets the library sync configuration. @@ -112,11 +112,13 @@ def set_library_sync_config(key: str, enabled: bool = None, destination: str = N """ library_sync_config.read() if enabled == None: - enabled = library_sync_config.get(key, {}).get("enabled", False) + enabled = library_sync_config.getSetting(key, {}).get("enabled", False) + if bisync == None: + bisync = library_sync_config.getSetting(key, {}).get("bisync", False) if destination == None: - destination = library_sync_config.get(key, {}).get("destination", f"deck-libraries/{key}") + destination = library_sync_config.getSetting(key, {}).get("destination", f"deck-libraries/{key}") - library_sync_config.setSetting(key, {"enabled": enabled, "destination": destination}) + library_sync_config.setSetting(key, {"enabled": enabled, "bisync": bisync, "destination": destination}) def migrate(): """ diff --git a/py_modules/rclone_sync_manager.py b/py_modules/rclone_sync_manager.py index 3e7ddfe..ead2992 100644 --- a/py_modules/rclone_sync_manager.py +++ b/py_modules/rclone_sync_manager.py @@ -23,14 +23,23 @@ async def delete_lock_files(self): async def sync_now(self, winner: str, resync: bool): destination_path = plugin_config.get_config_item("destination_directory", "decky-cloud-save") - await self.sync_now_internal(["/", f"backend:{destination_path}", "--filter-from", - plugin_config.cfg_syncpath_filter_file], winner, resync) + await self.sync_now_internal( + ["/", f"backend:{destination_path}", "--filter-from", plugin_config.cfg_syncpath_filter_file], + plugin_config.get_config_item("bisync_enabled", "false") == "true", + winner, + resync + ) for k, v in plugin_config.get_library_sync_config().items(): if v.get("enabled", False): - await self.sync_now_internal([str(Path.home() / k), f"backend:{v.get('destination', f'deck-libraries/{k}')}"], winner, resync) - - async def sync_now_internal(self, path_args: list, winner: str, resync: bool): + await self.sync_now_internal( + [str(Path.home() / k), f"backend:{v.get('destination', f'deck-libraries/{k}')}"], + v.get("bisync", False), + winner, + resync + ) + + async def sync_now_internal(self, path_args: list, bisync: bool, winner: str, resync: bool): """ Initiates a synchronization process using rclone. @@ -39,11 +48,9 @@ async def sync_now_internal(self, path_args: list, winner: str, resync: bool): resync (bool): Whether to perform a resynchronization. """ - bisync_enabled = plugin_config.get_config_item( - "bisync_enabled", "false") == "true" args = [] - if bisync_enabled: + if bisync: args.extend(["bisync"]) decky_plugin.logger.debug("Using bisync") else: diff --git a/src/helpers/libSyncState.ts b/src/helpers/libSyncState.ts new file mode 100644 index 0000000..5fbdb6f --- /dev/null +++ b/src/helpers/libSyncState.ts @@ -0,0 +1,134 @@ +import { ServerAPI } from "decky-frontend-lib"; +import { useEffect, useState } from "react"; +import { Logger } from "./logger"; + +export type LibrarySyncState = { + Documents: LibrarySyncStateEntry; + Music: LibrarySyncStateEntry; + Pictures: LibrarySyncStateEntry; + Videos: LibrarySyncStateEntry; +}; + +interface LibrarySyncStateEntry { + enabled: boolean; + bisync: boolean; + destination: string; +}; + +class AppState { + private _subscribers: { id: number; callback: (e: LibrarySyncState) => void }[] = []; + + private _currentState: LibrarySyncState = { + Documents: { enabled: false, bisync: false, destination: "deck-libraries/Documents" }, + Music: { enabled: false, bisync: false, destination: "deck-libraries/Music" }, + Pictures: { enabled: false, bisync: false, destination: "deck-libraries/Pictures" }, + Videos: { enabled: false, bisync: false, destination: "deck-libraries/Videos" }, + }; + + private _serverApi: ServerAPI = null!; + private _alreadyInit: boolean = true!; + + public get currentState() { + return this._currentState; + } + + public get serverApi() { + return this._serverApi; + } + + public get alreadyInit() { + return this._alreadyInit; + } + + public async initialize(serverApi: ServerAPI) { + if (this._serverApi == null) { + this._alreadyInit = false; + } + + this._serverApi = serverApi; + + const data = await serverApi.callPluginMethod<{}, LibrarySyncState>("get_library_sync_config", {}); + if (data.success) { + this.setState("Documents", { enabled: data.result.Documents.enabled, bisync: data.result.Documents.bisync, destination: data.result.Documents.destination }); + this.setState("Music", { enabled: data.result.Music.enabled, bisync: data.result.Music.bisync, destination: data.result.Music.destination }); + this.setState("Pictures", { enabled: data.result.Pictures.enabled, bisync: data.result.Pictures.bisync, destination: data.result.Pictures.destination }); + this.setState("Videos", { enabled: data.result.Videos.enabled, bisync: data.result.Videos.bisync, destination: data.result.Videos.destination }); + } else { + Logger.error(data); + } + } + + public setState = (key: keyof LibrarySyncState, values: Partial<{ enabled: boolean; bisync: boolean; destination: string }>, persist = false) => { + let entry = this._currentState[key]; + if (values.enabled !== undefined) { + entry.enabled = values.enabled; + } + if (values.bisync !== undefined) { + entry.bisync = values.bisync; + } + if (values.destination !== undefined) { + entry.destination = values.destination; + } + + if (persist) { + this.serverApi + .callPluginMethod<{ key: string; enabled: boolean; bisync: boolean, destination: string }, void>("set_library_sync_config", { + key, + enabled: entry.enabled, + bisync: entry.bisync, + destination: entry.destination, + }) + .then(e => Logger.debug(e)); + } + + this._subscribers.forEach((e) => e.callback(this.currentState)); + } + + public subscribe = (callback: (e: LibrarySyncState) => void) => { + const id = new Date().getTime(); + this._subscribers.push({ id, callback }); + + return id; + }; + + public unsubscribe = (id: number) => { + const index = this._subscribers.findIndex((f) => f.id === id); + if (index > -1) { + this._subscribers.splice(index, 1); + } + }; +} + +export class ApplicationLibrarySyncState { + + private constructor(){ + } + + private static appState = new AppState(); + + public static async initialize(serverApi: ServerAPI) { + await this.appState.initialize(serverApi); + } + + public static useAppState = () => { + const [state, setState] = useState(ApplicationLibrarySyncState.appState.currentState); + + useEffect(() => { + const id = ApplicationLibrarySyncState.appState.subscribe((e) => { + Logger.debug("Rendering: " + JSON.stringify(e)); + setState(e); + }); + return () => { + ApplicationLibrarySyncState.appState.unsubscribe(id); + }; + }, []); + + return state; + }; + + public static getAppState(): AppState { + return ApplicationLibrarySyncState.appState; + } + public static setAppState = ApplicationLibrarySyncState.appState.setState; + public static getServerApi = () => ApplicationLibrarySyncState.appState.serverApi; +} diff --git a/src/helpers/state.ts b/src/helpers/state.ts index 7daa941..84bf33d 100644 --- a/src/helpers/state.ts +++ b/src/helpers/state.ts @@ -9,20 +9,7 @@ type State = { experimental_menu: string; toast_auto_sync: string; destination_directory: string; - playing: string; - documents_sync: LibrarySyncState; - pictures_sync: LibrarySyncState; - videos_sync: LibrarySyncState; - music_sync: LibrarySyncState; -}; - -interface LibrarySyncState { - enabled: boolean; - destination: string; -}; - -type LibrarySyncDict = { - [key: string]: LibrarySyncState; + playing: string }; class AppState { @@ -35,11 +22,7 @@ class AppState { experimental_menu: "false", toast_auto_sync: "true", destination_directory: "decky-cloud-save", - playing: "false", - documents_sync: { enabled: false, destination: "deck-libraries/Documents" }, - pictures_sync: { enabled: false, destination: "deck-libraries/Pictures" }, - videos_sync: { enabled: false, destination: "deck-libraries/Videos" }, - music_sync: { enabled: false, destination: "deck-libraries/Music" }, + playing: "false" }; private _serverApi: ServerAPI = null!; @@ -70,18 +53,15 @@ class AppState { } else { Logger.error(data); } - - const libSyncData = await serverApi.callPluginMethod<{}, LibrarySyncDict>("get_library_sync_config", {}); - console.log(libSyncData); } - public setState = (key: keyof State, value: string | LibrarySyncState, persist = false) => { + public setState = (key: keyof State, value: string, persist = false) => { this._currentState = { ...this.currentState, [key]: value }; Logger.debug("Setting '" + key + "' to '" + value + "' with persistence: " + persist); if (persist) { - this.serverApi.callPluginMethod<{ key: string; value: string | LibrarySyncState }, null>("set_config", { key, value }).then(e => Logger.debug(e)); + this.serverApi.callPluginMethod<{ key: string; value: string }, null>("set_config", { key, value }).then(e => Logger.debug(e)); } this._subscribers.forEach((e) => e.callback(this.currentState)); @@ -117,9 +97,9 @@ export class ApplicationState { private constructor(){ } - + private static appState = new AppState(); - + public static async initialize(serverApi: ServerAPI) { await this.appState.initialize(serverApi); } diff --git a/src/index.tsx b/src/index.tsx index c222211..792110f 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,6 +3,7 @@ import { Toast } from "./helpers/toast"; import { Logger } from "./helpers/logger"; import { ApiClient } from "./helpers/apiClient"; import { ApplicationState } from "./helpers/state"; +import { ApplicationLibrarySyncState } from "./helpers/libSyncState"; import { Content } from "./pages/RenderDCSMenu"; import { Translator } from "./helpers/translator"; import { Storage } from './helpers/storage'; @@ -18,10 +19,13 @@ declare const appStore: any; export default definePlugin((serverApi: ServerAPI) => { Storage.clearAllSessionStorage() - ApplicationState.initialize(serverApi).then(async () => { - Backend.initialize(serverApi); - await Logger.initialize(); - await Translator.initialize(); + Promise.all([ + ApplicationState.initialize(serverApi), + ApplicationLibrarySyncState.initialize(serverApi)]).then( + async () => { + Backend.initialize(serverApi); + await Logger.initialize(); + await Translator.initialize(); }); serverApi.routerHook.addRoute("/dcs-configure-paths", () => , { exact: true }); @@ -34,7 +38,7 @@ export default definePlugin((serverApi: ServerAPI) => { if (ApplicationState.getAppState().currentState.sync_on_game_exit === "true") { const gameInfo = appStore.GetAppOverviewByGameID(e.unAppID) Logger.info((e.bRunning ? "Starting" : "Stopping") + " game '" + gameInfo.display_name + "' (" + e.unAppID + ")"); - + ApplicationState.setAppState("playing", String(e.bRunning)); let sync: boolean; diff --git a/src/pages/ConfigurePathsPage.tsx b/src/pages/ConfigurePathsPage.tsx index fe6bbcb..e5b71b5 100644 --- a/src/pages/ConfigurePathsPage.tsx +++ b/src/pages/ConfigurePathsPage.tsx @@ -7,10 +7,13 @@ import Container from "../components/Container"; import { HelpAssistant } from "../components/HelpAssistant"; import { ApiClient } from "../helpers/apiClient"; import { ApplicationState } from "../helpers/state"; +import { ApplicationLibrarySyncState, LibrarySyncState } from "../helpers/libSyncState"; import { Translator } from "../helpers/translator"; export default function ConfigurePathsPage({ serverApi }: PageProps<{}>) { const appState = ApplicationState.useAppState(); + const librarySyncState = ApplicationLibrarySyncState.useAppState(); + const [includePaths, setIncludePaths] = useState(undefined); const [excludePaths, setExcludePaths] = useState(undefined); @@ -82,65 +85,44 @@ export default function ConfigurePathsPage({ serverApi }: PageProps<{}>) { - -
-
- -
-
- -
-
-
- -
-
- -
-
- -
-
-
- -
-
- -
-
- -
-
-
- -
-
- -
-
- -
-
-
+ + + +
); + + interface LibrarySyncEntryProps { + title: string; + stateKey: keyof LibrarySyncState; + } + + function LibrarySyncEntry({ title, stateKey }: LibrarySyncEntryProps) { + return ( + +
+
+ ApplicationLibrarySyncState.setAppState(stateKey, {enabled: e}, true)}/> + ApplicationLibrarySyncState.setAppState(stateKey, {bisync: e}, true)}/> +
+
+ ApplicationLibrarySyncState.setAppState(stateKey, {destination: e.target.value}, false)} + onBlur={(e) => ApplicationLibrarySyncState.setAppState(stateKey, {destination: e.target.value}, true)}/> +
+
+
+ ); + } } From d70d23a9ab7a64c56447b501c57fd7d4c373835b Mon Sep 17 00:00:00 2001 From: Akaza Renn <8340896+AkazaRenn@users.noreply.github.com> Date: Wed, 19 Jun 2024 21:46:14 -0400 Subject: [PATCH 07/19] Add margin-top on path page to skip the top bar space --- src/pages/ConfigurePathsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/ConfigurePathsPage.tsx b/src/pages/ConfigurePathsPage.tsx index e5b71b5..42fe644 100644 --- a/src/pages/ConfigurePathsPage.tsx +++ b/src/pages/ConfigurePathsPage.tsx @@ -28,7 +28,7 @@ export default function ConfigurePathsPage({ serverApi }: PageProps<{}>) { return ( -
+
Date: Wed, 19 Jun 2024 21:51:08 -0400 Subject: [PATCH 08/19] Simplify LibrarySyncState init code --- src/helpers/libSyncState.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/helpers/libSyncState.ts b/src/helpers/libSyncState.ts index 5fbdb6f..6c38e56 100644 --- a/src/helpers/libSyncState.ts +++ b/src/helpers/libSyncState.ts @@ -49,10 +49,10 @@ class AppState { const data = await serverApi.callPluginMethod<{}, LibrarySyncState>("get_library_sync_config", {}); if (data.success) { - this.setState("Documents", { enabled: data.result.Documents.enabled, bisync: data.result.Documents.bisync, destination: data.result.Documents.destination }); - this.setState("Music", { enabled: data.result.Music.enabled, bisync: data.result.Music.bisync, destination: data.result.Music.destination }); - this.setState("Pictures", { enabled: data.result.Pictures.enabled, bisync: data.result.Pictures.bisync, destination: data.result.Pictures.destination }); - this.setState("Videos", { enabled: data.result.Videos.enabled, bisync: data.result.Videos.bisync, destination: data.result.Videos.destination }); + this.setState("Documents", data.result.Documents); + this.setState("Music", data.result.Music); + this.setState("Pictures", data.result.Pictures); + this.setState("Videos", data.result.Videos); } else { Logger.error(data); } From a83890a9397b8675d80fcb4e93aafa660d48cc85 Mon Sep 17 00:00:00 2001 From: Akaza Renn <8340896+AkazaRenn@users.noreply.github.com> Date: Thu, 20 Jun 2024 18:20:09 -0400 Subject: [PATCH 09/19] Migrate to json config --- py_modules/plugin_config.py | 96 ++++++++++++---------- src/helpers/libSyncState.ts | 134 ------------------------------- src/helpers/state.ts | 56 +++++++++++-- src/pages/ConfigurePathsPage.tsx | 20 +++-- 4 files changed, 111 insertions(+), 195 deletions(-) delete mode 100644 src/helpers/libSyncState.ts diff --git a/py_modules/plugin_config.py b/py_modules/plugin_config.py index c1e74e3..02f73e3 100644 --- a/py_modules/plugin_config.py +++ b/py_modules/plugin_config.py @@ -13,24 +13,38 @@ cfg_syncpath_includes_file = config_dir / "sync_paths.txt" cfg_syncpath_excludes_file = config_dir / "sync_paths_excludes.txt" cfg_syncpath_filter_file = config_dir / "sync_paths_filter.txt" -cfg_property_file = config_dir / "plugin.properties" -library_sync_config = settings_manager(name="library_sync", settings_directory=decky_plugin.DECKY_PLUGIN_SETTINGS_DIR) +config = settings_manager(name="config", settings_directory=decky_plugin.DECKY_PLUGIN_SETTINGS_DIR) -def get_config(): +def get_cfg_property(): """ - Reads and parses the plugin configuration file. + Reads and parses the plugin.properties. + This is only used for migration from legacy config file and should not be used for any other purpose. Returns: list: A list of key-value pairs representing the configuration. """ + cfg_property_file = config_dir / "plugin.properties" + if not cfg_property_file.is_file(): + return [] with open(cfg_property_file) as f: lines = f.readlines() lines = list(map(lambda x: x.strip().split('='), lines)) decky_plugin.logger.debug("config %s", lines) + cfg_property_file.unlink() return lines -def set_config(key: str, value: str): +def get_config(): + """ + Retrieves the plugin configuration. + + Returns: + dict: The plugin configuration. + """ + config.read() + return config.settings + +def set_config(key: str, value): """ Sets a configuration key-value pair in the plugin configuration file. @@ -38,20 +52,9 @@ def set_config(key: str, value: str): key (str): The key to set. value (str): The value to set for the key. """ - with open(cfg_property_file, "r") as f: - lines = f.readlines() - with open(cfg_property_file, "w") as f: - found = False - for line in lines: - if line.startswith(key + '='): - f.write(f"{key}={value}\n") - found = True - else: - f.write(line) - if not found: - f.write(f"{key}={value}\n") - -def get_config_item(name: str, default: str = None): + config.setSetting(key, value) + +def get_config_item(name: str, default = None): """ Retrieves a configuration item by name. @@ -62,7 +65,8 @@ def get_config_item(name: str, default: str = None): Returns: str: The value of the configuration item. """ - return next((x[1] for x in get_config() if x[0] == name), default) + config.read() + return config.getSetting(name, default) def regenerate_filter_file(): """ @@ -95,11 +99,11 @@ def get_library_sync_config(key: str = None): Returns: dict: The library sync configuration. """ - library_sync_config.read() + config.read() if key: - return library_sync_config.getSetting(key, {"enabled": False, "bisync": False, "destination": f"deck-libraries/{key}"}) + return config.getSetting("library_sync", {}).get(key, {"enabled": False, "bisync": False, "destination": f"deck-libraries/{key}"}) else: - return library_sync_config.settings + return config.getSetting("library_sync", {}) def set_library_sync_config(key: str, enabled: bool = None, bisync: bool = None, destination: str = None): """ @@ -110,15 +114,16 @@ def set_library_sync_config(key: str, enabled: bool = None, bisync: bool = None, enabled (bool): Whether the key is enabled. destination (str): The destination of the key. """ - library_sync_config.read() + config.read() if enabled == None: - enabled = library_sync_config.getSetting(key, {}).get("enabled", False) + enabled = get_library_sync_config(key).get("enabled", False) if bisync == None: - bisync = library_sync_config.getSetting(key, {}).get("bisync", False) + bisync = get_library_sync_config(key).get(key, {}).get("bisync", False) if destination == None: - destination = library_sync_config.getSetting(key, {}).get("destination", f"deck-libraries/{key}") - - library_sync_config.setSetting(key, {"enabled": enabled, "bisync": bisync, "destination": destination}) + destination = get_library_sync_config(key).get(key, {}).get("destination", f"deck-libraries/{key}") + library_sync_config = get_library_sync_config() + library_sync_config.update({key: {"enabled": enabled, "bisync": bisync, "destination": destination}}) + config.setSetting("library_sync", library_sync_config) def migrate(): """ @@ -130,24 +135,31 @@ def migrate(): cfg_syncpath_includes_file.touch() if not cfg_syncpath_excludes_file.is_file(): cfg_syncpath_excludes_file.touch() - if not cfg_property_file.is_file(): - cfg_property_file.touch() if not cfg_syncpath_filter_file.is_file(): regenerate_filter_file() - config = get_config() - if not any(e[0] == "destination_directory" for e in config): + # Migrate from plugin.properties to config.json + config_pairs_list = get_cfg_property() + for pair in config_pairs_list: + if pair[1] == "true": + pair[1] = True + elif pair[1] == "false": + pair[1] = False + set_config(pair[0], pair[1]) + + # Set default configurations + current_config = get_config() + if not "destination_directory" in current_config: set_config("destination_directory", "decky-cloud-save") - if not any(e[0] == "bisync_enabled" for e in config): - set_config("bisync_enabled", "false") - if not any(e[0] == "log_level" for e in config): + if not "bisync_enabled" in current_config: + set_config("bisync_enabled", False) + if not "log_level" in current_config: set_config("log_level", "INFO") - if not any(e[0] == "sync_on_game_exit" for e in config): - set_config("sync_on_game_exit", "true") - if not any(e[0] == "toast_auto_sync" for e in config): - set_config("toast_auto_sync", "true") - - if not get_library_sync_config(): + if not "sync_on_game_exit" in current_config: + set_config("sync_on_game_exit", True) + if not "toast_auto_sync" in current_config: + set_config("toast_auto_sync", True) + if not "library_sync" in current_config: set_library_sync_config("Documents") set_library_sync_config("Music") set_library_sync_config("Pictures") diff --git a/src/helpers/libSyncState.ts b/src/helpers/libSyncState.ts deleted file mode 100644 index 6c38e56..0000000 --- a/src/helpers/libSyncState.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { ServerAPI } from "decky-frontend-lib"; -import { useEffect, useState } from "react"; -import { Logger } from "./logger"; - -export type LibrarySyncState = { - Documents: LibrarySyncStateEntry; - Music: LibrarySyncStateEntry; - Pictures: LibrarySyncStateEntry; - Videos: LibrarySyncStateEntry; -}; - -interface LibrarySyncStateEntry { - enabled: boolean; - bisync: boolean; - destination: string; -}; - -class AppState { - private _subscribers: { id: number; callback: (e: LibrarySyncState) => void }[] = []; - - private _currentState: LibrarySyncState = { - Documents: { enabled: false, bisync: false, destination: "deck-libraries/Documents" }, - Music: { enabled: false, bisync: false, destination: "deck-libraries/Music" }, - Pictures: { enabled: false, bisync: false, destination: "deck-libraries/Pictures" }, - Videos: { enabled: false, bisync: false, destination: "deck-libraries/Videos" }, - }; - - private _serverApi: ServerAPI = null!; - private _alreadyInit: boolean = true!; - - public get currentState() { - return this._currentState; - } - - public get serverApi() { - return this._serverApi; - } - - public get alreadyInit() { - return this._alreadyInit; - } - - public async initialize(serverApi: ServerAPI) { - if (this._serverApi == null) { - this._alreadyInit = false; - } - - this._serverApi = serverApi; - - const data = await serverApi.callPluginMethod<{}, LibrarySyncState>("get_library_sync_config", {}); - if (data.success) { - this.setState("Documents", data.result.Documents); - this.setState("Music", data.result.Music); - this.setState("Pictures", data.result.Pictures); - this.setState("Videos", data.result.Videos); - } else { - Logger.error(data); - } - } - - public setState = (key: keyof LibrarySyncState, values: Partial<{ enabled: boolean; bisync: boolean; destination: string }>, persist = false) => { - let entry = this._currentState[key]; - if (values.enabled !== undefined) { - entry.enabled = values.enabled; - } - if (values.bisync !== undefined) { - entry.bisync = values.bisync; - } - if (values.destination !== undefined) { - entry.destination = values.destination; - } - - if (persist) { - this.serverApi - .callPluginMethod<{ key: string; enabled: boolean; bisync: boolean, destination: string }, void>("set_library_sync_config", { - key, - enabled: entry.enabled, - bisync: entry.bisync, - destination: entry.destination, - }) - .then(e => Logger.debug(e)); - } - - this._subscribers.forEach((e) => e.callback(this.currentState)); - } - - public subscribe = (callback: (e: LibrarySyncState) => void) => { - const id = new Date().getTime(); - this._subscribers.push({ id, callback }); - - return id; - }; - - public unsubscribe = (id: number) => { - const index = this._subscribers.findIndex((f) => f.id === id); - if (index > -1) { - this._subscribers.splice(index, 1); - } - }; -} - -export class ApplicationLibrarySyncState { - - private constructor(){ - } - - private static appState = new AppState(); - - public static async initialize(serverApi: ServerAPI) { - await this.appState.initialize(serverApi); - } - - public static useAppState = () => { - const [state, setState] = useState(ApplicationLibrarySyncState.appState.currentState); - - useEffect(() => { - const id = ApplicationLibrarySyncState.appState.subscribe((e) => { - Logger.debug("Rendering: " + JSON.stringify(e)); - setState(e); - }); - return () => { - ApplicationLibrarySyncState.appState.unsubscribe(id); - }; - }, []); - - return state; - }; - - public static getAppState(): AppState { - return ApplicationLibrarySyncState.appState; - } - public static setAppState = ApplicationLibrarySyncState.appState.setState; - public static getServerApi = () => ApplicationLibrarySyncState.appState.serverApi; -} diff --git a/src/helpers/state.ts b/src/helpers/state.ts index 84bf33d..f7dce19 100644 --- a/src/helpers/state.ts +++ b/src/helpers/state.ts @@ -9,7 +9,23 @@ type State = { experimental_menu: string; toast_auto_sync: string; destination_directory: string; - playing: string + playing: string; + library_sync: LibrarySyncState; +}; + +type ValueOf = T[keyof T]; + +export type LibrarySyncState = { + Documents: LibrarySyncStateEntry; + Music: LibrarySyncStateEntry; + Pictures: LibrarySyncStateEntry; + Videos: LibrarySyncStateEntry; +}; + +interface LibrarySyncStateEntry { + enabled: boolean; + bisync: boolean; + destination: string; }; class AppState { @@ -22,7 +38,13 @@ class AppState { experimental_menu: "false", toast_auto_sync: "true", destination_directory: "decky-cloud-save", - playing: "false" + playing: "false", + library_sync: { + Documents: { enabled: false, bisync: false, destination: "deck-libraries/Documents" }, + Music: { enabled: false, bisync: false, destination: "deck-libraries/Music" }, + Pictures: { enabled: false, bisync: false, destination: "deck-libraries/Pictures" }, + Videos: { enabled: false, bisync: false, destination: "deck-libraries/Videos" }, + }, }; private _serverApi: ServerAPI = null!; @@ -47,21 +69,38 @@ class AppState { this._serverApi = serverApi; - const data = await serverApi.callPluginMethod<{}, string[][]>("get_config", {}); + const data = await serverApi.callPluginMethod<{}, State>("get_config", {}); if (data.success) { - data.result.forEach((e) => this.setState(e[0] as keyof State, e[1])); + for (const [key, value] of Object.entries(data.result)) { + this.setState(key as keyof State, value as ValueOf); + } } else { Logger.error(data); } } - public setState = (key: keyof State, value: string, persist = false) => { + public setLibSyncState = (key: keyof LibrarySyncState, values: Partial<{ enabled: boolean; bisync: boolean; destination: string }>, persist = false) => { + let entry = this._currentState.library_sync[key]; + if (values.enabled !== undefined) { + entry.enabled = values.enabled; + } + if (values.bisync !== undefined) { + entry.bisync = values.bisync; + } + if (values.destination !== undefined) { + entry.destination = values.destination; + } + + this.setState("library_sync", this._currentState.library_sync, persist); + } + + public setState = (key: keyof State, value: ValueOf, persist = false) => { this._currentState = { ...this.currentState, [key]: value }; Logger.debug("Setting '" + key + "' to '" + value + "' with persistence: " + persist); if (persist) { - this.serverApi.callPluginMethod<{ key: string; value: string }, null>("set_config", { key, value }).then(e => Logger.debug(e)); + this.serverApi.callPluginMethod<{ key: string; value: ValueOf }, null>("set_config", { key, value }).then(e => Logger.debug(e)); } this._subscribers.forEach((e) => e.callback(this.currentState)); @@ -97,9 +136,9 @@ export class ApplicationState { private constructor(){ } - + private static appState = new AppState(); - + public static async initialize(serverApi: ServerAPI) { await this.appState.initialize(serverApi); } @@ -125,6 +164,7 @@ export class ApplicationState { return ApplicationState.appState; } public static setAppState = ApplicationState.appState.setState; + public static setLibSyncState = ApplicationState.appState.setLibSyncState; public static setbisync_enabled = ApplicationState.appState.setbisync_enabled; public static getServerApi = () => ApplicationState.appState.serverApi; } diff --git a/src/pages/ConfigurePathsPage.tsx b/src/pages/ConfigurePathsPage.tsx index 42fe644..bb212fb 100644 --- a/src/pages/ConfigurePathsPage.tsx +++ b/src/pages/ConfigurePathsPage.tsx @@ -6,13 +6,11 @@ import { RenderExistingPathButton } from "../components/RenderExistingPathButton import Container from "../components/Container"; import { HelpAssistant } from "../components/HelpAssistant"; import { ApiClient } from "../helpers/apiClient"; -import { ApplicationState } from "../helpers/state"; -import { ApplicationLibrarySyncState, LibrarySyncState } from "../helpers/libSyncState"; +import { ApplicationState, LibrarySyncState } from "../helpers/state"; import { Translator } from "../helpers/translator"; export default function ConfigurePathsPage({ serverApi }: PageProps<{}>) { const appState = ApplicationState.useAppState(); - const librarySyncState = ApplicationLibrarySyncState.useAppState(); const [includePaths, setIncludePaths] = useState(undefined); const [excludePaths, setExcludePaths] = useState(undefined); @@ -86,8 +84,8 @@ export default function ConfigurePathsPage({ serverApi }: PageProps<{}>) { - + @@ -107,19 +105,19 @@ export default function ConfigurePathsPage({ serverApi }: PageProps<{}>) {
ApplicationLibrarySyncState.setAppState(stateKey, {enabled: e}, true)}/> + checked={appState.library_sync[stateKey].enabled} + onChange={(e) => ApplicationState.setLibSyncState(stateKey, {enabled: e}, true)}/> ApplicationLibrarySyncState.setAppState(stateKey, {bisync: e}, true)}/> + checked={appState.library_sync[stateKey].bisync} + onChange={(e) => ApplicationState.setLibSyncState(stateKey, {bisync: e}, true)}/>
ApplicationLibrarySyncState.setAppState(stateKey, {destination: e.target.value}, false)} - onBlur={(e) => ApplicationLibrarySyncState.setAppState(stateKey, {destination: e.target.value}, true)}/> + defaultValue={appState.library_sync[stateKey].destination} + onChange={(e) => ApplicationState.setLibSyncState(stateKey, {destination: e.target.value}, false)} + onBlur={(e) => ApplicationState.setLibSyncState(stateKey, {destination: e.target.value}, true)}/>
From d5cc0f0f7f37f3b64b218c7f05fbe50df486b229 Mon Sep 17 00:00:00 2001 From: Akaza Renn <8340896+AkazaRenn@users.noreply.github.com> Date: Thu, 20 Jun 2024 19:02:50 -0400 Subject: [PATCH 10/19] Move whole settings to the json --- main.py | 10 ---------- py_modules/rclone_sync_manager.py | 4 ++-- src/helpers/apiClient.ts | 8 ++++---- src/helpers/state.ts | 26 +++++++++++++------------- src/index.tsx | 19 ++++++++----------- src/pages/ConfigurePathsPage.tsx | 4 ++-- src/pages/RenderDCSMenu.tsx | 26 +++++++++++++------------- 7 files changed, 42 insertions(+), 55 deletions(-) diff --git a/main.py b/main.py index 8ccb771..07bb0fd 100644 --- a/main.py +++ b/main.py @@ -93,16 +93,6 @@ async def set_config(self, key: str, value: str): decky_plugin.logger.debug("Executing: set_config(%s, %s)", key, value) plugin_config.set_config(key, value) - async def get_library_sync_config(self, key: str = None): - decky_plugin.logger.debug( - f"Executing: get_library_sync_config({key})") - return plugin_config.get_library_sync_config(key) - - async def set_library_sync_config(self, key: str, enabled, bisync, destination): - decky_plugin.logger.debug( - f"Executing: set_library_sync_config({key}:{{enabled:{enabled},bisync:{bisync},destination:{destination}}})") - plugin_config.set_library_sync_config(key=key, enabled=enabled, bisync=bisync, destination=destination) - # Logger async def log(self, level: str, msg: str) -> int: diff --git a/py_modules/rclone_sync_manager.py b/py_modules/rclone_sync_manager.py index ead2992..f68641d 100644 --- a/py_modules/rclone_sync_manager.py +++ b/py_modules/rclone_sync_manager.py @@ -25,7 +25,7 @@ async def sync_now(self, winner: str, resync: bool): destination_path = plugin_config.get_config_item("destination_directory", "decky-cloud-save") await self.sync_now_internal( ["/", f"backend:{destination_path}", "--filter-from", plugin_config.cfg_syncpath_filter_file], - plugin_config.get_config_item("bisync_enabled", "false") == "true", + plugin_config.get_config_item("bisync_enabled", False), winner, resync ) @@ -59,7 +59,7 @@ async def sync_now_internal(self, path_args: list, bisync: bool, winner: str, re args.extend(path_args) args.extend(["--copy-links"]) - if bisync_enabled: + if bisync: if resync: args.extend(["--resync-mode", winner, "--resync"]) else: diff --git a/src/helpers/apiClient.ts b/src/helpers/apiClient.ts index 74259eb..1e54566 100644 --- a/src/helpers/apiClient.ts +++ b/src/helpers/apiClient.ts @@ -128,15 +128,15 @@ export class ApiClient { private static async syncNowInternal(showToast: boolean, winner: string, resync: boolean = false): Promise { Logger.info("Synchronizing"); const start = new Date(); - if (ApplicationState.getAppState().currentState.syncing === "true") { + if (ApplicationState.getAppState().currentState.syncing) { Toast.toast(Translator.translate("waiting.previous"), 2000); - while (ApplicationState.getAppState().currentState.syncing === "true") { + while (ApplicationState.getAppState().currentState.syncing) { await sleep(300); } } Storage.setSessionStorageItem("syncing", "true"); - ApplicationState.setAppState("syncing", "true"); + ApplicationState.setAppState("syncing", true); await ApplicationState.getServerApi().callPluginMethod("sync_now_internal", { winner, resync }); let exitCode = 0; @@ -164,7 +164,7 @@ export class ApiClient { pass = false; break; } - ApplicationState.setAppState("syncing", "false"); + ApplicationState.setAppState("syncing", false); Storage.setSessionStorageItem("syncing", "false"); let body; diff --git a/src/helpers/state.ts b/src/helpers/state.ts index f7dce19..56e4f26 100644 --- a/src/helpers/state.ts +++ b/src/helpers/state.ts @@ -3,13 +3,13 @@ import { useEffect, useState } from "react"; import { Logger } from "./logger"; type State = { - sync_on_game_exit: string; - syncing: string; - bisync_enabled: string; - experimental_menu: string; - toast_auto_sync: string; + sync_on_game_exit: boolean; + syncing: boolean; + bisync_enabled: boolean; + experimental_menu: boolean; + toast_auto_sync: boolean; destination_directory: string; - playing: string; + playing: boolean; library_sync: LibrarySyncState; }; @@ -32,13 +32,13 @@ class AppState { private _subscribers: { id: number; callback: (e: State) => void }[] = []; private _currentState: State = { - syncing: "false", - sync_on_game_exit: "true", - bisync_enabled: "false", - experimental_menu: "false", - toast_auto_sync: "true", + syncing: false, + sync_on_game_exit: true, + bisync_enabled: false, + experimental_menu: false, + toast_auto_sync: true, destination_directory: "decky-cloud-save", - playing: "false", + playing: false, library_sync: { Documents: { enabled: false, bisync: false, destination: "deck-libraries/Documents" }, Music: { enabled: false, bisync: false, destination: "deck-libraries/Music" }, @@ -100,7 +100,7 @@ class AppState { Logger.debug("Setting '" + key + "' to '" + value + "' with persistence: " + persist); if (persist) { - this.serverApi.callPluginMethod<{ key: string; value: ValueOf }, null>("set_config", { key, value }).then(e => Logger.debug(e)); + this.serverApi.callPluginMethod<{ key: string; value: ValueOf }, null>("set_config", { key, value: this._currentState[key] }).then(e => Logger.debug(e)); } this._subscribers.forEach((e) => e.callback(this.currentState)); diff --git a/src/index.tsx b/src/index.tsx index 792110f..b55ccf7 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,7 +3,6 @@ import { Toast } from "./helpers/toast"; import { Logger } from "./helpers/logger"; import { ApiClient } from "./helpers/apiClient"; import { ApplicationState } from "./helpers/state"; -import { ApplicationLibrarySyncState } from "./helpers/libSyncState"; import { Content } from "./pages/RenderDCSMenu"; import { Translator } from "./helpers/translator"; import { Storage } from './helpers/storage'; @@ -19,13 +18,11 @@ declare const appStore: any; export default definePlugin((serverApi: ServerAPI) => { Storage.clearAllSessionStorage() - Promise.all([ - ApplicationState.initialize(serverApi), - ApplicationLibrarySyncState.initialize(serverApi)]).then( - async () => { - Backend.initialize(serverApi); - await Logger.initialize(); - await Translator.initialize(); + ApplicationState.initialize(serverApi).then( + async () => { + Backend.initialize(serverApi); + await Logger.initialize(); + await Translator.initialize(); }); serverApi.routerHook.addRoute("/dcs-configure-paths", () => , { exact: true }); @@ -35,11 +32,11 @@ export default definePlugin((serverApi: ServerAPI) => { serverApi.routerHook.addRoute("/dcs-plugin-logs", () => , { exact: true }); const { unregister: removeGameExecutionListener } = SteamClient.GameSessions.RegisterForAppLifetimeNotifications((e: LifetimeNotification) => { - if (ApplicationState.getAppState().currentState.sync_on_game_exit === "true") { + if (ApplicationState.getAppState().currentState.sync_on_game_exit) { const gameInfo = appStore.GetAppOverviewByGameID(e.unAppID) Logger.info((e.bRunning ? "Starting" : "Stopping") + " game '" + gameInfo.display_name + "' (" + e.unAppID + ")"); - ApplicationState.setAppState("playing", String(e.bRunning)); + ApplicationState.setAppState("playing", e.bRunning); let sync: boolean; if (gameInfo?.app_type === 1) { // Steam Games == 1 @@ -56,7 +53,7 @@ export default definePlugin((serverApi: ServerAPI) => { } if (sync) { - let toast = ApplicationState.getAppState().currentState.toast_auto_sync === "true"; + let toast = ApplicationState.getAppState().currentState.toast_auto_sync; if (e.bRunning) { if (toast) { Toast.toast(Translator.translate("synchronizing.savedata"), 2000); diff --git a/src/pages/ConfigurePathsPage.tsx b/src/pages/ConfigurePathsPage.tsx index bb212fb..5fc7d50 100644 --- a/src/pages/ConfigurePathsPage.tsx +++ b/src/pages/ConfigurePathsPage.tsx @@ -109,14 +109,14 @@ export default function ConfigurePathsPage({ serverApi }: PageProps<{}>) { onChange={(e) => ApplicationState.setLibSyncState(stateKey, {enabled: e}, true)}/> ApplicationState.setLibSyncState(stateKey, {bisync: e}, true)}/>
ApplicationState.setLibSyncState(stateKey, {destination: e.target.value}, false)} onBlur={(e) => ApplicationState.setLibSyncState(stateKey, {destination: e.target.value}, true)}/>
diff --git a/src/pages/RenderDCSMenu.tsx b/src/pages/RenderDCSMenu.tsx index 9e87757..db98c47 100644 --- a/src/pages/RenderDCSMenu.tsx +++ b/src/pages/RenderDCSMenu.tsx @@ -23,9 +23,9 @@ export const Content: VFC<{}> = () => { const [needsResync, setNeedsSync] = useState(false); setInterval(async () => { if ( - ApplicationState.getAppState().currentState.playing === "false" && - ApplicationState.getAppState().currentState.bisync_enabled === "true" && - ApplicationState.getAppState().currentState.syncing === "false" + (!ApplicationState.getAppState().currentState.playing )&& + ApplicationState.getAppState().currentState.bisync_enabled && + (!ApplicationState.getAppState().currentState.syncing) ) { setNeedsSync(await Backend.needsResync()); } @@ -38,7 +38,7 @@ export const Content: VFC<{}> = () => { { if (needsResync) { ApiClient.resyncNow("path1"); @@ -47,7 +47,7 @@ export const Content: VFC<{}> = () => { } }} > - }> + }> {needsResync && Translator.translate("resync.now")} {!needsResync && Translator.translate("sync.now")} @@ -60,16 +60,16 @@ export const Content: VFC<{}> = () => { ApplicationState.setAppState("sync_on_game_exit", e ? "true" : "false", true)} + checked={appState.sync_on_game_exit} + onChange={(e) => ApplicationState.setAppState("sync_on_game_exit", e, true)} /> ApplicationState.setAppState("toast_auto_sync", e ? "true" : "false", true)} + checked={appState.toast_auto_sync} + onChange={(e) => ApplicationState.setAppState("toast_auto_sync", e, true)} /> @@ -119,7 +119,7 @@ export const Content: VFC<{}> = () => { { (async () => { let logs = await Backend.getLastSyncLog(); @@ -140,8 +140,8 @@ export const Content: VFC<{}> = () => { ApplicationState.setAppState("bisync_enabled", e ? "true" : "false", true)} + checked={appState.bisync_enabled} + onChange={(e) => ApplicationState.setAppState("bisync_enabled", e, true)} />
From 06562273d172926b06fcb5c014bd55db79103c4e Mon Sep 17 00:00:00 2001 From: Akaza Renn <8340896+AkazaRenn@users.noreply.github.com> Date: Thu, 20 Jun 2024 19:07:13 -0400 Subject: [PATCH 11/19] Minor formatting --- src/pages/RenderDCSMenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/RenderDCSMenu.tsx b/src/pages/RenderDCSMenu.tsx index db98c47..186fea4 100644 --- a/src/pages/RenderDCSMenu.tsx +++ b/src/pages/RenderDCSMenu.tsx @@ -23,7 +23,7 @@ export const Content: VFC<{}> = () => { const [needsResync, setNeedsSync] = useState(false); setInterval(async () => { if ( - (!ApplicationState.getAppState().currentState.playing )&& + (!ApplicationState.getAppState().currentState.playing) && ApplicationState.getAppState().currentState.bisync_enabled && (!ApplicationState.getAppState().currentState.syncing) ) { From 68aba6cea4718a803aa3fefb18e5cd7e4bc9aa09 Mon Sep 17 00:00:00 2001 From: Akaza Renn <8340896+AkazaRenn@users.noreply.github.com> Date: Fri, 21 Jun 2024 21:05:58 -0400 Subject: [PATCH 12/19] Change destination input to a dialog --- src/pages/ConfigurePathsPage.tsx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/pages/ConfigurePathsPage.tsx b/src/pages/ConfigurePathsPage.tsx index 5fc7d50..914d56e 100644 --- a/src/pages/ConfigurePathsPage.tsx +++ b/src/pages/ConfigurePathsPage.tsx @@ -1,4 +1,4 @@ -import { PanelSectionRow, PanelSection, TextField, ToggleField } from "decky-frontend-lib"; +import { showModal, ConfirmModal, PanelSectionRow, PanelSection, TextField, ToggleField } from "decky-frontend-lib"; import { useEffect, useState } from "react"; import { PageProps } from "../helpers/types"; import AddNewPathButton from "../components/AddNewPathButton"; @@ -116,8 +116,18 @@ export default function ConfigurePathsPage({ serverApi }: PageProps<{}>) {
ApplicationState.setLibSyncState(stateKey, {destination: e.target.value}, true)}/> + value={appState.library_sync[stateKey].destination} + onClick={() => { + showModal( + ApplicationState.setLibSyncState(stateKey, {destination: appState.library_sync[stateKey].destination}, true)}> + ApplicationState.setLibSyncState(stateKey, {destination: e.target.value})} /> + + ); + }}/>
From 82938f7a2c6d0b47f3636d2d4235df67f9eee122 Mon Sep 17 00:00:00 2001 From: Akaza Renn <8340896+AkazaRenn@users.noreply.github.com> Date: Sun, 14 Jul 2024 09:45:03 -0400 Subject: [PATCH 13/19] Allow left/right navigation on lib sync entries --- src/pages/ConfigurePathsPage.tsx | 64 +++++++++++++++++--------------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/src/pages/ConfigurePathsPage.tsx b/src/pages/ConfigurePathsPage.tsx index 914d56e..1d86dcb 100644 --- a/src/pages/ConfigurePathsPage.tsx +++ b/src/pages/ConfigurePathsPage.tsx @@ -1,4 +1,4 @@ -import { showModal, ConfirmModal, PanelSectionRow, PanelSection, TextField, ToggleField } from "decky-frontend-lib"; +import { showModal, ConfirmModal, Focusable, PanelSectionRow, PanelSection, TextField, ToggleField, Field } from "decky-frontend-lib"; import { useEffect, useState } from "react"; import { PageProps } from "../helpers/types"; import AddNewPathButton from "../components/AddNewPathButton"; @@ -101,35 +101,39 @@ export default function ConfigurePathsPage({ serverApi }: PageProps<{}>) { function LibrarySyncEntry({ title, stateKey }: LibrarySyncEntryProps) { return ( -
-
- ApplicationState.setLibSyncState(stateKey, {enabled: e}, true)}/> - ApplicationState.setLibSyncState(stateKey, {bisync: e}, true)}/> -
-
- { - showModal( - ApplicationState.setLibSyncState(stateKey, {destination: appState.library_sync[stateKey].destination}, true)}> - ApplicationState.setLibSyncState(stateKey, {destination: e.target.value})} /> - - ); - }}/> -
-
+ + +
+ ApplicationState.setLibSyncState(stateKey, {enabled: e}, true)}/> + ApplicationState.setLibSyncState(stateKey, {bisync: e}, true)}/> +
+
+ { + showModal( + ApplicationState.setLibSyncState(stateKey, {destination: appState.library_sync[stateKey].destination}, true)}> + ApplicationState.setLibSyncState(stateKey, {destination: e.target.value})} /> + + ); + }}/> +
+
+
); } From fc62b08d4c4ff85e91d5b6ac83339de3557aa518 Mon Sep 17 00:00:00 2001 From: Renn <8340896+AkazaRenn@users.noreply.github.com> Date: Sat, 24 Aug 2024 16:43:57 +0000 Subject: [PATCH 14/19] Add github action --- .github/workflows/build.yml | 75 +++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..0b66c5e --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,75 @@ +name: Build Plugin + +on: + push: + branches: + - main + pull_request_target: + branches: ['*'] + +jobs: + build: + name: Build plugin + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + fetch-depth: 0 + submodules: "recursive" + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Append hash to plugin version + if: ${{ github.event_name == 'pull_request_target' }} + run: | + echo "::notice::This run was triggered by a commit. Appending the commit hash to the plugin version." + + PACKAGE_FILE="package.json" + + SHA=$(cut -c1-7 <<< "${{ github.event.pull_request.head.sha || github.sha }}") + VERSION=$(jq -r '.version' $PACKAGE_FILE) + NEW_VERSION="$VERSION-$SHA" + + echo "::notice::Going from $VERSION to $NEW_VERSION" + + tmp=$(mktemp) + jq --arg newversion "$NEW_VERSION" '.version = $newversion' $PACKAGE_FILE > $tmp + mv $tmp $PACKAGE_FILE + + echo "::endgroup::" + + - uses: pnpm/action-setup@v3 + with: + version: 9.4.0 + - name: Update pnpm lockfile + if: ${{ inputs.force_pnpm != 'none' }} + run: | + $(which pnpm) install --lockfile-only + + - name: Download Decky CLI + run: | + mkdir -p "$(pwd)"/cli + curl -L -o "$(pwd)"/cli/decky "https://github.com/SteamDeckHomebrew/cli/releases/latest/download/decky-linux-x86_64" + chmod +x "$(pwd)"/cli/decky + + - name: Build plugin + run: | + # Run the CLI as root to get around Docker's weird permissions + sudo .vscode/build.sh + sudo chown -R $(whoami) out + + - name: Unzip plugin + run: | + for file in out/*.zip; do + echo "Unzipping $file" + unzip -qq "$file" -d out + done + tree out + + - name: Upload Artifacts to Github + uses: actions/upload-artifact@v4 + with: + name: output + path: out/*/ \ No newline at end of file From 6ae2999815c03032694d1c4199ea449aef25db33 Mon Sep 17 00:00:00 2001 From: Renn <8340896+AkazaRenn@users.noreply.github.com> Date: Sat, 24 Aug 2024 17:17:05 +0000 Subject: [PATCH 15/19] Fix rclone launcher path --- assets/languages/zh.json | 53 ++++++++++++++++++++++++++++++++++++++++ backend/rcloneLauncher | 7 +++--- 2 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 assets/languages/zh.json diff --git a/assets/languages/zh.json b/assets/languages/zh.json new file mode 100644 index 0000000..90aac13 --- /dev/null +++ b/assets/languages/zh.json @@ -0,0 +1,53 @@ +{ + "sync": "同步", + "sync.now": "立即同步", + "resync.now": "立即重新同步", + "provider.not.configured": "未配置雲存儲提供商。請在「雲存儲提供商」中配置", + "sync.start.stop": "在遊戲開始和結束時同步", + "toast.auto.sync": "自動同步後彈出通知", + "configuration": "配置", + "sync.paths": "同步路徑", + "cloud.provider": "雲存儲提供商", + "experimental.use.risk": "實驗性功能", + "bidirectional.sync": "双向同步", + "you.mad": "你瘋了嗎??", + "warning.root": "為了你的個人安全,你無法同步整個檔案系統", + "confirm.add": "確認添加", + "confirm.add.path": "路徑 {{path}} 對應 {{count}} 個檔案。繼續?", + "select.path": "選擇要同步的路徑", + "file": "檔案", + "folder": "資料夾", + "folder.exclude": "資料夾 (排除子資料夾)", + "add.to.sync": "將路徑添加到同步", + "exclude.from.sync": "將路徑從同步中排除", + "confirm.remove": "確認移除", + "removing.path": "移除路徑 {{path}}。繼續?", + "includes.vs.exclude": "包含或排除", + "help.exclude": "自 v1.2.0 起, 特定資料夾可被從同步中排除。\n\n同步時,本插件會先檢查排除列表以確認某檔案(或資料夾)是否被排除,其後它才會檢查包含列表內的文件。\n\n舉例,若資料夾 /a/** 在包含列表內, 但檔案 /a/b 在排除列表內,則除 b 以外的檔案將會被備份。", + "include.or.exclude.subf": "包含或排除子資料夾", + "help.include.or.exclude.subf": "有時插件只需要備份資料夾本身而不需要繼續搜尋子資料夾。在這種情況下,你可以使用「資料夾 (排除子資料夾)」選項。\n\n此種情況十分罕見,多數時期應使用「資料夾」選項。", + "bug.file.picker": "臭蟲:檔案選擇器無限期載入", + "help.file.picker.fail": "全新安裝後,檔案選擇器可能無法載入。重新啟動 Steam 即可解決。\n詳情請參考討論區", + "cloud.save.path": "雲存儲路徑", + "configure.provider": "配置雲存儲提供商", + "currently.using": "使用中", + "click.providers": "點擊以下提供商之一來配置備份目的路徑", + "other.providers": "添加其他提供商", + "manually.desktop": "除上列兩家提供商外,也可以配置其他提供商,但該配置只能在桌面模式下完成。\n\n針對某些提供商(例如 Google 雲端硬碟)的安裝腳本已經就緒,請直接運行插件安裝目錄下的安裝腳本(預設為:/home/deck/homebrew/plugins/decky-cloud-save/quickstart/)。\n\n針對其他提供商,請參閱 README.md。", + "other.advanced": "其他(進階)", + "includes": "包含", + "excludes": "排除", + "waiting.previous": "正等待前一次同步", + "synchronizing.savedata": "同步儲存資料(savedata)", + "sync.failed": "出錯。點擊此處以查看該錯誤。", + "sync.completed": "同步完成,共耗時 {{time}} 秒", + "sync.conflict": "同步時遭遇衝突。", + "rclone.error.logs": "日誌同步出錯", + "resynchronizing.savedata": "重新同步儲存資料(savedata),請稍候", + "log.files": "日誌檔案", + "app.logs": "插件日誌", + "sync.logs": "同步日誌", + "no.available.logs": "無可用日誌", + "deleting.locks.sync": "刪除鎖定並同步", + "delete.locks": "刪除鎖定檔案(locks)" +} \ No newline at end of file diff --git a/backend/rcloneLauncher b/backend/rcloneLauncher index 7230f16..9a720c9 100644 --- a/backend/rcloneLauncher +++ b/backend/rcloneLauncher @@ -1,7 +1,8 @@ #!/usr/bin/env bash -PLUGIN_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -CONFIG_DIR="$PLUGIN_DIR/../../../settings/decky-cloud-save" +BIN_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +PLUGIN_DIR=$(realpath $BIN_DIR/..) +CONFIG_DIR=$(realpath $PLUGIN_DIR/../../settings/$(basename $PLUGIN_DIR)) -exec "$PLUGIN_DIR/rclone" "--config" "$CONFIG_DIR/rclone.conf" "$@" +exec "$BIN_DIR/rclone" "--config" "$CONFIG_DIR/rclone.conf" "$@" From 841fad13ac6a9332f6adb1e6916270eaf4e9e66a Mon Sep 17 00:00:00 2001 From: Renn <8340896+AkazaRenn@users.noreply.github.com> Date: Sat, 24 Aug 2024 17:46:54 +0000 Subject: [PATCH 16/19] Fix rclone launcher path --- backend/rcloneLauncher | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/rcloneLauncher b/backend/rcloneLauncher index 9a720c9..7d39037 100644 --- a/backend/rcloneLauncher +++ b/backend/rcloneLauncher @@ -1,8 +1,8 @@ #!/usr/bin/env bash BIN_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -PLUGIN_DIR=$(realpath $BIN_DIR/..) -CONFIG_DIR=$(realpath $PLUGIN_DIR/../../settings/$(basename $PLUGIN_DIR)) +PLUGIN_DIR=$(realpath "$BIN_DIR"/..) +CONFIG_DIR=$(realpath "$PLUGIN_DIR"/../../settings/"$(basename "$PLUGIN_DIR")") exec "$BIN_DIR/rclone" "--config" "$CONFIG_DIR/rclone.conf" "$@" From ecdaee7362644ca16cd3a5da0c07122606a91537 Mon Sep 17 00:00:00 2001 From: Renn <8340896+AkazaRenn@users.noreply.github.com> Date: Sat, 24 Aug 2024 18:42:00 +0000 Subject: [PATCH 17/19] Restore missing change --- src/helpers/translator.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/helpers/translator.ts b/src/helpers/translator.ts index 57a690c..fe6c73c 100644 --- a/src/helpers/translator.ts +++ b/src/helpers/translator.ts @@ -3,6 +3,7 @@ import spanish from "../../assets/languages/es.json"; import french from "../../assets/languages/fr.json"; import portuguese from "../../assets/languages/pt.json"; import german from "../../assets/languages/de.json"; +import chinese from "../../assets/languages/zh.json"; import { Logger } from "./logger"; /** @@ -14,7 +15,8 @@ enum Language { latam, french, portuguese, - german + german, + chinese } /** @@ -27,7 +29,7 @@ export class Translator { */ private constructor(){ } - + /** * An object that maps languages to their respective dictionaries. */ @@ -37,7 +39,8 @@ export class Translator { [Language.latam]: spanish, [Language.french]: french, [Language.portuguese]: portuguese, - [Language.german]: german + [Language.german]: german, + [Language.chinese]: chinese }; /** From fdc81251d3843f6a1574c2ae2a89934acb5f7549 Mon Sep 17 00:00:00 2001 From: Renn <8340896+AkazaRenn@users.noreply.github.com> Date: Sat, 24 Aug 2024 19:25:47 +0000 Subject: [PATCH 18/19] Update build.yml to publish releases --- .github/workflows/build.yml | 19 +++++++++++++------ package.json | 1 + 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0b66c5e..c902602 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,6 +13,9 @@ jobs: runs-on: ubuntu-latest steps: + - name: Create variables + run: echo "TIMESTAMP=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV + - name: Checkout uses: actions/checkout@v4 with: @@ -22,7 +25,6 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} - name: Append hash to plugin version - if: ${{ github.event_name == 'pull_request_target' }} run: | echo "::notice::This run was triggered by a commit. Appending the commit hash to the plugin version." @@ -41,10 +43,8 @@ jobs: echo "::endgroup::" - uses: pnpm/action-setup@v3 - with: - version: 9.4.0 + - name: Update pnpm lockfile - if: ${{ inputs.force_pnpm != 'none' }} run: | $(which pnpm) install --lockfile-only @@ -68,8 +68,15 @@ jobs: done tree out - - name: Upload Artifacts to Github + - name: Upload Artifacts uses: actions/upload-artifact@v4 with: name: output - path: out/*/ \ No newline at end of file + path: out/*/ + + - name: Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ env.TIMESTAMP }} + prerelease: true + files: out/*.zip \ No newline at end of file diff --git a/package.json b/package.json index 936f662..7f5c18f 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "decky-cloud-save", "version": "1.4.1", "description": "Manage cloud saves for games that do not support it in [current year].", + "packageManager": "pnpm@9.8.0", "scripts": { "build": "shx rm -rf dist && rollup -c", "watch": "rollup -c -w", From a8d6085d9c582792757fff2d84f94b53d4cd9455 Mon Sep 17 00:00:00 2001 From: Renn <8340896+AkazaRenn@users.noreply.github.com> Date: Sat, 24 Aug 2024 23:57:28 +0000 Subject: [PATCH 19/19] Fix sync state tracking --- py_modules/rclone_sync_manager.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/py_modules/rclone_sync_manager.py b/py_modules/rclone_sync_manager.py index 4d23776..fdcbc0c 100644 --- a/py_modules/rclone_sync_manager.py +++ b/py_modules/rclone_sync_manager.py @@ -21,14 +21,6 @@ async def delete_lock_files(self): os.remove(hgx) async def sync_now(self, winner: str, resync: bool): - destination_path = plugin_config.get_config_item("destination_directory", "decky-cloud-save") - await self.sync_now_internal( - ["/", f"backend:{destination_path}", "--filter-from", plugin_config.cfg_syncpath_filter_file], - plugin_config.get_config_item("bisync_enabled", False), - winner, - resync - ) - for k, v in plugin_config.get_library_sync_config().items(): if v.get("enabled", False): await self.sync_now_internal( @@ -38,14 +30,22 @@ async def sync_now(self, winner: str, resync: bool): resync ) + destination_path = plugin_config.get_config_item("destination_directory", "decky-cloud-save") + await self.sync_now_internal( + ["/", f"backend:{destination_path}", "--filter-from", plugin_config.cfg_syncpath_filter_file], + plugin_config.get_config_item("bisync_enabled", False), + winner, + resync + ) + async def sync_now_internal(self, path_args: list, bisync: bool, winner: str, resync: bool): """ Initiates a synchronization process using rclone. Parameters: + path_args (list[str]): List of arguments for rclone, it contains the destination and filter path winner (str): The winner of any conflicts during synchronization. resync (bool): Whether to perform a resynchronization. - """ args = []