Compare commits

...

15 Commits

Author SHA1 Message Date
Party Wumpus 0c83c9a2b5 Bump DFL to 3.20.7 (#449) 2023-05-10 22:12:27 +01:00
Marco Rodolfi 6b14f08d59 Hotfix: wrong variable name for English translation (#444) 2023-05-09 15:40:57 -07:00
Marco Rodolfi 089e6b086c Small fix: handle missing localization in backend plus a small typo in the english language (#443)
* Hotfix for i18n where the detector was overriding localStorage

* Please, pnpm, cooperate

* Small fix regarding the backend getting hammered when switching to not supported languages plus a small english typo
2023-05-04 16:46:00 +01:00
Marco Rodolfi 08d5c942a4 Hotfix for i18n where the detector was overriding localStorage (#439)
* Hotfix for i18n where the detector was overriding localStorage

* Please, pnpm, cooperate
2023-05-03 21:51:18 +01:00
Marco Rodolfi 35e7c80835 [Feature] Implement internazionalization for Decky Loader (#361)
* First iteration for internationalization of the loader

* First iteration for internationalization of the loader

* Cleanup node mess

* Cleanup node mess pt2

* Additional touches

* Latest decky changed merged into i18n and updated translation.

* Styling fixes

* Initial backend hosting implementation

* Added correct url path of the loopback server.

* Added correct url path of the loopback server.

* Some better namespaced text.

* Added whitelist for locales path.

* Refactor languages and fix hooks logic bugs.

* Small typo in language translation structure.

* Working backend, automatically swtich languages with steam and language fixes.

* Fix to languages

* Key fixes

* Additional language fixes.

* Additional json changes

* Final text revision and added a vscode tasks to automatically extract text from code.

* Typo in the middleware

* Remove unused imports

* Cleanup whitespaces.

* Import changes

* Revert "Import changes"

This reverts commit 8e8231950f.

* Update index.d.ts

* Clean up unused imports

* Delete pnpm-lock.yaml

* Update rollup.config.js

* Update PluginInstallModal.tsx

* Update index.tsx

* Update plugin-loader.tsx

* Update plugin-loader.tsx

* Revert "Delete pnpm-lock.yaml"

This reverts commit 3a39f36f21.

* Additional strings reworks.

* Fixes for issues coming from github merge.

* Fixes for master

* Styling fixes

* Styling pt2

* Missed a few strings in master,

* Styling fixes

* Additional master merge fixes.

* Final cleanup and adaptation to master.

* Final empty language cleanup and few string added

* Small changes to italian translation

* Disabled translation on a few components inside plugin-loader for missing react hooks.

* Fixed passing tag to translation.

* Disable debug output for reducing console spam.

* Return correct content type

* Small italian language change

* Added support for country code

* Fixed missing translation for uninstall popup.

* Fix class name shenanigans for  toast notification

* Update dependencies

* Fixed github workflow to include the new locales folder

* Update dependencies to latest version (unless it's React) and fixed the new small errors that cropped up

* Missed a file name change

* Updated dev dependencies to latest version

* Missed a few dev dependencies

* Revert "Update dependencies to latest version (unless it's React) and fixed the new small errors that cropped up"

Messed up merge with a different main branch

* Messed up deletion of rollup config.

* Fix broken pnpm lock file

* Missed a localized string during the merge

* Fixed a parameter mistake in the uninstall text parameter

* Fix pnpm random issues

* Small italian language tweaks

* Fix wrong parameter passed to the uninstall function call

* Another fix on a wrong function parameter

* Additional translation text on the store and branch selection channels

* Changed the default type passed to map to being able to index the two arrays.

* Reverted and reworked the last changes

* Distinguish events in UI for installing vs reinstalling plugins

* Additional fixes for reinstall prompt

* Revert the use of intevalPlural since the parser doesn't seem to support that.

* Missed a routing path in the backend

* Small bugfixes

* Small fixes

* Correctly adding the parameter to the request headers.

* Refactoring of the UI popup modal

* Fix pnpm shenanigans

* Final fixes for the install UI localization

* Clean up unnedeed backend code

* Small rework on text selection.

* Cleaned up parser configuration

* Removed extracttext dependency to pnpmsetup

* Merged translation and cleaned up parser

* Fixed JSON structure after manual merge.

* Added translation to the file picker

* Revert changes to PluginInstallModal

* Reworked the text modal for the final time

* Missed the proper linted text

* Missed the backend change

* Final branch cleanup

* Fixed small translation bleeding

Caused from the manual merge of _old.json files.

* fix extra space in browser.py

* fix extra newline in plugin-loader.tsx

* Cleanup i18next-parser.config.mjs

* Update plugin-loader.tsx

* Cleanup language files

* Better labeling of text

* Fixed language typos in BranchSelect

* Fixed language typos in StoreSelect

* Cleanup plugin-loader.tsx from unused imports

* Removed the path bypass since I'm using authentication from the frontend.

* Reimplemented this component as a functional component.

* Updated dependencies and lockfile

* Removed static route from main.py

Already handled in loader.py

* Small italian coherency fixes

* Fix small typography fixes on plugin name uninstall

* Fixed italian typo on removal popup

* Reenabled manual escaping value in i18next

* Set to fallback to the default language if the string in the JSON file is empty.

* Fixed pnpm wankery

* Added a missed italian text translation string

---------

Co-authored-by: AAGaming <aa@mail.catvibers.me>
2023-05-02 16:42:39 +01:00
AAGaming caf37d681f Fix tab jank on latest steam beta 2023-04-28 22:29:46 -04:00
EMERALD 93151e4e5e Add file picker plugin install, plugin installs to developer page (#405) 2023-04-25 19:20:39 -07:00
suchmememanyskill d6f336d84b Feat/configurable paths (#404) 2023-04-24 20:12:42 -07:00
Beebles 4777963b65 Make patch notes modal only show current branch (#429) 2023-04-23 17:18:54 -07:00
Party Wumpus fc193f98db Fix browser.py (#431) 2023-04-21 20:14:34 -07:00
Travis Lane a07e4d6fe6 fix: version is no longer missing from plugin list (#417) 2023-04-10 16:47:59 -07:00
Party Wumpus 4ab7d97ab2 Various readme changes (#422)
* various installation clarifications

* Update README.md

* Update README.md

* Update README.md

* rip crankshaft, hope you come back one day

* Update README.md

* oops i linked to PartyWumpus/decky-loader by accident

i fix
2023-04-07 11:04:08 -07:00
AAGaming 15a6f7fdb8 fix UI reloading on latest beta to prevent freeze when updating Decky 2023-04-06 13:00:54 -04:00
AAGaming 7d2cff8745 fix two missing arguments, fixing reordering of newly installed plugins (#412) 2023-04-04 14:52:32 -04:00
Party Wumpus ee5ed3faf0 Make readme's download button into an svg (#413) 2023-04-04 12:36:30 -04:00
43 changed files with 2785 additions and 501 deletions
+1 -1
View File
@@ -69,7 +69,7 @@ jobs:
run: pnpm run build
- name: Build Python Backend 🛠️
run: pyinstaller --noconfirm --onefile --name "PluginLoader" --add-data ./backend/static:/static --add-data ./backend/legacy:/legacy --add-data ./plugin:/plugin ./backend/*.py
run: pyinstaller --noconfirm --onefile --name "PluginLoader" --add-data ./backend/static:/static --add-data ./backend/locales:/locales --add-data ./backend/legacy:/legacy --add-data ./plugin:/plugin ./backend/*.py
- name: Upload package artifact ⬆️
if: ${{ !env.ACT }}
+8
View File
@@ -41,6 +41,14 @@
"command": "rsync -azp --rsh='ssh -p ${config:deckport} ${config:deckkey}' requirements.txt deck@${config:deckip}:${config:deckdir}/homebrew/dev/pluginloader/requirements.txt && ssh deck@${config:deckip} -p ${config:deckport} ${config:deckkey} 'python -m ensurepip && python -m pip install --upgrade pip && python -m pip install --upgrade setuptools && python -m pip install -r ${config:deckdir}/homebrew/dev/pluginloader/requirements.txt'",
"problemMatcher": []
},
{
"label": "extracttext",
"type": "shell",
"group": "none",
"detail": "Check for new strings in the frontend source code and extract it into the corresponding json language files",
"command": "cd frontend && ./node_modules/.bin/i18next --config ./i18next-parser.config.mjs",
"problemMatcher": []
},
// BUILD
{
"label": "pnpmsetup",
+8 -9
View File
@@ -3,7 +3,7 @@
<br>
Decky Loader
<br>
<a name="logo" href="https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/decky_installer.desktop"><img src="./docs/images/download_button.png" alt="Download decky" width="350"></a>
<a name="download button" href="https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/decky_installer.desktop"><img src="./docs/images/download_button.svg" alt="Download decky" width="350px" style="padding-top: 15px;"></a>
</h1>
<p align="center">
@@ -33,29 +33,28 @@ For more information about Decky Loader as well as documentation and development
### 🤔 Common Issues
- Crankshaft is incompatible with Decky Loader. If you are using Crankshaft, please uninstall it before installing Decky Loader.
- Syncthing may use port 8080 on Steam Deck, which Decky Loader needs to function. If you are using Syncthing as a service, please change its port to something else.
- If you are using any software that uses port 1337 or 8080, please change its port to something else or uninstall it.
- If you run the installer and it just opens a file in a text editor: click the (...) button in the top right of dolphin (the file manager) then 'configure' and 'configure dolphin'. Click on the 'confirmations' tab and set 'when opening an executable file' to 'run script'.
- Sometimes Decky will disappear on SteamOS updates. This can easily be fixed by just re-running the installer and installing the stable branch again. If this doesn't work, try installing the prerelease instead. If that doesn't work, then [check the existing issues](https://github.com/SteamDeckHomebrew/decky-loader/issues) and if there isn't one then you can [file a new issue](https://github.com/SteamDeckHomebrew/decky-loader/issues/new?assignees=&labels=bug&template=bug_report.yml&title=%5BBUG%5D+%3Ctitle%3E).
## 💾 Installation
- This installation can be done without an admin/sudo password set.
1. Prepare a mouse and keyboard if possible.
- Keyboards and mice can be connected to the Steam Deck via USB-C or Bluetooth.
- Many Bluetooth keyboard and mouse apps are available for iOS and Android.
- Many Bluetooth keyboard and mouse apps are available for iOS and Android. (KDE connect is preinstalled on the steam deck)
- The Steam Link app is available on [Windows](https://media.steampowered.com/steamlink/windows/latest/SteamLink.zip), [macOS](https://apps.apple.com/us/app/steam-link/id1246969117), and [Linux](https://flathub.org/apps/details/com.valvesoftware.SteamLink). It works well as a remote desktop substitute.
- If you have no other options, use the right trackpad as a mouse and press <img src="./docs/images/light/steam.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/steam.svg#gh-light-mode-only" height=16>+<img src="./docs/images/light/x.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/x.svg#gh-light-mode-only" height=16> to open the on-screen keyboard as needed.
1. Press the <img src="./docs/images/light/steam.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/steam.svg#gh-light-mode-only" height=16> button and open the Power menu.
1. Select "Switch to Desktop".
1. Navigate to this Github page on a browser of your choice.
1. Press the 'Download' button at the top of the page.
1. Run the downloaded file by clicking on it in Dolphin (the file manager).
1. Either type your admin password or allow Decky to temporarily set your password to `Decky!`
1. Download the [installer file](https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/decky_installer.desktop).
1. Drag the file onto your desktop and double click it to run it.
1. Either type your admin password or allow Decky to temporarily set your admin password to `Decky!` (this password will be removed after the installer finishes)
1. Choose the version of Decky Loader you want to install.
- **Latest Release**
Intended for most users. This is the latest stable version of Decky Loader.
- **Latest Pre-Release**
Intended for plugin developers. Pre-releases are unlikely to be fully stable but contain the latest changes. For more information on plugin development, please consult [the wiki page](https://deckbrew.xyz/en/loader-dev/development).
Intended for plugin developers. Pre-releases are unlikely to be fully stable but contain the latest changes. For more information on plugin development, please consult [the wiki page](https://wiki.deckbrew.xyz/en/loader-dev/development).
1. Open the Return to Gaming Mode shortcut on your desktop.
- There is also a fast install for those who can use Konsole. Run `curl -L https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/install_release.sh | sh` and type your password when prompted.
@@ -66,7 +65,7 @@ We are sorry to see you go! If you are considering uninstalling because you are
1. Press the <img src="./docs/images/light/steam.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/steam.svg#gh-light-mode-only" height=16> button and open the Power menu.
1. Select "Switch to Desktop".
1. Run the installer file again, and select `uninstall decky loader`
1. Run the installer file again, and select `uninstall decky loader`.
- There is also a fast uninstall for those who can use Konsole. Run `curl -L https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/uninstall.sh | sh` and type your password when prompted.
## 🚀 Getting Started
+63 -44
View File
@@ -118,7 +118,7 @@ class PluginBrowser:
# plugins_snapshot = self.plugins.copy()
# snapshot_string = pformat(plugins_snapshot)
# logger.debug("current plugins: %s", snapshot_string)
if self.plugins[name]:
if name in self.plugins:
logger.debug("Plugin %s was found", name)
self.plugins[name].stop()
logger.debug("Plugin %s was stopped", name)
@@ -139,6 +139,10 @@ class PluginBrowser:
self.loader.watcher.disabled = False
async def _install(self, artifact, name, version, hash):
# Will be set later in code
res_zip = None
# Check if plugin is installed
isInstalled = False
if self.loader.watcher:
self.loader.watcher.disabled = True
@@ -148,54 +152,69 @@ class PluginBrowser:
isInstalled = True
except:
logger.error(f"Failed to determine if {name} is already installed, continuing anyway.")
logger.info(f"Installing {name} (Version: {version})")
async with ClientSession() as client:
logger.debug(f"Fetching {artifact}")
res = await client.get(artifact, ssl=get_ssl_context())
if res.status == 200:
logger.debug("Got 200. Reading...")
data = await res.read()
logger.debug(f"Read {len(data)} bytes")
res_zip = BytesIO(data)
if isInstalled:
try:
logger.debug("Uninstalling existing plugin...")
await self.uninstall_plugin(name)
except:
logger.error(f"Plugin {name} could not be uninstalled.")
logger.debug("Unzipping...")
ret = self._unzip_to_plugin_dir(res_zip, name, hash)
if ret:
plugin_folder = self.find_plugin_folder(name)
plugin_dir = path.join(self.plugin_path, plugin_folder)
ret = await self._download_remote_binaries_for_plugin_with_name(plugin_dir)
if ret:
logger.info(f"Installed {name} (Version: {version})")
if name in self.loader.plugins:
self.loader.plugins[name].stop()
self.loader.plugins.pop(name, None)
await sleep(1)
current_plugin_order = self.settings.getSetting("pluginOrder")
current_plugin_order.append(name)
self.settings.setSetting("pluginOrder", current_plugin_order)
logger.debug("Plugin %s was added to the pluginOrder setting", name)
self.loader.import_plugin(path.join(plugin_dir, "main.py"), plugin_folder)
else:
logger.fatal(f"Failed Downloading Remote Binaries")
else:
self.log.fatal(f"SHA-256 Mismatch!!!! {name} (Version: {version})")
if self.loader.watcher:
self.loader.watcher.disabled = False
else:
logger.fatal(f"Could not fetch from URL. {await res.text()}")
async def request_plugin_install(self, artifact, name, version, hash):
# Check if the file is a local file or a URL
if artifact.startswith("file://"):
logger.info(f"Installing {name} from local ZIP file (Version: {version})")
res_zip = BytesIO(open(artifact[7:], "rb").read())
else:
logger.info(f"Installing {name} from URL (Version: {version})")
async with ClientSession() as client:
logger.debug(f"Fetching {artifact}")
res = await client.get(artifact, ssl=get_ssl_context())
if res.status == 200:
logger.debug("Got 200. Reading...")
data = await res.read()
logger.debug(f"Read {len(data)} bytes")
res_zip = BytesIO(data)
else:
logger.fatal(f"Could not fetch from URL. {await res.text()}")
# Check to make sure we got the file
if res_zip is None:
logger.fatal(f"Could not fetch {artifact}")
return
# If plugin is installed, uninstall it
if isInstalled:
try:
logger.debug("Uninstalling existing plugin...")
await self.uninstall_plugin(name)
except:
logger.error(f"Plugin {name} could not be uninstalled.")
# Install the plugin
logger.debug("Unzipping...")
ret = self._unzip_to_plugin_dir(res_zip, name, hash)
if ret:
plugin_folder = self.find_plugin_folder(name)
plugin_dir = path.join(self.plugin_path, plugin_folder)
ret = await self._download_remote_binaries_for_plugin_with_name(plugin_dir)
if ret:
logger.info(f"Installed {name} (Version: {version})")
if name in self.loader.plugins:
self.loader.plugins[name].stop()
self.loader.plugins.pop(name, None)
await sleep(1)
current_plugin_order = self.settings.getSetting("pluginOrder")
current_plugin_order.append(name)
self.settings.setSetting("pluginOrder", current_plugin_order)
logger.debug("Plugin %s was added to the pluginOrder setting", name)
self.loader.import_plugin(path.join(plugin_dir, "main.py"), plugin_folder)
else:
logger.fatal(f"Failed Downloading Remote Binaries")
else:
self.log.fatal(f"SHA-256 Mismatch!!!! {name} (Version: {version})")
if self.loader.watcher:
self.loader.watcher.disabled = False
async def request_plugin_install(self, artifact, name, version, hash, install_type):
request_id = str(time())
self.install_requests[request_id] = PluginInstallContext(artifact, name, version, hash)
tab = await get_gamepadui_tab()
await tab.open_websocket()
await tab.evaluate_js(f"DeckyPluginLoader.addPluginInstallPrompt('{name}', '{version}', '{request_id}', '{hash}')")
await tab.evaluate_js(f"DeckyPluginLoader.addPluginInstallPrompt('{name}', '{version}', '{request_id}', '{hash}', {install_type})")
async def confirm_plugin_install(self, request_id):
request = self.install_requests.pop(request_id)
+2 -2
View File
@@ -36,9 +36,9 @@ async def csrf_middleware(request, handler):
return await handler(request)
return Response(text='Forbidden', status='403')
# Get the default homebrew path unless a home_path is specified
# Get the default homebrew path unless a home_path is specified. home_path argument is deprecated
def get_homebrew_path(home_path = None) -> str:
return os.path.join(home_path if home_path != None else localplatform.get_home_path(), "homebrew")
return localplatform.get_unprivileged_path()
# Recursively create path and chown as user
def mkdir_as_user(path):
+3 -2
View File
@@ -111,7 +111,7 @@ class Tab:
"method": "Page.disable",
}, False)
async def refresh(self):
async def refresh(self, manage_socket=True):
try:
if manage_socket:
await self.open_websocket()
@@ -395,6 +395,7 @@ async def get_tab_lambda(test) -> Tab:
return tab
SHARED_CTX_NAMES = ["SharedJSContext", "Steam Shared Context presented by Valve™", "Steam", "SP"]
DO_NOT_CLOSE_URL = "Valve Steam Gamepad/default" # Steam Big Picture Mode tab
def tab_is_gamepadui(t: Tab) -> bool:
return "https://steamloopback.host/routes/" in t.url and t.title in SHARED_CTX_NAMES
@@ -414,7 +415,7 @@ async def inject_to_tab(tab_name, js, run_async=False):
async def close_old_tabs():
tabs = await get_tabs()
for t in tabs:
if not t.title or t.title not in SHARED_CTX_NAMES:
if not t.title or (t.title not in SHARED_CTX_NAMES and DO_NOT_CLOSE_URL not in t.url):
logger.debug("Closing tab: " + getattr(t, "title", "Untitled"))
await t.close()
await sleep(0.5)
+10
View File
@@ -77,6 +77,7 @@ class Loader:
server_instance.add_routes([
web.get("/frontend/{path:.*}", self.handle_frontend_assets),
web.get("/locales/{path:.*}", self.handle_frontend_locales),
web.get("/plugins", self.get_plugins),
web.get("/plugins/{plugin_name}/frontend_bundle", self.handle_frontend_bundle),
web.post("/plugins/{plugin_name}/methods/{method_name}", self.handle_plugin_method_call),
@@ -99,6 +100,15 @@ class Loader:
return web.FileResponse(file, headers={"Cache-Control": "no-cache"})
async def handle_frontend_locales(self, request):
req_lang = request.match_info["path"]
file = path.join(path.dirname(__file__), "locales", req_lang)
if exists(file):
return web.FileResponse(file, headers={"Cache-Control": "no-cache", "Content-Type": "application/json"})
else:
self.logger.info(f"Language {req_lang} not available, returning an empty dictionary")
return web.json_response(data={}, headers={"Cache-Control": "no-cache"})
async def get_plugins(self, request):
plugins = list(self.plugins.values())
return web.json_response([{"name": str(i) if not i.legacy else "$LEGACY_"+str(i), "version": i.version} for i in plugins])
+183
View File
@@ -0,0 +1,183 @@
{
"BranchSelect": {
"update_channel": {
"label": "Update Channel",
"prerelease": "Prerelease",
"stable": "Stable",
"testing": "Testing"
}
},
"Developer": {
"5secreload": "Reloading in 5 seconds",
"disabling": "Disabling",
"enabling": "Enabling"
},
"FilePickerIndex": {
"folder": {
"select": "Use this folder"
}
},
"PluginCard": {
"plugin_full_access": "This plugin has full access to your Steam Deck.",
"plugin_install": "Install",
"plugin_no_desc": "No description provided.",
"plugin_version_label": "Plugin Version"
},
"PluginInstallModal": {
"install": {
"button_idle": "Install",
"button_processing": "Installing",
"desc": "Are you sure you want to install {{artifact}} {{version}}?",
"title": "Install {{artifact}}"
},
"no_hash": "This plugin does not have a hash, you are installing it at your own risk.",
"reinstall": {
"button_idle": "Reinstall",
"button_processing": "Reinstalling",
"desc": "Are you sure you want to reinstall {{artifact}} {{version}}?",
"title": "Reinstall {{artifact}}"
},
"update": {
"button_idle": "Update",
"button_processing": "Updating",
"desc": "Are you sure you want to update {{artifact}} {{version}}?",
"title": "Update {{artifact}}"
}
},
"PluginListIndex": {
"no_plugin": "No plugins installed!",
"plugin_actions": "Plugin Actions",
"reinstall": "Reinstall",
"reload": "Reload",
"uninstall": "Uninstall",
"update_to": "Update to {{name}}"
},
"PluginLoader": {
"decky_title": "Decky",
"decky_update_available": "Update to {{tag_name}} available!",
"error": "Error",
"plugin_error_uninstall": "Please go to {{-icon}} in the Decky menu if you need to uninstall this plugin.",
"plugin_load_error": {
"message": "Error loading plugin {{name}}",
"toast": "Error loading {{name}}"
},
"plugin_uninstall": {
"button": "Uninstall",
"desc": "Are you sure you want to uninstall {{name}}?",
"title": "Uninstall {{name}}"
},
"plugin_update_one": "Updates available for 1 plugin!",
"plugin_update_other": "Updates available for {{count}} plugins!"
},
"RemoteDebugging": {
"remote_cef": {
"desc": "Allow unauthenticated access to the CEF debugger to anyone in your network",
"label": "Allow Remote CEF Debugging"
}
},
"SettingsDeveloperIndex": {
"header_other": "Other",
"react_devtools": {
"desc": "Enables connection to a computer running React DevTools. Changing this setting will reload Steam. Set the IP address before enabling.",
"ip_label": "IP",
"label": "Enable React DevTools"
},
"third_party_plugins": {
"button_install": "Install",
"button_zip": "Browse",
"header": "Third-Party Plugins",
"label_desc": "URL",
"label_url": "Install Plugin from URL",
"label_zip": "Install Plugin from ZIP File"
},
"toast_zip": {
"body": "Installation failed! Only ZIP files are supported.",
"title": "Decky"
},
"valve_internal": {
"desc1": "Enables the Valve internal developer menu.",
"desc2": "Do not touch anything in this menu unless you know what it does.",
"label": "Enable Valve Internal"
}
},
"SettingsGeneralIndex": {
"about": {
"decky_version": "Decky Version",
"header": "About"
},
"beta": {
"header": "Beta participation"
},
"developer_mode": {
"desc": "Enables Decky's developer settings.",
"label": "Developer mode"
},
"other": {
"header": "Other"
},
"updates": {
"header": "Updates"
}
},
"SettingsIndex": {
"developer_title": "Developer",
"general_title": "General",
"navbar_settings": "Decky Settings",
"plugins_title": "Plugins"
},
"Store": {
"store_contrib": {
"desc": "If you would like to contribute to the Decky Plugin Store, check the SteamDeckHomebrew/decky-plugin-template repository on GitHub. Information on development and distribution is available in the README.",
"label": "Contributing"
},
"store_filter": {
"label": "Filter",
"label_def": "All"
},
"store_search": {
"label": "Search"
},
"store_sort": {
"label": "Sort",
"label_def": "Last Updated (Newest)"
},
"store_source": {
"desc": "All plugin source code is available on SteamDeckHomebrew/decky-plugin-database repository on GitHub.",
"label": "Source Code"
},
"store_tabs": {
"about": "About",
"alph_asce": "Alphabetical (Z to A)",
"alph_desc": "Alphabetical (A to Z)",
"title": "Browse"
},
"store_testing_cta": "Please consider testing new plugins to help the Decky Loader team!"
},
"StoreSelect": {
"custom_store": {
"label": "Custom Store",
"url_label": "URL"
},
"store_channel": {
"custom": "Custom",
"default": "Default",
"label": "Store Channel",
"testing": "Testing"
}
},
"Updater": {
"decky_updates": "Decky Updates",
"no_patch_notes_desc": "no patch notes for this version",
"patch_notes_desc": "Patch Notes",
"updates": {
"check_button": "Check For Updates",
"checking": "Checking",
"cur_version": "Current version: {{ver}}",
"install_button": "Install Update",
"label": "Updates",
"lat_version": "Up to date: running {{ver}}",
"reloading": "Reloading",
"updating": "Updating"
}
}
}
+184
View File
@@ -0,0 +1,184 @@
{
"BranchSelect": {
"update_channel": {
"label": "Canale di aggiornamento",
"prerelease": "Prerilascio",
"stable": "Stabile",
"testing": "In prova"
}
},
"Developer": {
"5secreload": "Ricaricando in 5 secondi",
"disabling": "Disabilitando",
"enabling": "Abilitando"
},
"FilePickerIndex": {
"folder": {
"select": "Usa questa cartella"
}
},
"PluginCard": {
"plugin_full_access": "Questo plugin ha accesso completo al tuo Steam Deck.",
"plugin_install": "Installa",
"plugin_no_desc": "Nessuna descrizione fornita.",
"plugin_version_label": "Versione Plugin"
},
"PluginInstallModal": {
"install": {
"button_idle": "Installa",
"button_processing": "Installando",
"desc": "Sei sicuro di voler installare {{artifact}} {{version}}?",
"title": "Installa {{artifact}}"
},
"no_hash": "Questo plugin non ha un hash associato, lo stai installando a tuo rischio e pericolo.",
"reinstall": {
"button_idle": "Reinstalla",
"button_processing": "Reinstallando",
"desc": "Sei sicuro di voler reinstallare {{artifact}} {{version}}?",
"title": "Reinstalla {{artifact}}"
},
"update": {
"button_idle": "Aggiorna",
"button_processing": "Aggiornando",
"desc": "Sei sicuro di voler aggiornare {{artifact}} {{version}}?",
"title": "Aggiorna {{artifact}}"
}
},
"PluginListIndex": {
"no_plugin": "Nessun plugin installato!",
"plugin_actions": "Operazioni sui plugins",
"reinstall": "Reinstalla",
"reload": "Ricarica",
"uninstall": "Rimuovi",
"update_to": "Aggiorna a {{name}}"
},
"PluginLoader": {
"decky_title": "Decky",
"decky_update_available": "Disponibile aggiornamento a {{tag_name}}!",
"error": "Errore",
"plugin_error_uninstall": "Per rimuovere questo plugin vai su {{-icon}} nel menu di Decky.",
"plugin_load_error": {
"message": "Errore caricando il plugin {{name}}",
"toast": "Errore caricando {{name}}"
},
"plugin_uninstall": {
"button": "Rimuovi",
"desc": "Sei sicuro di voler rimuovere {{name}}?",
"title": "Rimuovi {{name}}"
},
"plugin_update_one": "Aggiornamento disponibile per un plugin!",
"plugin_update_many": "Aggiornamento disponibile per {{count}} plugins!",
"plugin_update_other": "Aggiornamento disponibile per {{count}} plugins!"
},
"RemoteDebugging": {
"remote_cef": {
"desc": "Permetti l'accesso non autenticato al debugger di CEF da tutti gli indirizzi sulla tua rete locale.",
"label": "Permetti il debug remoto di CEF"
}
},
"SettingsDeveloperIndex": {
"header_other": "Altro",
"react_devtools": {
"desc": "Abilita la connessione ad un computer che esegue i DevTools di React. Cambiando questa impostazione ricaricherà Steam. Imposta l'indirizzo IP prima di abilitarlo.",
"ip_label": "IP",
"label": "Abilita i DevTools di React"
},
"third_party_plugins": {
"button_install": "Installa",
"button_zip": "Seleziona",
"header": "Plugin di terze parti",
"label_desc": "URL",
"label_url": "Installa plugin da un'indirizzo web",
"label_zip": "Installa plugin da un file ZIP"
},
"toast_zip": {
"body": "Installazione non riuscita, solo i file ZIP sono supportati!",
"title": "Decky"
},
"valve_internal": {
"desc1": "Abilita il menu di sviluppo interno di Valve.",
"desc2": "Non toccare nulla in questo menu se non sai quello che fa.",
"label": "Abilita Menu Sviluppatore"
}
},
"SettingsGeneralIndex": {
"about": {
"decky_version": "Versione di Decky",
"header": "Riguardo a"
},
"beta": {
"header": "Partecipazione alla beta"
},
"developer_mode": {
"desc": "Abilità le impostazioni di sviluppo di Decky.",
"label": "Modalità sviluppatore"
},
"other": {
"header": "Altro"
},
"updates": {
"header": "Aggiornamenti"
}
},
"SettingsIndex": {
"developer_title": "Sviluppatore",
"general_title": "Generali",
"navbar_settings": "Impostazioni Decky",
"plugins_title": "Plugins"
},
"Store": {
"store_contrib": {
"desc": "Se desideri contribuire allo store di Decky, puoi trovare un template caricato su GitHub all'indirizzo SteamDeckHomebrew/decky-plugin-template. Informazioni riguardo sviluppo e distribuzione sono disponibili nel README.",
"label": "Contribuisci"
},
"store_filter": {
"label": "Filtra",
"label_def": "Tutto"
},
"store_search": {
"label": "Cerca"
},
"store_sort": {
"label": "Ordina",
"label_def": "Ultimo aggiornato (Più recente)"
},
"store_source": {
"desc": "Tutto il codice sorgente dei plugin è disponibile su GitHub all'indirizzo SteamDeckHomebrew/decky-plugin-database",
"label": "Codice Sorgente"
},
"store_tabs": {
"about": "Riguardo a",
"alph_asce": "Alfabetico (Z a A)",
"alph_desc": "Alfabetico (A a Z)",
"title": "Sfoglia"
},
"store_testing_cta": "Valuta la possibilità di testare nuovi plugin per aiutare il team di Decky Loader!"
},
"StoreSelect": {
"custom_store": {
"label": "Negozio custom",
"url_label": "URL"
},
"store_channel": {
"custom": "Personalizzato",
"default": "Default",
"label": "Canale del negozio",
"testing": "In prova"
}
},
"Updater": {
"decky_updates": "Aggiornamento di Decky",
"no_patch_notes_desc": "nessuna patch notes per questa versione",
"patch_notes_desc": "Cambiamenti",
"updates": {
"check_button": "Cerca aggiornamenti",
"checking": "Controllando",
"cur_version": "Versione attuale: {{ver}}",
"install_button": "Installa aggiornamento",
"label": "Aggiornamenti",
"lat_version": "Aggiornato. Eseguendo {{ver}}",
"reloading": "Ricaricando",
"updating": "Aggiornando"
}
}
}
+31 -2
View File
@@ -1,4 +1,4 @@
import platform
import platform, os
ON_WINDOWS = platform.system() == "Windows"
ON_LINUX = not ON_WINDOWS
@@ -8,4 +8,33 @@ if ON_WINDOWS:
import localplatformwin as localplatform
else:
from localplatformlinux import *
import localplatformlinux as localplatform
import localplatformlinux as localplatform
def get_privileged_path() -> str:
'''Get path accessible by elevated user. Holds plugins, decky loader and decky loader configs'''
return localplatform.get_privileged_path()
def get_unprivileged_path() -> str:
'''Get path accessible by non-elevated user. Holds plugin configuration, plugin data and plugin logs. Externally referred to as the 'Homebrew' directory'''
return localplatform.get_unprivileged_path()
def get_unprivileged_user() -> str:
'''Get user that should own files made in unprivileged path'''
return localplatform.get_unprivileged_user()
def get_chown_plugin_path() -> bool:
return os.getenv("CHOWN_PLUGIN_PATH", "1") == "1"
def get_server_host() -> str:
return os.getenv("SERVER_HOST", "127.0.0.1")
def get_server_port() -> int:
return int(os.getenv("SERVER_PORT", "1337"))
def get_live_reload() -> bool:
os.getenv("LIVE_RELOAD", "1") == "1"
def get_log_level() -> int:
return {"CRITICAL": 50, "ERROR": 40, "WARNING": 30, "INFO": 20, "DEBUG": 10}[
os.getenv("LOG_LEVEL", "INFO")
]
+67 -11
View File
@@ -1,19 +1,16 @@
import os, pwd, grp, sys
import os, pwd, grp, sys, logging
from subprocess import call, run, DEVNULL, PIPE, STDOUT
from customtypes import UserType
logger = logging.getLogger("localplatform")
# Get the user id hosting the plugin loader
def _get_user_id() -> int:
proc_path = os.path.realpath(sys.argv[0])
pws = sorted(pwd.getpwall(), reverse=True, key=lambda pw: len(pw.pw_dir))
for pw in pws:
if proc_path.startswith(os.path.realpath(pw.pw_dir)):
return pw.pw_uid
raise PermissionError("The plugin loader does not seem to be hosted by any known user.")
return pwd.getpwnam(_get_user()).pw_uid
# Get the user hosting the plugin loader
def _get_user() -> str:
return pwd.getpwuid(_get_user_id()).pw_name
return get_unprivileged_user()
# Get the effective user id of the running process
def _get_effective_user_id() -> int:
@@ -50,10 +47,12 @@ def _get_user_group() -> str:
def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool = True) -> bool:
user_str = ""
if (user == UserType.HOST_USER):
if user == UserType.HOST_USER:
user_str = _get_user()+":"+_get_user_group()
elif (user == UserType.EFFECTIVE_USER):
elif user == UserType.EFFECTIVE_USER:
user_str = _get_effective_user()+":"+_get_effective_user_group()
elif user == UserType.ROOT:
user_str = "root:root"
else:
raise Exception("Unknown User Type")
@@ -135,4 +134,61 @@ async def service_stop(service_name : str) -> bool:
async def service_start(service_name : str) -> bool:
cmd = ["systemctl", "start", service_name]
res = run(cmd, stdout=PIPE, stderr=STDOUT)
return res.returncode == 0
return res.returncode == 0
def get_privileged_path() -> str:
path = os.getenv("PRIVILEGED_PATH")
if path == None:
path = get_unprivileged_path()
return path
def _parent_dir(path : str) -> str:
if path == None:
return None
if path.endswith('/'):
path = path[:-1]
return os.path.dirname(path)
def get_unprivileged_path() -> str:
path = os.getenv("UNPRIVILEGED_PATH")
if path == None:
path = _parent_dir(os.getenv("PLUGIN_PATH"))
if path == None:
logger.debug("Unprivileged path is not properly configured. Making something up!")
# Expected path of loader binary is /home/deck/homebrew/service/PluginLoader
path = _parent_dir(_parent_dir(os.path.realpath(sys.argv[0])))
if not os.path.exists(path):
path = None
if path == None:
logger.warn("Unprivileged path is not properly configured. Defaulting to /home/deck/homebrew")
path = "/home/deck/homebrew" # We give up
return path
def get_unprivileged_user() -> str:
user = os.getenv("UNPRIVILEGED_USER")
if user == None:
# Lets hope we can extract it from the unprivileged dir
dir = os.path.realpath(get_unprivileged_path())
pws = sorted(pwd.getpwall(), reverse=True, key=lambda pw: len(pw.pw_dir))
for pw in pws:
if dir.startswith(os.path.realpath(pw.pw_dir)):
user = pw.pw_name
break
if user == None:
logger.warn("Unprivileged user is not properly configured. Defaulting to 'deck'")
user = 'deck'
return user
+16 -1
View File
@@ -35,4 +35,19 @@ async def service_restart(service_name : str) -> bool:
return True # Stubbed
def get_username() -> str:
return os.getlogin()
return os.getlogin()
def get_privileged_path() -> str:
'''On windows, privileged_path is equal to unprivileged_path'''
return get_unprivileged_path()
def get_unprivileged_path() -> str:
path = os.getenv("UNPRIVILEGED_PATH")
if path == None:
path = os.getenv("PRIVILEGED_PATH", os.path.join(os.path.expanduser("~"), "homebrew"))
return path
def get_unprivileged_user() -> str:
return os.getenv("UNPRIVILEGED_USER", os.getlogin())
+16 -22
View File
@@ -1,6 +1,10 @@
# Change PyInstaller files permissions
import sys
from localplatform import chmod, chown, service_stop, service_start, ON_WINDOWS
from localplatform import (chmod, chown, service_stop, service_start,
ON_WINDOWS, get_log_level, get_live_reload,
get_server_port, get_server_host, get_chown_plugin_path,
get_unprivileged_user, get_unprivileged_path,
get_privileged_path)
if hasattr(sys, '_MEIPASS'):
chmod(sys._MEIPASS, 755)
# Full imports
@@ -20,7 +24,7 @@ from aiohttp_jinja2 import setup as jinja_setup
# local modules
from browser import PluginBrowser
from helpers import (REMOTE_DEBUGGER_UNIT, csrf_middleware, get_csrf_token,
get_homebrew_path, mkdir_as_user, get_system_pythonpaths)
mkdir_as_user, get_system_pythonpaths)
from injector import get_gamepadui_tab, Tab, get_tabs, close_old_tabs
from loader import Loader
@@ -29,33 +33,23 @@ from updater import Updater
from utilities import Utilities
from customtypes import UserType
HOMEBREW_PATH = get_homebrew_path()
CONFIG = {
"plugin_path": getenv("PLUGIN_PATH", path.join(HOMEBREW_PATH, "plugins")),
"chown_plugin_path": getenv("CHOWN_PLUGIN_PATH", "1") == "1",
"server_host": getenv("SERVER_HOST", "127.0.0.1"),
"server_port": int(getenv("SERVER_PORT", "1337")),
"live_reload": getenv("LIVE_RELOAD", "1") == "1",
"log_level": {"CRITICAL": 50, "ERROR": 40, "WARNING": 30, "INFO": 20, "DEBUG": 10}[
getenv("LOG_LEVEL", "INFO")
],
}
basicConfig(
level=CONFIG["log_level"],
level=get_log_level(),
format="[%(module)s][%(levelname)s]: %(message)s"
)
logger = getLogger("Main")
plugin_path = path.join(get_privileged_path(), "plugins")
def chown_plugin_dir():
if not path.exists(CONFIG["plugin_path"]): # For safety, create the folder before attempting to do anything with it
mkdir_as_user(CONFIG["plugin_path"])
if not path.exists(plugin_path): # For safety, create the folder before attempting to do anything with it
mkdir_as_user(plugin_path)
if not chown(CONFIG["plugin_path"], UserType.HOST_USER) or not chmod(CONFIG["plugin_path"], 555):
if not chown(plugin_path, UserType.HOST_USER) or not chmod(plugin_path, 555):
logger.error(f"chown/chmod exited with a non-zero exit code")
if CONFIG["chown_plugin_path"] == True:
if get_chown_plugin_path() == True:
chown_plugin_dir()
class PluginManager:
@@ -70,9 +64,9 @@ class PluginManager:
allow_credentials=True
)
})
self.plugin_loader = Loader(self.web_app, CONFIG["plugin_path"], self.loop, CONFIG["live_reload"])
self.settings = SettingsManager("loader", path.join(HOMEBREW_PATH, "settings"))
self.plugin_browser = PluginBrowser(CONFIG["plugin_path"], self.plugin_loader.plugins, self.plugin_loader, self.settings)
self.plugin_loader = Loader(self.web_app, plugin_path, self.loop, get_live_reload())
self.settings = SettingsManager("loader", path.join(get_privileged_path(), "settings"))
self.plugin_browser = PluginBrowser(plugin_path, self.plugin_loader.plugins, self.plugin_loader, self.settings)
self.utilities = Utilities(self)
self.updater = Updater(self)
@@ -174,7 +168,7 @@ class PluginManager:
pass
def run(self):
return run_app(self.web_app, host=CONFIG["server_host"], port=CONFIG["server_port"], loop=self.loop, access_log=None)
return run_app(self.web_app, host=get_server_host(), port=get_server_port(), loop=self.loop, access_log=None)
if __name__ == "__main__":
if ON_WINDOWS:
+5 -6
View File
@@ -1,6 +1,6 @@
from json import dump, load
from os import mkdir, path, listdir, rename
from localplatform import chown, folder_owner
from localplatform import chown, folder_owner, get_chown_plugin_path
from customtypes import UserType
from helpers import get_homebrew_path
@@ -17,7 +17,6 @@ class SettingsManager:
#Create the folder with the correct permission
if not path.exists(settings_directory):
mkdir(settings_directory)
chown(settings_directory)
#Copy all old settings file in the root directory to the correct folder
for file in listdir(wrong_dir):
@@ -28,9 +27,9 @@ class SettingsManager:
#If the owner of the settings directory is not the user, then set it as the user:
if folder_owner(settings_directory) != UserType.HOST_USER:
chown(settings_directory, UserType.HOST_USER, False)
expected_user = UserType.HOST_USER if get_chown_plugin_path() else UserType.ROOT
if folder_owner(settings_directory) != expected_user:
chown(settings_directory, expected_user, False)
self.settings = {}
@@ -52,7 +51,7 @@ class SettingsManager:
with open(self.path, "w+", encoding="utf-8") as file:
dump(self.settings, file, indent=4, ensure_ascii=False)
def getSetting(self, key, default):
def getSetting(self, key, default=None):
return self.settings.get(key, default)
def setSetting(self, key, value):
+9
View File
@@ -104,6 +104,15 @@ class Updater:
async with ClientSession() as web:
async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases", ssl=helpers.get_ssl_context()) as res:
remoteVersions = await res.json()
if selectedBranch == 0:
logger.debug("release type: release")
remoteVersions = list(filter(lambda ver: ver["tag_name"].startswith("v") and not ver["prerelease"] and not ver["tag_name"].find("-pre") > 0 and ver["tag_name"], remoteVersions))
elif selectedBranch == 1:
logger.debug("release type: pre-release")
remoteVersions = list(filter(lambda ver:ver["tag_name"].startswith("v"), remoteVersions))
else:
logger.error("release type: NOT FOUND")
raise ValueError("no valid branch found")
self.allRemoteVers = remoteVersions
logger.debug("determining release type to find, branch is %i" % selectedBranch)
if selectedBranch == 0:
+3 -2
View File
@@ -61,12 +61,13 @@ class Utilities:
res["success"] = False
return web.json_response(res)
async def install_plugin(self, artifact="", name="No name", version="dev", hash=False):
async def install_plugin(self, artifact="", name="No name", version="dev", hash=False, install_type=0):
return await self.context.plugin_browser.request_plugin_install(
artifact=artifact,
name=name,
version=version,
hash=hash
hash=hash,
install_type=install_type
)
async def confirm_plugin_install(self, request_id):
+2 -1
View File
@@ -9,7 +9,8 @@ Restart=always
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
WorkingDirectory=${HOMEBREW_FOLDER}/services
KillSignal=SIGKILL
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
Environment=UNPRIVILEGED_PATH=${HOMEBREW_FOLDER}
Environment=PRIVILEGED_PATH=${HOMEBREW_FOLDER}
Environment=LOG_LEVEL=DEBUG
[Install]
WantedBy=multi-user.target
+2 -1
View File
@@ -9,7 +9,8 @@ Restart=always
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
WorkingDirectory=${HOMEBREW_FOLDER}/services
KillSignal=SIGKILL
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
Environment=UNPRIVILEGED_PATH=${HOMEBREW_FOLDER}
Environment=PRIVILEGED_PATH=${HOMEBREW_FOLDER}
Environment=LOG_LEVEL=INFO
[Install]
WantedBy=multi-user.target
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

+162
View File
@@ -0,0 +1,162 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="81.700577mm"
height="24.334814mm"
viewBox="0 0 81.700577 24.334814"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="download.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#505050"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="3.659624"
inkscape:cx="115.44902"
inkscape:cy="59.295709"
inkscape:window-width="1827"
inkscape:window-height="1233"
inkscape:window-x="69"
inkscape:window-y="38"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs2">
<linearGradient
inkscape:collect="always"
id="linearGradient4494">
<stop
style="stop-color:#009fff;stop-opacity:1;"
offset="0"
id="stop4490" />
<stop
style="stop-color:#ff1965;stop-opacity:1;"
offset="0.79417855"
id="stop4498" />
<stop
style="stop-color:#b9b500;stop-opacity:1;"
offset="1"
id="stop4492" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4494"
id="linearGradient4496"
x1="49.131042"
y1="118.6573"
x2="150.29259"
y2="138.74957"
gradientUnits="userSpaceOnUse"
spreadMethod="pad"
gradientTransform="matrix(1.0500324,0,0,1,-1.6155884,24.621921)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4494"
id="linearGradient13802"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0500324,0,0,1,-1.6155884,24.621921)"
x1="49.131042"
y1="118.6573"
x2="150.29259"
y2="138.74957"
spreadMethod="pad" />
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-64.149712,-136.3326)">
<rect
style="mix-blend-mode:normal;fill:url(#linearGradient13802);fill-opacity:1;stroke:none;stroke-width:0.271121"
id="rect111"
width="81.700577"
height="24.334814"
x="64.149712"
y="136.3326"
ry="8.1781616" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:#000000;stroke:none;stroke-width:0.264583"
x="66.364288"
y="124.84658"
id="text10382"><tspan
sodipodi:role="line"
id="tspan10380"
style="stroke-width:0.264583"
x="66.364288"
y="124.84658" /></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15.1694px;font-family:sans-serif;-inkscape-font-specification:sans-serif;white-space:pre;inline-size:82.6483;display:inline;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="67.732498"
y="126.05277"
id="text10440"
transform="translate(1.088576,28.135753)"><tspan
x="67.732498"
y="126.05277"
id="tspan13872">Download</tspan></text>
<rect
style="mix-blend-mode:normal;fill:url(#linearGradient4496);fill-opacity:1;stroke:none;stroke-width:0.271121"
id="rect13792"
width="81.700577"
height="24.334814"
x="64.149712"
y="136.3326"
ry="8.1781616" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:#000000;stroke:none;stroke-width:0.264583"
x="66.364288"
y="124.84658"
id="text13796"><tspan
sodipodi:role="line"
id="tspan13794"
style="stroke-width:0.264583"
x="66.364288"
y="124.84658" /></text>
<g
aria-label="Download"
transform="translate(1.088576,28.135753)"
id="text13800"
style="font-size:15.1694px;-inkscape-font-specification:sans-serif;white-space:pre;inline-size:82.6483;display:inline;fill:#ffffff;stroke-width:0.264583">
<path
d="m 77.880751,120.53111 q 0,2.74566 -1.501771,4.14125 -1.486601,1.38041 -4.156416,1.38041 h -3.01871 v -10.83095 h 3.337268 q 1.638295,0 2.836678,0.60678 1.198382,0.60677 1.850666,1.78999 0.652285,1.16804 0.652285,2.91252 z m -1.441093,0.0455 q 0,-2.16923 -1.077028,-3.17041 -1.061858,-1.01635 -3.01871,-1.01635 H 70.5691 v 8.49487 h 1.471432 q 4.399126,0 4.399126,-4.30811 z"
id="path13828" />
<path
d="m 87.164417,121.9722 q 0,2.01753 -1.03152,3.1249 -1.01635,1.10737 -2.760831,1.10737 -1.077027,0 -1.926513,-0.48542 -0.834317,-0.50059 -1.319738,-1.4411 -0.485421,-0.95567 -0.485421,-2.30575 0,-2.01753 1.01635,-3.10972 1.01635,-1.0922 2.760831,-1.0922 1.107366,0 1.941683,0.50059 0.849486,0.48542 1.319738,1.42592 0.485421,0.92534 0.485421,2.27541 z m -6.143608,0 q 0,1.4411 0.561268,2.29058 0.576437,0.83432 1.820328,0.83432 1.228722,0 1.805159,-0.83432 0.576437,-0.84948 0.576437,-2.29058 0,-1.44109 -0.576437,-2.26024 -0.576437,-0.81914 -1.820328,-0.81914 -1.243891,0 -1.805159,0.81914 -0.561268,0.81915 -0.561268,2.26024 z"
id="path13830" />
<path
d="m 94.218174,121.45644 q -0.197202,-0.62194 -0.348896,-1.21355 -0.136525,-0.60677 -0.212372,-0.9405 h -0.06068 q -0.06068,0.33373 -0.197203,0.9405 -0.136524,0.59161 -0.348896,1.22872 l -1.456262,4.56599 h -1.51694 l -2.229902,-8.1308 h 1.380415 l 1.122536,4.33845 q 0.166863,0.65229 0.318557,1.31974 0.151694,0.66745 0.212372,1.10737 h 0.06068 q 0.06068,-0.25788 0.136525,-0.63712 0.09102,-0.37923 0.197202,-0.78881 0.106186,-0.42474 0.212372,-0.75847 l 1.441093,-4.58116 h 1.456262 l 1.395585,4.58116 q 0.166864,0.51576 0.318558,1.12254 0.166863,0.60678 0.227541,1.04669 h 0.06068 q 0.04551,-0.37924 0.197202,-1.04669 0.166864,-0.66745 0.348897,-1.36525 l 1.137705,-4.33845 h 1.365246 l -2.260241,8.1308 h -1.562448 z"
id="path13832" />
<path
d="m 104.8064,117.77028 q 1.45627,0 2.19957,0.71296 0.7433,0.69779 0.7433,2.27541 v 5.29412 h -1.31974 v -5.2031 q 0,-1.95685 -1.82033,-1.95685 -1.35007,0 -1.86583,0.75847 -0.51576,0.75847 -0.51576,2.18439 v 4.21709 h -1.33491 v -8.1308 h 1.07703 l 0.1972,1.10737 h 0.0759 q 0.3944,-0.63711 1.09219,-0.9405 0.69779,-0.31856 1.47143,-0.31856 z"
id="path13834" />
<path
d="m 111.6023,126.05277 h -1.33491 v -11.52874 h 1.33491 z"
id="path13836" />
<path
d="m 121.25003,121.9722 q 0,2.01753 -1.03152,3.1249 -1.01635,1.10737 -2.76084,1.10737 -1.07702,0 -1.92651,-0.48542 -0.83432,-0.50059 -1.31974,-1.4411 -0.48542,-0.95567 -0.48542,-2.30575 0,-2.01753 1.01635,-3.10972 1.01635,-1.0922 2.76083,-1.0922 1.10737,0 1.94169,0.50059 0.84948,0.48542 1.31973,1.42592 0.48543,0.92534 0.48543,2.27541 z m -6.14361,0 q 0,1.4411 0.56127,2.29058 0.57643,0.83432 1.82032,0.83432 1.22873,0 1.80516,-0.83432 0.57644,-0.84948 0.57644,-2.29058 0,-1.44109 -0.57644,-2.26024 -0.57643,-0.81914 -1.82033,-0.81914 -1.24389,0 -1.80515,0.81914 -0.56127,0.81915 -0.56127,2.26024 z"
id="path13838" />
<path
d="m 126.43796,117.78545 q 1.4866,0 2.19956,0.65228 0.71296,0.65229 0.71296,2.07821 v 5.53683 h -0.97084 l -0.25788,-1.15287 h -0.0607 q -0.53093,0.66745 -1.12253,0.98601 -0.57644,0.31856 -1.60796,0.31856 -1.10737,0 -1.8355,-0.57644 -0.72813,-0.59161 -0.72813,-1.8355 0,-1.21355 0.95567,-1.86583 0.95567,-0.66746 2.94287,-0.72814 l 1.38041,-0.0455 v -0.48542 q 0,-1.01635 -0.43991,-1.41076 -0.43991,-0.3944 -1.24389,-0.3944 -0.63712,0 -1.21355,0.1972 -0.57644,0.18203 -1.07703,0.42474 l -0.40957,-1.00118 q 0.53092,-0.28822 1.25906,-0.48542 0.72813,-0.21237 1.51694,-0.21237 z m 0.3944,4.33845 q -1.51694,0.0607 -2.10855,0.48542 -0.57643,0.42474 -0.57643,1.19838 0,0.68262 0.40957,1.00118 0.42474,0.31856 1.07703,0.31856 1.03152,0 1.71414,-0.56127 0.68262,-0.57644 0.68262,-1.75965 v -0.72813 z"
id="path13840" />
<path
d="m 134.7508,126.20447 q -1.51694,0 -2.42711,-1.04669 -0.91016,-1.06186 -0.91016,-3.15524 0,-2.09337 0.91016,-3.15523 0.92534,-1.07703 2.44228,-1.07703 0.9405,0 1.53211,0.3489 0.60677,0.34889 0.98601,0.84948 h 0.091 q -0.0152,-0.1972 -0.0607,-0.57643 -0.0303,-0.39441 -0.0303,-0.62195 v -3.24625 h 1.3349 v 11.52874 h -1.07702 l -0.19721,-1.09219 h -0.0607 q -0.36407,0.51576 -0.97084,0.87982 -0.60678,0.36407 -1.56245,0.36407 z m 0.21237,-1.10737 q 1.2894,0 1.80516,-0.69779 0.53093,-0.71296 0.53093,-2.13889 v -0.24271 q 0,-1.51694 -0.50059,-2.32092 -0.50059,-0.81914 -1.85067,-0.81914 -1.07703,0 -1.62313,0.86465 -0.53093,0.84949 -0.53093,2.29058 0,1.45626 0.53093,2.26024 0.5461,0.80398 1.6383,0.80398 z"
id="path13842" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.9 KiB

+100
View File
@@ -0,0 +1,100 @@
export default {
contextSeparator: '_',
// Key separator used in your translation keys
createOldCatalogs: false,
// Save the \_old files
defaultNamespace: 'translation',
// Default namespace used in your i18next config
defaultValue: '',
// Default value to give to keys with no value
// You may also specify a function accepting the locale, namespace, key, and value as arguments
indentation: 2,
// Indentation of the catalog files
keepRemoved: true,
// Keep keys from the catalog that are no longer in code
keySeparator: '.',
// Key separator used in your translation keys
// If you want to use plain english keys, separators such as `.` and `:` will conflict. You might want to set `keySeparator: false` and `namespaceSeparator: false`. That way, `t('Status: Loading...')` will not think that there are a namespace and three separator dots for instance.
// see below for more details
lexers: {
mjs: ['JavascriptLexer'],
js: ['JavascriptLexer'], // if you're writing jsx inside .js files, change this to JsxLexer
ts: ['JavascriptLexer'],
jsx: ['JsxLexer'],
tsx: ['JsxLexer'],
default: ['JavascriptLexer'],
},
lineEnding: 'auto',
// Control the line ending. See options at https://github.com/ryanve/eol
locales: ['en-US', 'it-IT'],
// An array of the locales in your applications
namespaceSeparator: false,
// Namespace separator used in your translation keys
// If you want to use plain english keys, separators such as `.` and `:` will conflict. You might want to set `keySeparator: false` and `namespaceSeparator: false`. That way, `t('Status: Loading...')` will not think that there are a namespace and three separator dots for instance.
output: '../backend/locales/$LOCALE.json',
// Supports $LOCALE and $NAMESPACE injection
// Supports JSON (.json) and YAML (.yml) file formats
// Where to write the locale files relative to process.cwd()
pluralSeparator: '_',
// Plural separator used in your translation keys
// If you want to use plain english keys, separators such as `_` might conflict. You might want to set `pluralSeparator` to a different string that does not occur in your keys.
input: './src/**/*.{ts,tsx}',
// An array of globs that describe where to look for source files
// relative to the location of the configuration file
sort: true,
// Whether or not to sort the catalog. Can also be a [compareFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#parameters)
verbose: false,
// Display info about the parsing including some stats
failOnWarnings: false,
// Exit with an exit code of 1 on warnings
failOnUpdate: false,
// Exit with an exit code of 1 when translations are updated (for CI purpose)
customValueTemplate: null,
// If you wish to customize the value output the value as an object, you can set your own format.
// ${defaultValue} is the default value you set in your translation function.
// Any other custom property will be automatically extracted.
//
// Example:
// {
// message: "${defaultValue}",
// description: "${maxLength}", // t('my-key', {maxLength: 150})
// }
resetDefaultValueLocale: null,
// The locale to compare with default values to determine whether a default value has been changed.
// If this is set and a default value differs from a translation in the specified locale, all entries
// for that key across locales are reset to the default value, and existing translations are moved to
// the `_old` file.
i18nextOptions: null,
// If you wish to customize options in internally used i18next instance, you can define an object with any
// configuration property supported by i18next (https://www.i18next.com/overview/configuration-options).
// { compatibilityJSON: 'v3' } can be used to generate v3 compatible plurals.
yamlOptions: null,
// If you wish to customize options for yaml output, you can define an object here.
// Configuration options are here (https://github.com/nodeca/js-yaml#dump-object---options-).
// Example:
// {
// lineWidth: -1,
// }
}
+7 -3
View File
@@ -22,9 +22,10 @@
"@types/react-router": "5.1.18",
"@types/webpack": "^5.28.1",
"husky": "^8.0.3",
"i18next-parser": "^7.7.0",
"import-sort-style-module": "^6.0.0",
"inquirer": "^8.2.5",
"prettier": "^2.8.7",
"prettier": "^2.8.8",
"prettier-plugin-import-sort": "^0.0.7",
"react": "16.14.0",
"react-dom": "16.14.0",
@@ -42,10 +43,13 @@
}
},
"dependencies": {
"decky-frontend-lib": "^3.20.0",
"decky-frontend-lib": "3.20.7",
"i18next": "^22.4.15",
"i18next-http-backend": "^2.2.0",
"react-file-icon": "^1.3.0",
"react-i18next": "^12.2.2",
"react-icons": "^4.8.0",
"react-markdown": "^8.0.6",
"react-markdown": "^8.0.7",
"remark-gfm": "^3.0.1"
}
}
+1426 -231
View File
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -15,7 +15,9 @@ export default defineConfig({
plugins: [
del({ targets: '../backend/static/*', force: true }),
commonjs(),
nodeResolve(),
nodeResolve({
browser: true
}),
externalGlobals({
react: 'SP_REACT',
'react-dom': 'SP_REACTDOM',
@@ -44,4 +46,4 @@ export default defineConfig({
if (hiddenWarnings.some((warning) => message.code === warning)) return;
handleWarning(message);
},
});
});
@@ -1,18 +1,31 @@
import { ConfirmModal, Navigation, QuickAccessTab } from 'decky-frontend-lib';
import { FC, useState } from 'react';
import { useTranslation } from 'react-i18next';
import TPluginInstallModal, { TranslatedPart } from './TPluginInstallModal';
interface PluginInstallModalProps {
artifact: string;
version: string;
hash: string;
// reinstall: boolean;
installType: number;
onOK(): void;
onCancel(): void;
closeModal?(): void;
}
const PluginInstallModal: FC<PluginInstallModalProps> = ({ artifact, version, hash, onOK, onCancel, closeModal }) => {
const PluginInstallModal: FC<PluginInstallModalProps> = ({
artifact,
version,
hash,
installType,
onOK,
onCancel,
closeModal,
}) => {
const [loading, setLoading] = useState<boolean>(false);
const { t } = useTranslation();
return (
<ConfirmModal
bOKDisabled={loading}
@@ -26,14 +39,22 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({ artifact, version, ha
onCancel={async () => {
await onCancel();
}}
strTitle={`Install ${artifact}`}
strOKButtonText={loading ? 'Installing' : 'Install'}
strTitle={<TPluginInstallModal trans_part={TranslatedPart.TITLE} trans_type={installType} artifact={artifact} />}
strOKButtonText={
loading ? (
<TPluginInstallModal trans_part={TranslatedPart.BUTTON_PROC} trans_type={installType} />
) : (
<TPluginInstallModal trans_part={TranslatedPart.BUTTON_IDLE} trans_type={installType} />
)
}
>
{hash == 'False' ? (
<h3 style={{ color: 'red' }}>!!!!NO HASH PROVIDED!!!!</h3>
) : (
`Are you sure you want to install ${artifact} ${version}?`
)}
<TPluginInstallModal
trans_part={TranslatedPart.DESC}
trans_type={installType}
artifact={artifact}
version={version ? version : ''}
/>
{hash == 'False' && <span style={{ color: 'red' }}>{t('PluginInstallModal.no_hash')}</span>}
</ConfirmModal>
);
};
@@ -0,0 +1,95 @@
import { FC } from 'react';
import { Translation } from 'react-i18next';
import { InstallType } from '../../plugin';
export enum TranslatedPart {
TITLE,
DESC,
BUTTON_IDLE,
BUTTON_PROC,
}
interface TPluginInstallModalProps {
trans_part: TranslatedPart;
trans_type: number;
artifact?: string;
version?: string;
}
const TPluginInstallModal: FC<TPluginInstallModalProps> = ({ trans_part, trans_type, artifact, version }) => {
return (
<Translation>
{(t, {}) => {
switch (trans_part) {
case TranslatedPart.TITLE:
switch (trans_type) {
case InstallType.INSTALL:
return <div>{t('PluginInstallModal.install.title', { artifact: artifact })}</div>;
case InstallType.REINSTALL:
return <div>{t('PluginInstallModal.reinstall.title', { artifact: artifact })}</div>;
case InstallType.UPDATE:
return <div>{t('PluginInstallModal.update.title', { artifact: artifact })}</div>;
default:
return null;
}
case TranslatedPart.DESC:
switch (trans_type) {
case InstallType.INSTALL:
return (
<div>
{t('PluginInstallModal.install.desc', {
artifact: artifact,
version: version,
})}
</div>
);
case InstallType.REINSTALL:
return (
<div>
{t('PluginInstallModal.reinstall.desc', {
artifact: artifact,
version: version,
})}
</div>
);
case InstallType.UPDATE:
return (
<div>
{t('PluginInstallModal.update.desc', {
artifact: artifact,
version: version,
})}
</div>
);
default:
return null;
}
case TranslatedPart.BUTTON_IDLE:
switch (trans_type) {
case InstallType.INSTALL:
return <div>{t('PluginInstallModal.install.button_idle')}</div>;
case InstallType.REINSTALL:
return <div>{t('PluginInstallModal.reinstall.button_idle')}</div>;
case InstallType.UPDATE:
return <div>{t('PluginInstallModal.update.button_idle')}</div>;
default:
return null;
}
case TranslatedPart.BUTTON_PROC:
switch (trans_type) {
case InstallType.INSTALL:
return <div>{t('PluginInstallModal.install.button_processing')}</div>;
case InstallType.REINSTALL:
return <div>{t('PluginInstallModal.reinstall.button_processing')}</div>;
case InstallType.UPDATE:
return <div>{t('PluginInstallModal.update.button_processing')}</div>;
default:
return null;
}
}
}}
</Translation>
);
};
export default TPluginInstallModal;
@@ -2,6 +2,7 @@ import { DialogButton, Focusable, SteamSpinner, TextField } from 'decky-frontend
import { useEffect } from 'react';
import { FunctionComponent, useState } from 'react';
import { FileIcon, defaultStyles } from 'react-file-icon';
import { useTranslation } from 'react-i18next';
import { FaArrowUp, FaFolder } from 'react-icons/fa';
import Logger from '../../../logger';
@@ -47,6 +48,7 @@ const FilePicker: FunctionComponent<FilePickerProps> = ({
onSubmit,
closeModal,
}) => {
const { t } = useTranslation();
if (startPath.endsWith('/')) startPath = startPath.substring(0, startPath.length - 1); // remove trailing path
const [path, setPath] = useState<string>(startPath);
const [listing, setListing] = useState<FileListing>({ files: [], realpath: path });
@@ -134,7 +136,15 @@ const FilePicker: FunctionComponent<FilePickerProps> = ({
)}
</div>
)}
{file.name}
<span
style={{
textOverflow: 'ellipsis',
overflow: 'hidden',
textAlign: 'left',
}}
>
{file.name}
</span>
</div>
</DialogButton>
);
@@ -150,7 +160,7 @@ const FilePicker: FunctionComponent<FilePickerProps> = ({
closeModal?.();
}}
>
Use this folder
{t('FilePickerIndex.folder.select')}
</DialogButton>
)}
</div>
+5 -3
View File
@@ -1,5 +1,6 @@
import { SidebarNavigation } from 'decky-frontend-lib';
import { lazy } from 'react';
import { useTranslation } from 'react-i18next';
import { FaCode, FaPlug } from 'react-icons/fa';
import { useSetting } from '../../utils/hooks/useSetting';
@@ -12,22 +13,23 @@ const DeveloperSettings = lazy(() => import('./pages/developer'));
export default function SettingsPage() {
const [isDeveloper, setIsDeveloper] = useSetting<boolean>('developer.enabled', false);
const { t } = useTranslation();
const pages = [
{
title: 'Decky',
title: t('SettingsIndex.general_title'),
content: <GeneralSettings isDeveloper={isDeveloper} setIsDeveloper={setIsDeveloper} />,
route: '/decky/settings/general',
icon: <DeckyIcon />,
},
{
title: 'Plugins',
title: t('SettingsIndex.plugins_title'),
content: <PluginList />,
route: '/decky/settings/plugins',
icon: <FaPlug />,
},
{
title: 'Developer',
title: t('SettingsIndex.developer_title'),
content: (
<WithSuspense>
<DeveloperSettings />
@@ -1,64 +1,127 @@
import { DialogBody, Field, TextField, Toggle } from 'decky-frontend-lib';
import { useRef } from 'react';
import { FaReact, FaSteamSymbol } from 'react-icons/fa';
import {
DialogBody,
DialogButton,
DialogControlsSection,
DialogControlsSectionHeader,
Field,
TextField,
Toggle,
} from 'decky-frontend-lib';
import { useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaFileArchive, FaLink, FaReact, FaSteamSymbol } from 'react-icons/fa';
import { setShouldConnectToReactDevTools, setShowValveInternal } from '../../../../developer';
import { installFromURL } from '../../../../store';
import { useSetting } from '../../../../utils/hooks/useSetting';
import RemoteDebuggingSettings from '../general/RemoteDebugging';
const installFromZip = () => {
window.DeckyPluginLoader.openFilePicker('/home/deck', true).then((val) => {
const url = `file://${val.path}`;
console.log(`Installing plugin locally from ${url}`);
if (url.endsWith('.zip')) {
installFromURL(url);
} else {
window.DeckyPluginLoader.toaster.toast({
//title: t('SettingsDeveloperIndex.toast_zip.title'),
title: 'Decky',
//body: t('SettingsDeveloperIndex.toast_zip.body'),
body: 'Installation failed! Only ZIP files are supported.',
onClick: installFromZip,
});
}
});
};
export default function DeveloperSettings() {
const [enableValveInternal, setEnableValveInternal] = useSetting<boolean>('developer.valve_internal', false);
const [reactDevtoolsEnabled, setReactDevtoolsEnabled] = useSetting<boolean>('developer.rdt.enabled', false);
const [reactDevtoolsIP, setReactDevtoolsIP] = useSetting<string>('developer.rdt.ip', '');
const [pluginURL, setPluginURL] = useState('');
const textRef = useRef<HTMLDivElement>(null);
const { t } = useTranslation();
return (
<DialogBody>
<RemoteDebuggingSettings />
<Field
label="Enable Valve Internal"
description={
<span style={{ whiteSpace: 'pre-line' }}>
Enables the Valve internal developer menu.{' '}
<span style={{ color: 'red' }}>Do not touch anything in this menu unless you know what it does.</span>
</span>
}
icon={<FaSteamSymbol style={{ display: 'block' }} />}
>
<Toggle
value={enableValveInternal}
onChange={(toggleValue) => {
setEnableValveInternal(toggleValue);
setShowValveInternal(toggleValue);
}}
/>
</Field>
<Field
label="Enable React DevTools"
description={
<>
<DialogControlsSection>
<DialogControlsSectionHeader>
{t('SettingsDeveloperIndex.third_party_plugins.header')}
</DialogControlsSectionHeader>
<Field
label={t('SettingsDeveloperIndex.third_party_plugins.label_zip')}
icon={<FaFileArchive style={{ display: 'block' }} />}
>
<DialogButton onClick={installFromZip}>
{t('SettingsDeveloperIndex.third_party_plugins.button_zip')}
</DialogButton>
</Field>
<Field
label={t('SettingsDeveloperIndex.third_party_plugins.label_url')}
description={
<TextField
label={t('SettingsDeveloperIndex.third_party_plugins.label_desc')}
value={pluginURL}
onChange={(e) => setPluginURL(e?.target.value)}
/>
}
icon={<FaLink style={{ display: 'block' }} />}
>
<DialogButton disabled={pluginURL.length == 0} onClick={() => installFromURL(pluginURL)}>
{t('SettingsDeveloperIndex.third_party_plugins.button_install')}
</DialogButton>
</Field>
</DialogControlsSection>
<DialogControlsSection>
<DialogControlsSectionHeader>{t('SettingsDeveloperIndex.header_other')}</DialogControlsSectionHeader>
<RemoteDebuggingSettings />
<Field
label={t('SettingsDeveloperIndex.valve_internal.label')}
description={
<span style={{ whiteSpace: 'pre-line' }}>
Enables connection to a computer running React DevTools. Changing this setting will reload Steam. Set the
IP address before enabling.
{t('SettingsDeveloperIndex.valve_internal.desc1')}{' '}
<span style={{ color: 'red' }}>{t('SettingsDeveloperIndex.valve_internal.desc2')}</span>
</span>
<br />
<br />
<div ref={textRef}>
<TextField label={'IP'} value={reactDevtoolsIP} onChange={(e) => setReactDevtoolsIP(e?.target.value)} />
</div>
</>
}
icon={<FaReact style={{ display: 'block' }} />}
>
<Toggle
value={reactDevtoolsEnabled}
// disabled={reactDevtoolsIP == ''}
onChange={(toggleValue) => {
setReactDevtoolsEnabled(toggleValue);
setShouldConnectToReactDevTools(toggleValue);
}}
/>
</Field>
}
icon={<FaSteamSymbol style={{ display: 'block' }} />}
>
<Toggle
value={enableValveInternal}
onChange={(toggleValue) => {
setEnableValveInternal(toggleValue);
setShowValveInternal(toggleValue);
}}
/>
</Field>
<Field
label={t('SettingsDeveloperIndex.react_devtools.label')}
description={
<>
<span style={{ whiteSpace: 'pre-line' }}>{t('SettingsDeveloperIndex.react_devtools.desc')}</span>
<br />
<br />
<div ref={textRef}>
<TextField
label={t('SettingsDeveloperIndex.react_devtools.ip_label')}
value={reactDevtoolsIP}
onChange={(e) => setReactDevtoolsIP(e?.target.value)}
/>
</div>
</>
}
icon={<FaReact style={{ display: 'block' }} />}
>
<Toggle
value={reactDevtoolsEnabled}
// disabled={reactDevtoolsIP == ''}
onChange={(toggleValue) => {
setReactDevtoolsEnabled(toggleValue);
setShouldConnectToReactDevTools(toggleValue);
}}
/>
</Field>
</DialogControlsSection>
</DialogBody>
);
}
@@ -1,5 +1,6 @@
import { Dropdown, Field } from 'decky-frontend-lib';
import { FunctionComponent } from 'react';
import { useTranslation } from 'react-i18next';
import Logger from '../../../../logger';
import { callUpdaterMethod } from '../../../../updater';
@@ -14,17 +15,23 @@ enum UpdateBranch {
}
const BranchSelect: FunctionComponent<{}> = () => {
const { t } = useTranslation();
const tBranches = [
t('BranchSelect.update_channel.stable'),
t('BranchSelect.update_channel.prerelease'),
t('BranchSelect.update_channel.testing'),
];
const [selectedBranch, setSelectedBranch] = useSetting<UpdateBranch>('branch', UpdateBranch.Prerelease);
return (
// Returns numerical values from 0 to 2 (with current branch setup as of 8/28/22)
// 0 being stable, 1 being pre-release and 2 being nightly
<Field label="Decky Update Channel" childrenContainerWidth={'fixed'}>
<Field label={t('BranchSelect.update_channel.label')} childrenContainerWidth={'fixed'}>
<Dropdown
rgOptions={Object.values(UpdateBranch)
.filter((branch) => typeof branch == 'string')
.map((branch) => ({
label: branch,
label: tBranches[UpdateBranch[branch]],
data: UpdateBranch[branch],
}))}
selectedOption={selectedBranch}
@@ -1,19 +1,17 @@
import { Field, Toggle } from 'decky-frontend-lib';
import { useTranslation } from 'react-i18next';
import { FaChrome } from 'react-icons/fa';
import { useSetting } from '../../../../utils/hooks/useSetting';
export default function RemoteDebuggingSettings() {
const [allowRemoteDebugging, setAllowRemoteDebugging] = useSetting<boolean>('cef_forward', false);
const { t } = useTranslation();
return (
<Field
label="Allow Remote CEF Debugging"
description={
<span style={{ whiteSpace: 'pre-line' }}>
Allows unauthenticated access to the CEF debugger to anyone in your network.
</span>
}
label={t('RemoteDebugging.remote_cef.label')}
description={<span style={{ whiteSpace: 'pre-line' }}>{t('RemoteDebugging.remote_cef.desc')}</span>}
icon={<FaChrome style={{ display: 'block' }} />}
>
<Toggle
@@ -1,5 +1,6 @@
import { Dropdown, Field, TextField } from 'decky-frontend-lib';
import { FunctionComponent } from 'react';
import { useTranslation } from 'react-i18next';
import { FaShapes } from 'react-icons/fa';
import Logger from '../../../../logger';
@@ -11,17 +12,23 @@ const logger = new Logger('StoreSelect');
const StoreSelect: FunctionComponent<{}> = () => {
const [selectedStore, setSelectedStore] = useSetting<Store>('store', Store.Default);
const [selectedStoreURL, setSelectedStoreURL] = useSetting<string | null>('store-url', null);
const { t } = useTranslation();
const tStores = [
t('StoreSelect.store_channel.default'),
t('StoreSelect.store_channel.testing'),
t('StoreSelect.store_channel.custom'),
];
// Returns numerical values from 0 to 2 (with current branch setup as of 8/28/22)
// 0 being Default, 1 being Testing and 2 being Custom
return (
<>
<Field label="Plugin Store Channel" childrenContainerWidth={'fixed'}>
<Field label={t('StoreSelect.store_channel.label')} childrenContainerWidth={'fixed'}>
<Dropdown
rgOptions={Object.values(Store)
.filter((store) => typeof store == 'string')
.map((store) => ({
label: store,
label: tStores[Store[store]],
data: Store[store],
}))}
selectedOption={selectedStore}
@@ -33,11 +40,11 @@ const StoreSelect: FunctionComponent<{}> = () => {
</Field>
{selectedStore == Store.Custom && (
<Field
label="Custom Store"
label={t('StoreSelect.custom_store.label')}
indentLevel={1}
description={
<TextField
label={'URL'}
label={t('StoreSelect.custom_store.url_label')}
value={selectedStoreURL || undefined}
onChange={(e) => setSelectedStoreURL(e?.target.value || null)}
/>
@@ -12,6 +12,7 @@ import {
import { useCallback } from 'react';
import { Suspense, lazy } from 'react';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaExclamation } from 'react-icons/fa';
import { VerInfo, callUpdaterMethod, finishUpdate } from '../../../../updater';
@@ -23,6 +24,7 @@ const MarkdownRenderer = lazy(() => import('../../../Markdown'));
function PatchNotesModal({ versionInfo, closeModal }: { versionInfo: VerInfo | null; closeModal?: () => {} }) {
const SP = findSP();
const { t } = useTranslation();
return (
<Focusable onCancelButton={closeModal}>
<FocusRing>
@@ -39,13 +41,13 @@ function PatchNotesModal({ versionInfo, closeModal }: { versionInfo: VerInfo | n
}}
>
<div>
<h1>{versionInfo?.all?.[id]?.name}</h1>
<h1>{versionInfo?.all?.[id]?.name || 'Invalid Update Name'}</h1>
{versionInfo?.all?.[id]?.body ? (
<WithSuspense>
<MarkdownRenderer onDismiss={closeModal}>{versionInfo.all[id].body}</MarkdownRenderer>
</WithSuspense>
) : (
'no patch notes for this version'
t('Updater.no_patch_notes_desc')
)}
</div>
</Focusable>
@@ -58,7 +60,7 @@ function PatchNotesModal({ versionInfo, closeModal }: { versionInfo: VerInfo | n
initialColumn={0}
autoFocus={true}
fnGetColumnWidth={() => SP.innerWidth}
name="Decky Updates"
name={t('Updater.decky_updates') as string}
/>
</FocusRing>
</Focusable>
@@ -72,6 +74,8 @@ export default function UpdaterSettings() {
const [updateProgress, setUpdateProgress] = useState<number>(-1);
const [reloading, setReloading] = useState<boolean>(false);
const { t } = useTranslation();
useEffect(() => {
window.DeckyUpdater = {
updateProgress: (i) => {
@@ -93,14 +97,14 @@ export default function UpdaterSettings() {
return (
<>
<Field
onOptionsActionDescription={versionInfo?.all ? 'Patch Notes' : undefined}
onOptionsActionDescription={versionInfo?.all ? t('Updater.patch_notes_desc') : undefined}
onOptionsButton={versionInfo?.all ? showPatchNotes : undefined}
label="Decky Updates"
label={t('Updater.updates.label')}
description={
checkingForUpdates || versionInfo?.remote?.tag_name != versionInfo?.current || !versionInfo?.remote ? (
''
) : (
<span>Up to date: running {versionInfo?.current}</span>
<span>{t('Updater.updates.lat_version', { ver: versionInfo?.current })} </span>
)
}
icon={
@@ -129,10 +133,10 @@ export default function UpdaterSettings() {
}
>
{checkingForUpdates
? 'Checking'
? t('Updater.updates.checking')
: !versionInfo?.remote || versionInfo?.remote?.tag_name == versionInfo?.current
? 'Check For Updates'
: 'Install Update'}
? t('Updater.updates.check_button')
: t('Updater.updates.install_button')}
</DialogButton>
) : (
<ProgressBarWithInfo
@@ -140,7 +144,7 @@ export default function UpdaterSettings() {
bottomSeparator="none"
nProgress={updateProgress}
indeterminate={reloading}
sOperationText={reloading ? 'Reloading' : 'Updating'}
sOperationText={reloading ? t('Updater.updates.reloading') : t('Updater.updates.updating')}
/>
)}
</Field>
@@ -1,15 +1,6 @@
import {
DialogBody,
DialogButton,
DialogControlsSection,
DialogControlsSectionHeader,
Field,
TextField,
Toggle,
} from 'decky-frontend-lib';
import { useState } from 'react';
import { DialogBody, DialogControlsSection, DialogControlsSectionHeader, Field, Toggle } from 'decky-frontend-lib';
import { useTranslation } from 'react-i18next';
import { installFromURL } from '../../../../store';
import { useDeckyState } from '../../../DeckyState';
import BranchSelect from './BranchSelect';
import StoreSelect from './StoreSelect';
@@ -22,23 +13,23 @@ export default function GeneralSettings({
isDeveloper: boolean;
setIsDeveloper: (val: boolean) => void;
}) {
const [pluginURL, setPluginURL] = useState('');
const { versionInfo } = useDeckyState();
const { t } = useTranslation();
return (
<DialogBody>
<DialogControlsSection>
<DialogControlsSectionHeader>Updates</DialogControlsSectionHeader>
<DialogControlsSectionHeader>{t('SettingsGeneralIndex.updates.header')}</DialogControlsSectionHeader>
<UpdaterSettings />
</DialogControlsSection>
<DialogControlsSection>
<DialogControlsSectionHeader>Beta Participation</DialogControlsSectionHeader>
<DialogControlsSectionHeader>{t('SettingsGeneralIndex.beta.header')}</DialogControlsSectionHeader>
<BranchSelect />
<StoreSelect />
</DialogControlsSection>
<DialogControlsSection>
<DialogControlsSectionHeader>Other</DialogControlsSectionHeader>
<Field label="Enable Developer Mode">
<DialogControlsSectionHeader>{t('SettingsGeneralIndex.other.header')}</DialogControlsSectionHeader>
<Field label={t('SettingsGeneralIndex.developer_mode.label')}>
<Toggle
value={isDeveloper}
onChange={(toggleValue) => {
@@ -46,18 +37,10 @@ export default function GeneralSettings({
}}
/>
</Field>
<Field
label="Install plugin from URL"
description={<TextField label={'URL'} value={pluginURL} onChange={(e) => setPluginURL(e?.target.value)} />}
>
<DialogButton disabled={pluginURL.length == 0} onClick={() => installFromURL(pluginURL)}>
Install
</DialogButton>
</Field>
</DialogControlsSection>
<DialogControlsSection>
<DialogControlsSectionHeader>About</DialogControlsSectionHeader>
<Field label="Decky Version" focusable={true}>
<DialogControlsSectionHeader>{t('SettingsGeneralIndex.about.header')}</DialogControlsSectionHeader>
<Field label={t('SettingsGeneralIndex.about.decky_version')} focusable={true}>
<div style={{ color: 'var(--gpSystemLighterGrey)' }}>{versionInfo?.current}</div>
</Field>
</DialogControlsSection>
@@ -10,22 +10,50 @@ import {
showContextMenu,
} from 'decky-frontend-lib';
import { useEffect, useState } from 'react';
import { FaDownload, FaEllipsisH } from 'react-icons/fa';
import { useTranslation } from 'react-i18next';
import { FaDownload, FaEllipsisH, FaRecycle } from 'react-icons/fa';
import { StorePluginVersion, requestPluginInstall } from '../../../../store';
import { InstallType } from '../../../../plugin';
import { StorePluginVersion, getPluginList, requestPluginInstall } from '../../../../store';
import { useSetting } from '../../../../utils/hooks/useSetting';
import { useDeckyState } from '../../../DeckyState';
function labelToName(pluginLabel: string, pluginVersion?: string): string {
return pluginVersion ? pluginLabel.substring(0, pluginLabel.indexOf(` - ${pluginVersion}`)) : pluginLabel;
}
async function reinstallPlugin(pluginName: string, currentVersion?: string) {
const serverData = await getPluginList();
const remotePlugin = serverData?.find((x) => x.name == pluginName);
if (remotePlugin && remotePlugin.versions?.length > 0) {
const currentVersionData = remotePlugin.versions.find((version) => version.name == currentVersion);
if (currentVersionData) requestPluginInstall(pluginName, currentVersionData, InstallType.REINSTALL);
}
}
function PluginInteractables(props: { entry: ReorderableEntry<PluginData> }) {
const data = props.entry.data;
const { t } = useTranslation();
let pluginName = labelToName(props.entry.label, data?.version);
const showCtxMenu = (e: MouseEvent | GamepadEvent) => {
showContextMenu(
<Menu label="Plugin Actions">
<MenuItem onSelected={() => window.DeckyPluginLoader.importPlugin(props.entry.label, data?.version)}>
Reload
<Menu label={t('PluginListIndex.plugin_actions')}>
<MenuItem onSelected={() => window.DeckyPluginLoader.importPlugin(pluginName, data?.version)}>
{t('PluginListIndex.reload')}
</MenuItem>
<MenuItem
onSelected={() =>
window.DeckyPluginLoader.uninstallPlugin(
pluginName,
t('PluginLoader.plugin_uninstall.title', { name: pluginName }),
t('PluginLoader.plugin_uninstall.button'),
t('PluginLoader.plugin_uninstall.desc', { name: pluginName }),
)
}
>
{t('PluginListIndex.uninstall')}
</MenuItem>
<MenuItem onSelected={() => window.DeckyPluginLoader.uninstallPlugin(props.entry.label)}>Uninstall</MenuItem>
</Menu>,
e.currentTarget ?? window,
);
@@ -33,17 +61,28 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginData> }) {
return (
<>
{data?.update && (
{data?.update ? (
<DialogButton
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
onClick={() => requestPluginInstall(props.entry.label, data?.update as StorePluginVersion)}
onOKButton={() => requestPluginInstall(props.entry.label, data?.update as StorePluginVersion)}
onClick={() => requestPluginInstall(pluginName, data?.update as StorePluginVersion, InstallType.UPDATE)}
onOKButton={() => requestPluginInstall(pluginName, data?.update as StorePluginVersion, InstallType.UPDATE)}
>
<div style={{ display: 'flex', flexDirection: 'row' }}>
Update to {data?.update?.name}
{t('PluginListIndex.update_to', { name: data?.update?.name })}
<FaDownload style={{ paddingLeft: '2rem' }} />
</div>
</DialogButton>
) : (
<DialogButton
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
onClick={() => reinstallPlugin(pluginName, data?.version)}
onOKButton={() => reinstallPlugin(pluginName, data?.version)}
>
<div style={{ display: 'flex', flexDirection: 'row' }}>
{t('PluginListIndex.reinstall')}
<FaRecycle style={{ paddingLeft: '5.3rem' }} />
</div>
</DialogButton>
)}
<DialogButton
style={{ height: '40px', width: '40px', padding: '10px 12px', minWidth: '40px' }}
@@ -67,6 +106,7 @@ export default function PluginList() {
'pluginOrder',
plugins.map((plugin) => plugin.name),
);
const { t } = useTranslation();
useEffect(() => {
window.DeckyPluginLoader.checkPluginUpdates();
@@ -78,7 +118,7 @@ export default function PluginList() {
setPluginEntries(
plugins.map((plugin) => {
return {
label: plugin.name,
label: plugin.version ? `${plugin.name} - ${plugin.version}` : plugin.name,
data: {
update: updates?.get(plugin.name),
version: plugin.version,
@@ -92,13 +132,13 @@ export default function PluginList() {
if (plugins.length === 0) {
return (
<div>
<p>No plugins installed</p>
<p>{t('PluginListIndex.no_plugin')}</p>
</div>
);
}
function onSave(entries: ReorderableEntry<PluginData>[]) {
const newOrder = entries.map((entry) => entry.label);
const newOrder = entries.map((entry) => labelToName(entry.label, entry?.data?.version));
console.log(newOrder);
setPluginOrder(newOrder);
setPluginOrderSetting(newOrder);
+11 -5
View File
@@ -7,7 +7,9 @@ import {
SuspensefulImage,
} from 'decky-frontend-lib';
import { FC, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { InstallType } from '../../plugin';
import { StorePlugin, StorePluginVersion, requestPluginInstall } from '../../store';
interface PluginCardProps {
@@ -18,6 +20,8 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
const [selectedOption, setSelectedOption] = useState<number>(0);
const root: boolean = plugin.tags.some((tag) => tag === 'root');
const { t } = useTranslation();
return (
<div
className="deckyStoreCard"
@@ -97,7 +101,7 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
plugin.description
) : (
<span>
<i style={{ color: '#666' }}>No description provided.</i>
<i style={{ color: '#666' }}>{t('PluginCard.plugin_no_desc')}</i>
</span>
)}
</span>
@@ -109,7 +113,7 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
color: '#fee75c',
}}
>
<i>This plugin has full access to your Steam Deck.</i>{' '}
<i>{t('PluginCard.plugin_full_access')}</i>{' '}
<a
className="deckyStoreCardDescriptionRootLink"
href="https://deckbrew.xyz/root"
@@ -144,9 +148,11 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
<ButtonItem
bottomSeparator="none"
layout="below"
onClick={() => requestPluginInstall(plugin.name, plugin.versions[selectedOption])}
onClick={() =>
requestPluginInstall(plugin.name, plugin.versions[selectedOption], InstallType.INSTALL)
}
>
<span className="deckyStoreCardInstallText">Install</span>
<span className="deckyStoreCardInstallText">{t('PluginCard.plugin_install')}</span>
</ButtonItem>
</div>
<div
@@ -163,7 +169,7 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
label: version.name,
})) as SingleDropdownOption[]
}
menuLabel="Plugin Version"
menuLabel={t('PluginCard.plugin_version_label') as string}
selectedOption={selectedOption}
onChange={({ data }) => setSelectedOption(data)}
/>
+31 -23
View File
@@ -9,6 +9,7 @@ import {
findModule,
} from 'decky-frontend-lib';
import { FC, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import logo from '../../../assets/plugin_store.png';
import Logger from '../../logger';
@@ -25,6 +26,8 @@ const StorePage: FC<{}> = () => {
return false;
});
const { t } = useTranslation();
useEffect(() => {
(async () => {
const res = await getPluginList();
@@ -54,13 +57,13 @@ const StorePage: FC<{}> = () => {
}}
tabs={[
{
title: 'Browse',
title: t('Store.store_tabs.title'),
content: <BrowseTab children={{ data: data }} />,
id: 'browse',
renderTabAddon: () => <span className={TabCount}>{data.length}</span>,
},
{
title: 'About',
title: t('Store.store_tabs.about'),
content: <AboutTab />,
id: 'about',
},
@@ -73,10 +76,12 @@ const StorePage: FC<{}> = () => {
};
const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
const { t } = useTranslation();
const sortOptions = useMemo(
(): DropdownOption[] => [
{ data: 1, label: 'Alphabetical (A to Z)' },
{ data: 2, label: 'Alphabetical (Z to A)' },
{ data: 1, label: t('Store.store_tabs.alph_desc') },
{ data: 2, label: t('Store.store_tabs.alph_asce') },
],
[],
);
@@ -105,11 +110,11 @@ const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
width: '47.5%',
}}
>
<span className="DialogLabel">Sort</span>
<span className="DialogLabel">{t("Store.store_sort.label")}</span>
<Dropdown
menuLabel="Sort"
menuLabel={t("Store.store_sort.label") as string}
rgOptions={sortOptions}
strDefaultLabel="Last Updated (Newest)"
strDefaultLabel={t("Store.store_sort.label_def") as string}
selectedOption={selectedSort}
onChange={(e) => setSort(e.data)}
/>
@@ -122,11 +127,11 @@ const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
marginLeft: 'auto',
}}
>
<span className="DialogLabel">Filter</span>
<span className="DialogLabel">{t("Store.store_filter.label")}</span>
<Dropdown
menuLabel="Filter"
menuLabel={t("Store.store_filter.label")}
rgOptions={filterOptions}
strDefaultLabel="All"
strDefaultLabel={t("Store.store_filter.label_def")}
selectedOption={selectedFilter}
onChange={(e) => setFilter(e.data)}
/>
@@ -136,7 +141,7 @@ const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
<div style={{ justifyContent: 'center', display: 'flex' }}>
<Focusable style={{ display: 'flex', alignItems: 'center', width: '96%' }}>
<div style={{ width: '100%' }}>
<TextField label="Search" value={searchFieldValue} onChange={(e) => setSearchValue(e.target.value)} />
<TextField label={t("Store.store_search.label")} value={searchFieldValue} onChange={(e) => setSearchValue(e.target.value)} />
</div>
</Focusable>
</div>
@@ -151,11 +156,11 @@ const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
maxWidth: '100%',
}}
>
<span className="DialogLabel">Sort</span>
<span className="DialogLabel">{t('Store.store_sort.label')}</span>
<Dropdown
menuLabel="Sort"
menuLabel={t('Store.store_sort.label') as string}
rgOptions={sortOptions}
strDefaultLabel="Last Updated (Newest)"
strDefaultLabel={t('Store.store_sort.label_def') as string}
selectedOption={selectedSort}
onChange={(e) => setSort(e.data)}
/>
@@ -165,7 +170,11 @@ const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
<div style={{ justifyContent: 'center', display: 'flex' }}>
<Focusable style={{ display: 'flex', alignItems: 'center', width: '96%' }}>
<div style={{ width: '100%' }}>
<TextField label="Search" value={searchFieldValue} onChange={(e) => setSearchValue(e.target.value)} />
<TextField
label={t('Store.store_search.label')}
value={searchFieldValue}
onChange={(e) => setSearchValue(e.target.value)}
/>
</div>
</Focusable>
</div>
@@ -192,6 +201,8 @@ const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
};
const AboutTab: FC<{}> = () => {
const { t } = useTranslation();
return (
<div
style={{
@@ -216,7 +227,7 @@ const AboutTab: FC<{}> = () => {
/>
<span className="deckyStoreAboutHeader">Testing</span>
<span>
Please consider testing new plugins to help the Decky Loader team!{' '}
{t('Store.store_testing_cta')}{' '}
<a
href="https://deckbrew.xyz/testing"
target="_blank"
@@ -227,13 +238,10 @@ const AboutTab: FC<{}> = () => {
deckbrew.xyz/testing
</a>
</span>
<span className="deckyStoreAboutHeader">Contributing</span>
<span>
If you would like to contribute to the Decky Plugin Store, check the SteamDeckHomebrew/decky-plugin-template
repository on GitHub. Information on development and distribution is available in the README.
</span>
<span className="deckyStoreAboutHeader">Source Code</span>
<span>All plugin source code is available on SteamDeckHomebrew/decky-plugin-database repository on GitHub.</span>
<span className="deckyStoreAboutHeader">{t('Store.store_contrib.label')}</span>
<span>{t('Store.store_contrib.desc')}</span>
<span className="deckyStoreAboutHeader">{t('Store.store_source.label')}</span>
<span>{t('Store.store_source.desc')}</span>
</div>
);
};
+5 -2
View File
@@ -18,6 +18,7 @@ import {
staticClasses,
updaterFieldClasses,
} from 'decky-frontend-lib';
import { useTranslation } from 'react-i18next';
import { FaReact } from 'react-icons/fa';
import Logger from './logger';
@@ -58,9 +59,11 @@ export async function setShowValveInternal(show: boolean) {
}
export async function setShouldConnectToReactDevTools(enable: boolean) {
const { t } = useTranslation();
window.DeckyPluginLoader.toaster.toast({
title: (enable ? 'Enabling' : 'Disabling') + ' React DevTools',
body: 'Reloading in 5 seconds',
title: (enable ? t('Developer.enabling') : t('Developer.disabling')) + ' React DevTools',
body: t('Developer.5secreload'),
icon: <FaReact />,
});
await sleep(5000);
+32 -1
View File
@@ -1,4 +1,7 @@
import { Navigation, Router, sleep } from 'decky-frontend-lib';
import i18n from 'i18next';
import Backend from 'i18next-http-backend';
import { initReactI18next } from 'react-i18next';
import PluginLoader from './plugin-loader';
import { DeckyUpdater } from './updater';
@@ -36,9 +39,35 @@ declare global {
(async () => {
window.deckyAuthToken = await fetch('http://127.0.0.1:1337/auth/token').then((r) => r.text());
i18n
.use(Backend)
.use(initReactI18next)
.init({
load: 'currentOnly',
detection: {
order: ['querystring', 'navigator'],
lookupQuerystring: 'lng',
},
//debug: true,
lng: navigator.language,
fallbackLng: 'en-US',
interpolation: {
escapeValue: true,
},
returnEmptyString: false,
backend: {
loadPath: 'http://127.0.0.1:1337/locales/{{lng}}.json',
customHeaders: {
Authentication: window.deckyAuthToken,
},
requestOptions: {
credentials: 'include',
},
},
});
window.DeckyPluginLoader?.dismountAll();
window.DeckyPluginLoader?.deinit();
window.DeckyPluginLoader = new PluginLoader();
window.DeckyPluginLoader.init();
window.importDeckyPlugin = function (name: string, version: string) {
@@ -62,3 +91,5 @@ declare global {
setTimeout(() => window.syncDeckyPlugins(), 5000);
})();
export default i18n;
+32 -6
View File
@@ -98,7 +98,9 @@ class PluginLoader extends Logger {
const versionInfo = await this.updateVersion();
if (versionInfo?.remote && versionInfo?.remote?.tag_name != versionInfo?.current) {
this.toaster.toast({
//title: t('PluginLoader.decky_title'),
title: 'Decky',
//body: t('PluginLoader.decky_update_available', { tag_name: versionInfo?.remote?.tag_name }),
body: `Update to ${versionInfo?.remote?.tag_name} available!`,
onClick: () => Router.Navigate('/decky/settings'),
});
@@ -118,26 +120,35 @@ class PluginLoader extends Logger {
const updates = await this.checkPluginUpdates();
if (updates?.size > 0) {
this.toaster.toast({
//title: t('PluginLoader.decky_title'),
title: 'Decky',
//body: t('PluginLoader.plugin_update', { count: updates.size }),
body: `Updates available for ${updates.size} plugin${updates.size > 1 ? 's' : ''}!`,
onClick: () => Router.Navigate('/decky/settings/plugins'),
});
}
}
public addPluginInstallPrompt(artifact: string, version: string, request_id: string, hash: string) {
public addPluginInstallPrompt(
artifact: string,
version: string,
request_id: string,
hash: string,
install_type: number,
) {
showModal(
<PluginInstallModal
artifact={artifact}
version={version}
hash={hash}
installType={install_type}
onOK={() => this.callServerMethod('confirm_plugin_install', { request_id })}
onCancel={() => this.callServerMethod('cancel_plugin_install', { request_id })}
/>,
);
}
public uninstallPlugin(name: string) {
public uninstallPlugin(name: string, title: string, button_text: string, description: string) {
showModal(
<ConfirmModal
onOK={async () => {
@@ -146,10 +157,10 @@ class PluginLoader extends Logger {
onCancel={() => {
// do nothing
}}
strTitle={`Uninstall ${name}`}
strOKButtonText={'Uninstall'}
strTitle={title}
strOKButtonText={button_text}
>
Are you sure you want to uninstall {name}?
{description}
</ConfirmModal>,
);
}
@@ -242,7 +253,17 @@ class PluginLoader extends Logger {
version: version,
});
} catch (e) {
//this.error(t('PluginLoader.plugin_load_error.message', { name: name }), e);
this.error('Error loading plugin ' + name, e);
/*const TheError: FC<{}> = () => (
<>
{t('PluginLoader.error')}:{' '}
<pre>
<code>{e instanceof Error ? e.stack : JSON.stringify(e)}</code>
</pre>
<>{t('PluginLoader.plugin_error_uninstall', { icon: "<FaCog style={{ display: 'inline' }} />" })}</>
</>
);*/
const TheError: FC<{}> = () => (
<>
Error:{' '}
@@ -261,7 +282,12 @@ class PluginLoader extends Logger {
content: <TheError />,
icon: <FaExclamationCircle />,
});
this.toaster.toast({ title: 'Error loading ' + name, body: '' + e, icon: <FaExclamationCircle /> });
this.toaster.toast({
//title: t('PluginLoader.plugin_load_error.toast', { name: name }),
title: 'Error loading ' + name,
body: '' + e,
icon: <FaExclamationCircle />,
});
}
} else throw new Error(`${name} frontend_bundle not OK`);
}
+6
View File
@@ -6,3 +6,9 @@ export interface Plugin {
onDismount?(): void;
alwaysRender?: boolean;
}
export enum InstallType {
INSTALL,
REINSTALL,
UPDATE,
}
+3 -2
View File
@@ -1,4 +1,4 @@
import { Plugin } from './plugin';
import { InstallType, Plugin } from './plugin';
import { getSetting, setSetting } from './utils/settings';
export enum Store {
@@ -73,7 +73,7 @@ export async function installFromURL(url: string) {
});
}
export async function requestPluginInstall(plugin: string, selectedVer: StorePluginVersion) {
export async function requestPluginInstall(plugin: string, selectedVer: StorePluginVersion, installType: InstallType) {
const artifactUrl =
selectedVer.artifact ?? `https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/versions/${selectedVer.hash}.zip`;
await window.DeckyPluginLoader.callServerMethod('install_plugin', {
@@ -81,6 +81,7 @@ export async function requestPluginInstall(plugin: string, selectedVer: StorePlu
artifact: artifactUrl,
version: selectedVer.name,
hash: selectedVer.hash,
install_type: installType,
});
}
+2 -1
View File
@@ -16,7 +16,8 @@
"strict": true,
"suppressImplicitAnyIndexErrors": true,
"allowSyntheticDefaultImports": true,
"skipLibCheck": true
"skipLibCheck": true,
"resolveJsonModule": true
},
"include": ["src", "index.d.ts"],
"exclude": ["node_modules"]