[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>
This commit is contained in:
Marco Rodolfi
2023-05-02 17:42:39 +02:00
committed by GitHub
parent caf37d681f
commit 35e7c80835
30 changed files with 2289 additions and 339 deletions
+1 -1
View File
@@ -69,7 +69,7 @@ jobs:
run: pnpm run build run: pnpm run build
- name: Build Python Backend 🛠️ - 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 ⬆️ - name: Upload package artifact ⬆️
if: ${{ !env.ACT }} 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'", "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": [] "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 // BUILD
{ {
"label": "pnpmsetup", "label": "pnpmsetup",
+2 -2
View File
@@ -209,12 +209,12 @@ class PluginBrowser:
if self.loader.watcher: if self.loader.watcher:
self.loader.watcher.disabled = False self.loader.watcher.disabled = False
async def request_plugin_install(self, artifact, name, version, hash): async def request_plugin_install(self, artifact, name, version, hash, install_type):
request_id = str(time()) request_id = str(time())
self.install_requests[request_id] = PluginInstallContext(artifact, name, version, hash) self.install_requests[request_id] = PluginInstallContext(artifact, name, version, hash)
tab = await get_gamepadui_tab() tab = await get_gamepadui_tab()
await tab.open_websocket() 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): async def confirm_plugin_install(self, request_id):
request = self.install_requests.pop(request_id) request = self.install_requests.pop(request_id)
+6
View File
@@ -77,6 +77,7 @@ class Loader:
server_instance.add_routes([ server_instance.add_routes([
web.get("/frontend/{path:.*}", self.handle_frontend_assets), 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", self.get_plugins),
web.get("/plugins/{plugin_name}/frontend_bundle", self.handle_frontend_bundle), web.get("/plugins/{plugin_name}/frontend_bundle", self.handle_frontend_bundle),
web.post("/plugins/{plugin_name}/methods/{method_name}", self.handle_plugin_method_call), web.post("/plugins/{plugin_name}/methods/{method_name}", self.handle_plugin_method_call),
@@ -99,6 +100,11 @@ class Loader:
return web.FileResponse(file, headers={"Cache-Control": "no-cache"}) return web.FileResponse(file, headers={"Cache-Control": "no-cache"})
async def handle_frontend_locales(self, request):
file = path.join(path.dirname(__file__), "locales", request.match_info["path"])
return web.FileResponse(file, headers={"Cache-Control": "no-cache", "Content-Type": "application/json"})
async def get_plugins(self, request): async def get_plugins(self, request):
plugins = list(self.plugins.values()) 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]) 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 {{name}}"
}
},
"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"
}
}
}
+3 -2
View File
@@ -61,12 +61,13 @@ class Utilities:
res["success"] = False res["success"] = False
return web.json_response(res) 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( return await self.context.plugin_browser.request_plugin_install(
artifact=artifact, artifact=artifact,
name=name, name=name,
version=version, version=version,
hash=hash hash=hash,
install_type=install_type
) )
async def confirm_plugin_install(self, request_id): async def confirm_plugin_install(self, request_id):
+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 -2
View File
@@ -22,9 +22,10 @@
"@types/react-router": "5.1.18", "@types/react-router": "5.1.18",
"@types/webpack": "^5.28.1", "@types/webpack": "^5.28.1",
"husky": "^8.0.3", "husky": "^8.0.3",
"i18next-parser": "^7.7.0",
"import-sort-style-module": "^6.0.0", "import-sort-style-module": "^6.0.0",
"inquirer": "^8.2.5", "inquirer": "^8.2.5",
"prettier": "^2.8.7", "prettier": "^2.8.8",
"prettier-plugin-import-sort": "^0.0.7", "prettier-plugin-import-sort": "^0.0.7",
"react": "16.14.0", "react": "16.14.0",
"react-dom": "16.14.0", "react-dom": "16.14.0",
@@ -43,9 +44,13 @@
}, },
"dependencies": { "dependencies": {
"decky-frontend-lib": "3.20.6", "decky-frontend-lib": "3.20.6",
"i18next": "^22.4.15",
"i18next-browser-languagedetector": "^7.0.1",
"i18next-http-backend": "^2.2.0",
"react-file-icon": "^1.3.0", "react-file-icon": "^1.3.0",
"react-i18next": "^12.2.2",
"react-icons": "^4.8.0", "react-icons": "^4.8.0",
"react-markdown": "^8.0.6", "react-markdown": "^8.0.7",
"remark-gfm": "^3.0.1" "remark-gfm": "^3.0.1"
} }
} }
+1428 -223
View File
File diff suppressed because it is too large Load Diff
+5 -3
View File
@@ -14,8 +14,10 @@ export default defineConfig({
input: 'src/index.tsx', input: 'src/index.tsx',
plugins: [ plugins: [
del({ targets: '../backend/static/*', force: true }), del({ targets: '../backend/static/*', force: true }),
commonjs(), commonjs({include: '../frontend/node_modules/**'}),
nodeResolve(), nodeResolve({
browser: true
}),
externalGlobals({ externalGlobals({
react: 'SP_REACT', react: 'SP_REACT',
'react-dom': 'SP_REACTDOM', 'react-dom': 'SP_REACTDOM',
@@ -44,4 +46,4 @@ export default defineConfig({
if (hiddenWarnings.some((warning) => message.code === warning)) return; if (hiddenWarnings.some((warning) => message.code === warning)) return;
handleWarning(message); handleWarning(message);
}, },
}); });
@@ -1,18 +1,31 @@
import { ConfirmModal, Navigation, QuickAccessTab } from 'decky-frontend-lib'; import { ConfirmModal, Navigation, QuickAccessTab } from 'decky-frontend-lib';
import { FC, useState } from 'react'; import { FC, useState } from 'react';
import { useTranslation } from 'react-i18next';
import TPluginInstallModal, { TranslatedPart } from './TPluginInstallModal';
interface PluginInstallModalProps { interface PluginInstallModalProps {
artifact: string; artifact: string;
version: string; version: string;
hash: string; hash: string;
// reinstall: boolean; installType: number;
onOK(): void; onOK(): void;
onCancel(): void; onCancel(): void;
closeModal?(): 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 [loading, setLoading] = useState<boolean>(false);
const { t } = useTranslation();
return ( return (
<ConfirmModal <ConfirmModal
bOKDisabled={loading} bOKDisabled={loading}
@@ -26,14 +39,22 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({ artifact, version, ha
onCancel={async () => { onCancel={async () => {
await onCancel(); await onCancel();
}} }}
strTitle={`Install ${artifact}`} strTitle={<TPluginInstallModal trans_part={TranslatedPart.TITLE} trans_type={installType} artifact={artifact} />}
strOKButtonText={loading ? 'Installing' : 'Install'} strOKButtonText={
loading ? (
<TPluginInstallModal trans_part={TranslatedPart.BUTTON_PROC} trans_type={installType} />
) : (
<TPluginInstallModal trans_part={TranslatedPart.BUTTON_IDLE} trans_type={installType} />
)
}
> >
Are you sure you want to install {artifact} <TPluginInstallModal
{version ? ` ${version}` : ''}? trans_part={TranslatedPart.DESC}
{hash == 'False' && ( trans_type={installType}
<span style={{ color: 'red' }}> This plugin does not have a hash, you are installing it at your own risk.</span> artifact={artifact}
)} version={version ? version : ''}
/>
{hash == 'False' && <span style={{ color: 'red' }}>{t('PluginInstallModal.no_hash')}</span>}
</ConfirmModal> </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 { useEffect } from 'react';
import { FunctionComponent, useState } from 'react'; import { FunctionComponent, useState } from 'react';
import { FileIcon, defaultStyles } from 'react-file-icon'; import { FileIcon, defaultStyles } from 'react-file-icon';
import { useTranslation } from 'react-i18next';
import { FaArrowUp, FaFolder } from 'react-icons/fa'; import { FaArrowUp, FaFolder } from 'react-icons/fa';
import Logger from '../../../logger'; import Logger from '../../../logger';
@@ -47,6 +48,7 @@ const FilePicker: FunctionComponent<FilePickerProps> = ({
onSubmit, onSubmit,
closeModal, closeModal,
}) => { }) => {
const { t } = useTranslation();
if (startPath.endsWith('/')) startPath = startPath.substring(0, startPath.length - 1); // remove trailing path if (startPath.endsWith('/')) startPath = startPath.substring(0, startPath.length - 1); // remove trailing path
const [path, setPath] = useState<string>(startPath); const [path, setPath] = useState<string>(startPath);
const [listing, setListing] = useState<FileListing>({ files: [], realpath: path }); const [listing, setListing] = useState<FileListing>({ files: [], realpath: path });
@@ -158,7 +160,7 @@ const FilePicker: FunctionComponent<FilePickerProps> = ({
closeModal?.(); closeModal?.();
}} }}
> >
Use this folder {t('FilePickerIndex.folder.select')}
</DialogButton> </DialogButton>
)} )}
</div> </div>
+5 -3
View File
@@ -1,5 +1,6 @@
import { SidebarNavigation } from 'decky-frontend-lib'; import { SidebarNavigation } from 'decky-frontend-lib';
import { lazy } from 'react'; import { lazy } from 'react';
import { useTranslation } from 'react-i18next';
import { FaCode, FaPlug } from 'react-icons/fa'; import { FaCode, FaPlug } from 'react-icons/fa';
import { useSetting } from '../../utils/hooks/useSetting'; import { useSetting } from '../../utils/hooks/useSetting';
@@ -12,22 +13,23 @@ const DeveloperSettings = lazy(() => import('./pages/developer'));
export default function SettingsPage() { export default function SettingsPage() {
const [isDeveloper, setIsDeveloper] = useSetting<boolean>('developer.enabled', false); const [isDeveloper, setIsDeveloper] = useSetting<boolean>('developer.enabled', false);
const { t } = useTranslation();
const pages = [ const pages = [
{ {
title: 'Decky', title: t('SettingsIndex.general_title'),
content: <GeneralSettings isDeveloper={isDeveloper} setIsDeveloper={setIsDeveloper} />, content: <GeneralSettings isDeveloper={isDeveloper} setIsDeveloper={setIsDeveloper} />,
route: '/decky/settings/general', route: '/decky/settings/general',
icon: <DeckyIcon />, icon: <DeckyIcon />,
}, },
{ {
title: 'Plugins', title: t('SettingsIndex.plugins_title'),
content: <PluginList />, content: <PluginList />,
route: '/decky/settings/plugins', route: '/decky/settings/plugins',
icon: <FaPlug />, icon: <FaPlug />,
}, },
{ {
title: 'Developer', title: t('SettingsIndex.developer_title'),
content: ( content: (
<WithSuspense> <WithSuspense>
<DeveloperSettings /> <DeveloperSettings />
@@ -8,6 +8,7 @@ import {
Toggle, Toggle,
} from 'decky-frontend-lib'; } from 'decky-frontend-lib';
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaFileArchive, FaLink, FaReact, FaSteamSymbol } from 'react-icons/fa'; import { FaFileArchive, FaLink, FaReact, FaSteamSymbol } from 'react-icons/fa';
import { setShouldConnectToReactDevTools, setShowValveInternal } from '../../../../developer'; import { setShouldConnectToReactDevTools, setShowValveInternal } from '../../../../developer';
@@ -24,8 +25,10 @@ const installFromZip = () => {
installFromURL(url); installFromURL(url);
} else { } else {
window.DeckyPluginLoader.toaster.toast({ window.DeckyPluginLoader.toaster.toast({
//title: t('SettingsDeveloperIndex.toast_zip.title'),
title: 'Decky', title: 'Decky',
body: `Installation failed! Only ZIP files are supported.`, //body: t('SettingsDeveloperIndex.toast_zip.body'),
body: 'Installation failed! Only ZIP files are supported.',
onClick: installFromZip, onClick: installFromZip,
}); });
} }
@@ -38,33 +41,47 @@ export default function DeveloperSettings() {
const [reactDevtoolsIP, setReactDevtoolsIP] = useSetting<string>('developer.rdt.ip', ''); const [reactDevtoolsIP, setReactDevtoolsIP] = useSetting<string>('developer.rdt.ip', '');
const [pluginURL, setPluginURL] = useState(''); const [pluginURL, setPluginURL] = useState('');
const textRef = useRef<HTMLDivElement>(null); const textRef = useRef<HTMLDivElement>(null);
const { t } = useTranslation();
return ( return (
<DialogBody> <DialogBody>
<DialogControlsSection> <DialogControlsSection>
<DialogControlsSectionHeader>Third-Party Plugins</DialogControlsSectionHeader> <DialogControlsSectionHeader>
<Field label="Install Plugin from ZIP File" icon={<FaFileArchive style={{ display: 'block' }} />}> {t('SettingsDeveloperIndex.third_party_plugins.header')}
<DialogButton onClick={installFromZip}>Browse</DialogButton> </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>
<Field <Field
label="Install Plugin from URL" label={t('SettingsDeveloperIndex.third_party_plugins.label_url')}
description={<TextField label={'URL'} value={pluginURL} onChange={(e) => setPluginURL(e?.target.value)} />} description={
<TextField
label={t('SettingsDeveloperIndex.third_party_plugins.label_desc')}
value={pluginURL}
onChange={(e) => setPluginURL(e?.target.value)}
/>
}
icon={<FaLink style={{ display: 'block' }} />} icon={<FaLink style={{ display: 'block' }} />}
> >
<DialogButton disabled={pluginURL.length == 0} onClick={() => installFromURL(pluginURL)}> <DialogButton disabled={pluginURL.length == 0} onClick={() => installFromURL(pluginURL)}>
Install {t('SettingsDeveloperIndex.third_party_plugins.button_install')}
</DialogButton> </DialogButton>
</Field> </Field>
</DialogControlsSection> </DialogControlsSection>
<DialogControlsSection> <DialogControlsSection>
<DialogControlsSectionHeader>Other</DialogControlsSectionHeader> <DialogControlsSectionHeader>{t('SettingsDeveloperIndex.header_other')}</DialogControlsSectionHeader>
<RemoteDebuggingSettings /> <RemoteDebuggingSettings />
<Field <Field
label="Enable Valve Internal" label={t('SettingsDeveloperIndex.valve_internal.label')}
description={ description={
<span style={{ whiteSpace: 'pre-line' }}> <span style={{ whiteSpace: 'pre-line' }}>
Enables the Valve internal developer menu.{' '} {t('SettingsDeveloperIndex.valve_internal.desc1')}{' '}
<span style={{ color: 'red' }}>Do not touch anything in this menu unless you know what it does.</span> <span style={{ color: 'red' }}>{t('SettingsDeveloperIndex.valve_internal.desc2')}</span>
</span> </span>
} }
icon={<FaSteamSymbol style={{ display: 'block' }} />} icon={<FaSteamSymbol style={{ display: 'block' }} />}
@@ -78,17 +95,18 @@ export default function DeveloperSettings() {
/> />
</Field> </Field>
<Field <Field
label="Enable React DevTools" label={t('SettingsDeveloperIndex.react_devtools.label')}
description={ description={
<> <>
<span style={{ whiteSpace: 'pre-line' }}> <span style={{ whiteSpace: 'pre-line' }}>{t('SettingsDeveloperIndex.react_devtools.desc')}</span>
Enables connection to a computer running React DevTools. Changing this setting will reload Steam. Set
the IP address before enabling.
</span>
<br /> <br />
<br /> <br />
<div ref={textRef}> <div ref={textRef}>
<TextField label={'IP'} value={reactDevtoolsIP} onChange={(e) => setReactDevtoolsIP(e?.target.value)} /> <TextField
label={t('SettingsDeveloperIndex.react_devtools.ip_label')}
value={reactDevtoolsIP}
onChange={(e) => setReactDevtoolsIP(e?.target.value)}
/>
</div> </div>
</> </>
} }
@@ -1,5 +1,6 @@
import { Dropdown, Field } from 'decky-frontend-lib'; import { Dropdown, Field } from 'decky-frontend-lib';
import { FunctionComponent } from 'react'; import { FunctionComponent } from 'react';
import { useTranslation } from 'react-i18next';
import Logger from '../../../../logger'; import Logger from '../../../../logger';
import { callUpdaterMethod } from '../../../../updater'; import { callUpdaterMethod } from '../../../../updater';
@@ -14,17 +15,23 @@ enum UpdateBranch {
} }
const BranchSelect: FunctionComponent<{}> = () => { 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); const [selectedBranch, setSelectedBranch] = useSetting<UpdateBranch>('branch', UpdateBranch.Prerelease);
return ( return (
// Returns numerical values from 0 to 2 (with current branch setup as of 8/28/22) // 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 // 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 <Dropdown
rgOptions={Object.values(UpdateBranch) rgOptions={Object.values(UpdateBranch)
.filter((branch) => typeof branch == 'string') .filter((branch) => typeof branch == 'string')
.map((branch) => ({ .map((branch) => ({
label: branch, label: tBranches[UpdateBranch[branch]],
data: UpdateBranch[branch], data: UpdateBranch[branch],
}))} }))}
selectedOption={selectedBranch} selectedOption={selectedBranch}
@@ -1,19 +1,17 @@
import { Field, Toggle } from 'decky-frontend-lib'; import { Field, Toggle } from 'decky-frontend-lib';
import { useTranslation } from 'react-i18next';
import { FaChrome } from 'react-icons/fa'; import { FaChrome } from 'react-icons/fa';
import { useSetting } from '../../../../utils/hooks/useSetting'; import { useSetting } from '../../../../utils/hooks/useSetting';
export default function RemoteDebuggingSettings() { export default function RemoteDebuggingSettings() {
const [allowRemoteDebugging, setAllowRemoteDebugging] = useSetting<boolean>('cef_forward', false); const [allowRemoteDebugging, setAllowRemoteDebugging] = useSetting<boolean>('cef_forward', false);
const { t } = useTranslation();
return ( return (
<Field <Field
label="Allow Remote CEF Debugging" label={t('RemoteDebugging.remote_cef.label')}
description={ description={<span style={{ whiteSpace: 'pre-line' }}>{t('RemoteDebugging.remote_cef.desc')}</span>}
<span style={{ whiteSpace: 'pre-line' }}>
Allows unauthenticated access to the CEF debugger to anyone in your network.
</span>
}
icon={<FaChrome style={{ display: 'block' }} />} icon={<FaChrome style={{ display: 'block' }} />}
> >
<Toggle <Toggle
@@ -1,5 +1,6 @@
import { Dropdown, Field, TextField } from 'decky-frontend-lib'; import { Dropdown, Field, TextField } from 'decky-frontend-lib';
import { FunctionComponent } from 'react'; import { FunctionComponent } from 'react';
import { useTranslation } from 'react-i18next';
import { FaShapes } from 'react-icons/fa'; import { FaShapes } from 'react-icons/fa';
import Logger from '../../../../logger'; import Logger from '../../../../logger';
@@ -11,17 +12,23 @@ const logger = new Logger('StoreSelect');
const StoreSelect: FunctionComponent<{}> = () => { const StoreSelect: FunctionComponent<{}> = () => {
const [selectedStore, setSelectedStore] = useSetting<Store>('store', Store.Default); const [selectedStore, setSelectedStore] = useSetting<Store>('store', Store.Default);
const [selectedStoreURL, setSelectedStoreURL] = useSetting<string | null>('store-url', null); 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) // 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 // 0 being Default, 1 being Testing and 2 being Custom
return ( return (
<> <>
<Field label="Plugin Store Channel" childrenContainerWidth={'fixed'}> <Field label={t('StoreSelect.store_channel.label')} childrenContainerWidth={'fixed'}>
<Dropdown <Dropdown
rgOptions={Object.values(Store) rgOptions={Object.values(Store)
.filter((store) => typeof store == 'string') .filter((store) => typeof store == 'string')
.map((store) => ({ .map((store) => ({
label: store, label: tStores[Store[store]],
data: Store[store], data: Store[store],
}))} }))}
selectedOption={selectedStore} selectedOption={selectedStore}
@@ -33,11 +40,11 @@ const StoreSelect: FunctionComponent<{}> = () => {
</Field> </Field>
{selectedStore == Store.Custom && ( {selectedStore == Store.Custom && (
<Field <Field
label="Custom Store" label={t('StoreSelect.custom_store.label')}
indentLevel={1} indentLevel={1}
description={ description={
<TextField <TextField
label={'URL'} label={t('StoreSelect.custom_store.url_label')}
value={selectedStoreURL || undefined} value={selectedStoreURL || undefined}
onChange={(e) => setSelectedStoreURL(e?.target.value || null)} onChange={(e) => setSelectedStoreURL(e?.target.value || null)}
/> />
@@ -12,6 +12,7 @@ import {
import { useCallback } from 'react'; import { useCallback } from 'react';
import { Suspense, lazy } from 'react'; import { Suspense, lazy } from 'react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaExclamation } from 'react-icons/fa'; import { FaExclamation } from 'react-icons/fa';
import { VerInfo, callUpdaterMethod, finishUpdate } from '../../../../updater'; import { VerInfo, callUpdaterMethod, finishUpdate } from '../../../../updater';
@@ -23,6 +24,7 @@ const MarkdownRenderer = lazy(() => import('../../../Markdown'));
function PatchNotesModal({ versionInfo, closeModal }: { versionInfo: VerInfo | null; closeModal?: () => {} }) { function PatchNotesModal({ versionInfo, closeModal }: { versionInfo: VerInfo | null; closeModal?: () => {} }) {
const SP = findSP(); const SP = findSP();
const { t } = useTranslation();
return ( return (
<Focusable onCancelButton={closeModal}> <Focusable onCancelButton={closeModal}>
<FocusRing> <FocusRing>
@@ -45,7 +47,7 @@ function PatchNotesModal({ versionInfo, closeModal }: { versionInfo: VerInfo | n
<MarkdownRenderer onDismiss={closeModal}>{versionInfo.all[id].body}</MarkdownRenderer> <MarkdownRenderer onDismiss={closeModal}>{versionInfo.all[id].body}</MarkdownRenderer>
</WithSuspense> </WithSuspense>
) : ( ) : (
'no patch notes for this version' t('Updater.no_patch_notes_desc')
)} )}
</div> </div>
</Focusable> </Focusable>
@@ -58,7 +60,7 @@ function PatchNotesModal({ versionInfo, closeModal }: { versionInfo: VerInfo | n
initialColumn={0} initialColumn={0}
autoFocus={true} autoFocus={true}
fnGetColumnWidth={() => SP.innerWidth} fnGetColumnWidth={() => SP.innerWidth}
name="Decky Updates" name={t('Updater.decky_updates') as string}
/> />
</FocusRing> </FocusRing>
</Focusable> </Focusable>
@@ -72,6 +74,8 @@ export default function UpdaterSettings() {
const [updateProgress, setUpdateProgress] = useState<number>(-1); const [updateProgress, setUpdateProgress] = useState<number>(-1);
const [reloading, setReloading] = useState<boolean>(false); const [reloading, setReloading] = useState<boolean>(false);
const { t } = useTranslation();
useEffect(() => { useEffect(() => {
window.DeckyUpdater = { window.DeckyUpdater = {
updateProgress: (i) => { updateProgress: (i) => {
@@ -93,14 +97,14 @@ export default function UpdaterSettings() {
return ( return (
<> <>
<Field <Field
onOptionsActionDescription={versionInfo?.all ? 'Patch Notes' : undefined} onOptionsActionDescription={versionInfo?.all ? t('Updater.patch_notes_desc') : undefined}
onOptionsButton={versionInfo?.all ? showPatchNotes : undefined} onOptionsButton={versionInfo?.all ? showPatchNotes : undefined}
label="Decky Updates" label={t('Updater.updates.label')}
description={ description={
checkingForUpdates || versionInfo?.remote?.tag_name != versionInfo?.current || !versionInfo?.remote ? ( 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={ icon={
@@ -129,10 +133,10 @@ export default function UpdaterSettings() {
} }
> >
{checkingForUpdates {checkingForUpdates
? 'Checking' ? t('Updater.updates.checking')
: !versionInfo?.remote || versionInfo?.remote?.tag_name == versionInfo?.current : !versionInfo?.remote || versionInfo?.remote?.tag_name == versionInfo?.current
? 'Check For Updates' ? t('Updater.updates.check_button')
: 'Install Update'} : t('Updater.updates.install_button')}
</DialogButton> </DialogButton>
) : ( ) : (
<ProgressBarWithInfo <ProgressBarWithInfo
@@ -140,7 +144,7 @@ export default function UpdaterSettings() {
bottomSeparator="none" bottomSeparator="none"
nProgress={updateProgress} nProgress={updateProgress}
indeterminate={reloading} indeterminate={reloading}
sOperationText={reloading ? 'Reloading' : 'Updating'} sOperationText={reloading ? t('Updater.updates.reloading') : t('Updater.updates.updating')}
/> />
)} )}
</Field> </Field>
@@ -1,4 +1,5 @@
import { DialogBody, DialogControlsSection, DialogControlsSectionHeader, Field, Toggle } from 'decky-frontend-lib'; import { DialogBody, DialogControlsSection, DialogControlsSectionHeader, Field, Toggle } from 'decky-frontend-lib';
import { useTranslation } from 'react-i18next';
import { useDeckyState } from '../../../DeckyState'; import { useDeckyState } from '../../../DeckyState';
import BranchSelect from './BranchSelect'; import BranchSelect from './BranchSelect';
@@ -13,21 +14,22 @@ export default function GeneralSettings({
setIsDeveloper: (val: boolean) => void; setIsDeveloper: (val: boolean) => void;
}) { }) {
const { versionInfo } = useDeckyState(); const { versionInfo } = useDeckyState();
const { t } = useTranslation();
return ( return (
<DialogBody> <DialogBody>
<DialogControlsSection> <DialogControlsSection>
<DialogControlsSectionHeader>Updates</DialogControlsSectionHeader> <DialogControlsSectionHeader>{t('SettingsGeneralIndex.updates.header')}</DialogControlsSectionHeader>
<UpdaterSettings /> <UpdaterSettings />
</DialogControlsSection> </DialogControlsSection>
<DialogControlsSection> <DialogControlsSection>
<DialogControlsSectionHeader>Beta Participation</DialogControlsSectionHeader> <DialogControlsSectionHeader>{t('SettingsGeneralIndex.beta.header')}</DialogControlsSectionHeader>
<BranchSelect /> <BranchSelect />
<StoreSelect /> <StoreSelect />
</DialogControlsSection> </DialogControlsSection>
<DialogControlsSection> <DialogControlsSection>
<DialogControlsSectionHeader>Other</DialogControlsSectionHeader> <DialogControlsSectionHeader>{t('SettingsGeneralIndex.other.header')}</DialogControlsSectionHeader>
<Field label="Enable Developer Mode"> <Field label={t('SettingsGeneralIndex.developer_mode.label')}>
<Toggle <Toggle
value={isDeveloper} value={isDeveloper}
onChange={(toggleValue) => { onChange={(toggleValue) => {
@@ -37,8 +39,8 @@ export default function GeneralSettings({
</Field> </Field>
</DialogControlsSection> </DialogControlsSection>
<DialogControlsSection> <DialogControlsSection>
<DialogControlsSectionHeader>About</DialogControlsSectionHeader> <DialogControlsSectionHeader>{t('SettingsGeneralIndex.about.header')}</DialogControlsSectionHeader>
<Field label="Decky Version" focusable={true}> <Field label={t('SettingsGeneralIndex.about.decky_version')} focusable={true}>
<div style={{ color: 'var(--gpSystemLighterGrey)' }}>{versionInfo?.current}</div> <div style={{ color: 'var(--gpSystemLighterGrey)' }}>{versionInfo?.current}</div>
</Field> </Field>
</DialogControlsSection> </DialogControlsSection>
@@ -10,8 +10,10 @@ import {
showContextMenu, showContextMenu,
} from 'decky-frontend-lib'; } from 'decky-frontend-lib';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaDownload, FaEllipsisH, FaRecycle } from 'react-icons/fa'; import { FaDownload, FaEllipsisH, FaRecycle } from 'react-icons/fa';
import { InstallType } from '../../../../plugin';
import { StorePluginVersion, getPluginList, requestPluginInstall } from '../../../../store'; import { StorePluginVersion, getPluginList, requestPluginInstall } from '../../../../store';
import { useSetting } from '../../../../utils/hooks/useSetting'; import { useSetting } from '../../../../utils/hooks/useSetting';
import { useDeckyState } from '../../../DeckyState'; import { useDeckyState } from '../../../DeckyState';
@@ -25,19 +27,33 @@ async function reinstallPlugin(pluginName: string, currentVersion?: string) {
const remotePlugin = serverData?.find((x) => x.name == pluginName); const remotePlugin = serverData?.find((x) => x.name == pluginName);
if (remotePlugin && remotePlugin.versions?.length > 0) { if (remotePlugin && remotePlugin.versions?.length > 0) {
const currentVersionData = remotePlugin.versions.find((version) => version.name == currentVersion); const currentVersionData = remotePlugin.versions.find((version) => version.name == currentVersion);
if (currentVersionData) requestPluginInstall(pluginName, currentVersionData); if (currentVersionData) requestPluginInstall(pluginName, currentVersionData, InstallType.REINSTALL);
} }
} }
function PluginInteractables(props: { entry: ReorderableEntry<PluginData> }) { function PluginInteractables(props: { entry: ReorderableEntry<PluginData> }) {
const data = props.entry.data; const data = props.entry.data;
const { t } = useTranslation();
let pluginName = labelToName(props.entry.label, data?.version); let pluginName = labelToName(props.entry.label, data?.version);
const showCtxMenu = (e: MouseEvent | GamepadEvent) => { const showCtxMenu = (e: MouseEvent | GamepadEvent) => {
showContextMenu( showContextMenu(
<Menu label="Plugin Actions"> <Menu label={t('PluginListIndex.plugin_actions')}>
<MenuItem onSelected={() => window.DeckyPluginLoader.importPlugin(pluginName, data?.version)}>Reload</MenuItem> <MenuItem onSelected={() => window.DeckyPluginLoader.importPlugin(pluginName, data?.version)}>
<MenuItem onSelected={() => window.DeckyPluginLoader.uninstallPlugin(pluginName)}>Uninstall</MenuItem> {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>
</Menu>, </Menu>,
e.currentTarget ?? window, e.currentTarget ?? window,
); );
@@ -48,11 +64,11 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginData> }) {
{data?.update ? ( {data?.update ? (
<DialogButton <DialogButton
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }} style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
onClick={() => requestPluginInstall(pluginName, data?.update as StorePluginVersion)} onClick={() => requestPluginInstall(pluginName, data?.update as StorePluginVersion, InstallType.UPDATE)}
onOKButton={() => requestPluginInstall(pluginName, data?.update as StorePluginVersion)} onOKButton={() => requestPluginInstall(pluginName, data?.update as StorePluginVersion, InstallType.UPDATE)}
> >
<div style={{ display: 'flex', flexDirection: 'row' }}> <div style={{ display: 'flex', flexDirection: 'row' }}>
Update to {data?.update?.name} {t('PluginListIndex.update_to', { name: data?.update?.name })}
<FaDownload style={{ paddingLeft: '2rem' }} /> <FaDownload style={{ paddingLeft: '2rem' }} />
</div> </div>
</DialogButton> </DialogButton>
@@ -63,7 +79,7 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginData> }) {
onOKButton={() => reinstallPlugin(pluginName, data?.version)} onOKButton={() => reinstallPlugin(pluginName, data?.version)}
> >
<div style={{ display: 'flex', flexDirection: 'row' }}> <div style={{ display: 'flex', flexDirection: 'row' }}>
Reinstall {t('PluginListIndex.reinstall')}
<FaRecycle style={{ paddingLeft: '5.3rem' }} /> <FaRecycle style={{ paddingLeft: '5.3rem' }} />
</div> </div>
</DialogButton> </DialogButton>
@@ -90,6 +106,7 @@ export default function PluginList() {
'pluginOrder', 'pluginOrder',
plugins.map((plugin) => plugin.name), plugins.map((plugin) => plugin.name),
); );
const { t } = useTranslation();
useEffect(() => { useEffect(() => {
window.DeckyPluginLoader.checkPluginUpdates(); window.DeckyPluginLoader.checkPluginUpdates();
@@ -115,7 +132,7 @@ export default function PluginList() {
if (plugins.length === 0) { if (plugins.length === 0) {
return ( return (
<div> <div>
<p>No plugins installed</p> <p>{t('PluginListIndex.no_plugin')}</p>
</div> </div>
); );
} }
+11 -5
View File
@@ -7,7 +7,9 @@ import {
SuspensefulImage, SuspensefulImage,
} from 'decky-frontend-lib'; } from 'decky-frontend-lib';
import { FC, useState } from 'react'; import { FC, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { InstallType } from '../../plugin';
import { StorePlugin, StorePluginVersion, requestPluginInstall } from '../../store'; import { StorePlugin, StorePluginVersion, requestPluginInstall } from '../../store';
interface PluginCardProps { interface PluginCardProps {
@@ -18,6 +20,8 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
const [selectedOption, setSelectedOption] = useState<number>(0); const [selectedOption, setSelectedOption] = useState<number>(0);
const root: boolean = plugin.tags.some((tag) => tag === 'root'); const root: boolean = plugin.tags.some((tag) => tag === 'root');
const { t } = useTranslation();
return ( return (
<div <div
className="deckyStoreCard" className="deckyStoreCard"
@@ -97,7 +101,7 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
plugin.description plugin.description
) : ( ) : (
<span> <span>
<i style={{ color: '#666' }}>No description provided.</i> <i style={{ color: '#666' }}>{t('PluginCard.plugin_no_desc')}</i>
</span> </span>
)} )}
</span> </span>
@@ -109,7 +113,7 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
color: '#fee75c', color: '#fee75c',
}} }}
> >
<i>This plugin has full access to your Steam Deck.</i>{' '} <i>{t('PluginCard.plugin_full_access')}</i>{' '}
<a <a
className="deckyStoreCardDescriptionRootLink" className="deckyStoreCardDescriptionRootLink"
href="https://deckbrew.xyz/root" href="https://deckbrew.xyz/root"
@@ -144,9 +148,11 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
<ButtonItem <ButtonItem
bottomSeparator="none" bottomSeparator="none"
layout="below" 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> </ButtonItem>
</div> </div>
<div <div
@@ -163,7 +169,7 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
label: version.name, label: version.name,
})) as SingleDropdownOption[] })) as SingleDropdownOption[]
} }
menuLabel="Plugin Version" menuLabel={t('PluginCard.plugin_version_label') as string}
selectedOption={selectedOption} selectedOption={selectedOption}
onChange={({ data }) => setSelectedOption(data)} onChange={({ data }) => setSelectedOption(data)}
/> />
+31 -23
View File
@@ -9,6 +9,7 @@ import {
findModule, findModule,
} from 'decky-frontend-lib'; } from 'decky-frontend-lib';
import { FC, useEffect, useMemo, useState } from 'react'; import { FC, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import logo from '../../../assets/plugin_store.png'; import logo from '../../../assets/plugin_store.png';
import Logger from '../../logger'; import Logger from '../../logger';
@@ -25,6 +26,8 @@ const StorePage: FC<{}> = () => {
return false; return false;
}); });
const { t } = useTranslation();
useEffect(() => { useEffect(() => {
(async () => { (async () => {
const res = await getPluginList(); const res = await getPluginList();
@@ -54,13 +57,13 @@ const StorePage: FC<{}> = () => {
}} }}
tabs={[ tabs={[
{ {
title: 'Browse', title: t('Store.store_tabs.title'),
content: <BrowseTab children={{ data: data }} />, content: <BrowseTab children={{ data: data }} />,
id: 'browse', id: 'browse',
renderTabAddon: () => <span className={TabCount}>{data.length}</span>, renderTabAddon: () => <span className={TabCount}>{data.length}</span>,
}, },
{ {
title: 'About', title: t('Store.store_tabs.about'),
content: <AboutTab />, content: <AboutTab />,
id: 'about', id: 'about',
}, },
@@ -73,10 +76,12 @@ const StorePage: FC<{}> = () => {
}; };
const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => { const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
const { t } = useTranslation();
const sortOptions = useMemo( const sortOptions = useMemo(
(): DropdownOption[] => [ (): DropdownOption[] => [
{ data: 1, label: 'Alphabetical (A to Z)' }, { data: 1, label: t('Store.store_tabs.alph_desc') },
{ data: 2, label: 'Alphabetical (Z to A)' }, { data: 2, label: t('Store.store_tabs.alph_asce') },
], ],
[], [],
); );
@@ -105,11 +110,11 @@ const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
width: '47.5%', width: '47.5%',
}} }}
> >
<span className="DialogLabel">Sort</span> <span className="DialogLabel">{t("Store.store_sort.label")}</span>
<Dropdown <Dropdown
menuLabel="Sort" menuLabel={t("Store.store_sort.label") as string}
rgOptions={sortOptions} rgOptions={sortOptions}
strDefaultLabel="Last Updated (Newest)" strDefaultLabel={t("Store.store_sort.label_def") as string}
selectedOption={selectedSort} selectedOption={selectedSort}
onChange={(e) => setSort(e.data)} onChange={(e) => setSort(e.data)}
/> />
@@ -122,11 +127,11 @@ const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
marginLeft: 'auto', marginLeft: 'auto',
}} }}
> >
<span className="DialogLabel">Filter</span> <span className="DialogLabel">{t("Store.store_filter.label")}</span>
<Dropdown <Dropdown
menuLabel="Filter" menuLabel={t("Store.store_filter.label")}
rgOptions={filterOptions} rgOptions={filterOptions}
strDefaultLabel="All" strDefaultLabel={t("Store.store_filter.label_def")}
selectedOption={selectedFilter} selectedOption={selectedFilter}
onChange={(e) => setFilter(e.data)} onChange={(e) => setFilter(e.data)}
/> />
@@ -136,7 +141,7 @@ const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
<div style={{ justifyContent: 'center', display: 'flex' }}> <div style={{ justifyContent: 'center', display: 'flex' }}>
<Focusable style={{ display: 'flex', alignItems: 'center', width: '96%' }}> <Focusable style={{ display: 'flex', alignItems: 'center', width: '96%' }}>
<div style={{ width: '100%' }}> <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> </div>
</Focusable> </Focusable>
</div> </div>
@@ -151,11 +156,11 @@ const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
maxWidth: '100%', maxWidth: '100%',
}} }}
> >
<span className="DialogLabel">Sort</span> <span className="DialogLabel">{t('Store.store_sort.label')}</span>
<Dropdown <Dropdown
menuLabel="Sort" menuLabel={t('Store.store_sort.label') as string}
rgOptions={sortOptions} rgOptions={sortOptions}
strDefaultLabel="Last Updated (Newest)" strDefaultLabel={t('Store.store_sort.label_def') as string}
selectedOption={selectedSort} selectedOption={selectedSort}
onChange={(e) => setSort(e.data)} onChange={(e) => setSort(e.data)}
/> />
@@ -165,7 +170,11 @@ const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
<div style={{ justifyContent: 'center', display: 'flex' }}> <div style={{ justifyContent: 'center', display: 'flex' }}>
<Focusable style={{ display: 'flex', alignItems: 'center', width: '96%' }}> <Focusable style={{ display: 'flex', alignItems: 'center', width: '96%' }}>
<div style={{ width: '100%' }}> <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> </div>
</Focusable> </Focusable>
</div> </div>
@@ -192,6 +201,8 @@ const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
}; };
const AboutTab: FC<{}> = () => { const AboutTab: FC<{}> = () => {
const { t } = useTranslation();
return ( return (
<div <div
style={{ style={{
@@ -216,7 +227,7 @@ const AboutTab: FC<{}> = () => {
/> />
<span className="deckyStoreAboutHeader">Testing</span> <span className="deckyStoreAboutHeader">Testing</span>
<span> <span>
Please consider testing new plugins to help the Decky Loader team!{' '} {t('Store.store_testing_cta')}{' '}
<a <a
href="https://deckbrew.xyz/testing" href="https://deckbrew.xyz/testing"
target="_blank" target="_blank"
@@ -227,13 +238,10 @@ const AboutTab: FC<{}> = () => {
deckbrew.xyz/testing deckbrew.xyz/testing
</a> </a>
</span> </span>
<span className="deckyStoreAboutHeader">Contributing</span> <span className="deckyStoreAboutHeader">{t('Store.store_contrib.label')}</span>
<span> <span>{t('Store.store_contrib.desc')}</span>
If you would like to contribute to the Decky Plugin Store, check the SteamDeckHomebrew/decky-plugin-template <span className="deckyStoreAboutHeader">{t('Store.store_source.label')}</span>
repository on GitHub. Information on development and distribution is available in the README. <span>{t('Store.store_source.desc')}</span>
</span>
<span className="deckyStoreAboutHeader">Source Code</span>
<span>All plugin source code is available on SteamDeckHomebrew/decky-plugin-database repository on GitHub.</span>
</div> </div>
); );
}; };
+5 -2
View File
@@ -18,6 +18,7 @@ import {
staticClasses, staticClasses,
updaterFieldClasses, updaterFieldClasses,
} from 'decky-frontend-lib'; } from 'decky-frontend-lib';
import { useTranslation } from 'react-i18next';
import { FaReact } from 'react-icons/fa'; import { FaReact } from 'react-icons/fa';
import Logger from './logger'; import Logger from './logger';
@@ -58,9 +59,11 @@ export async function setShowValveInternal(show: boolean) {
} }
export async function setShouldConnectToReactDevTools(enable: boolean) { export async function setShouldConnectToReactDevTools(enable: boolean) {
const { t } = useTranslation();
window.DeckyPluginLoader.toaster.toast({ window.DeckyPluginLoader.toaster.toast({
title: (enable ? 'Enabling' : 'Disabling') + ' React DevTools', title: (enable ? t('Developer.enabling') : t('Developer.disabling')) + ' React DevTools',
body: 'Reloading in 5 seconds', body: t('Developer.5secreload'),
icon: <FaReact />, icon: <FaReact />,
}); });
await sleep(5000); await sleep(5000);
+33 -1
View File
@@ -1,4 +1,8 @@
import { Navigation, Router, sleep } from 'decky-frontend-lib'; import { Navigation, Router, sleep } from 'decky-frontend-lib';
import i18n from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import Backend from 'i18next-http-backend';
import { initReactI18next } from 'react-i18next';
import PluginLoader from './plugin-loader'; import PluginLoader from './plugin-loader';
import { DeckyUpdater } from './updater'; import { DeckyUpdater } from './updater';
@@ -36,9 +40,35 @@ declare global {
(async () => { (async () => {
window.deckyAuthToken = await fetch('http://127.0.0.1:1337/auth/token').then((r) => r.text()); window.deckyAuthToken = await fetch('http://127.0.0.1:1337/auth/token').then((r) => r.text());
i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
load: 'currentOnly',
detection: {
order: ['querystring', 'navigator'],
lookupQuerystring: 'lng',
},
//debug: true,
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?.dismountAll();
window.DeckyPluginLoader?.deinit(); window.DeckyPluginLoader?.deinit();
window.DeckyPluginLoader = new PluginLoader(); window.DeckyPluginLoader = new PluginLoader();
window.DeckyPluginLoader.init(); window.DeckyPluginLoader.init();
window.importDeckyPlugin = function (name: string, version: string) { window.importDeckyPlugin = function (name: string, version: string) {
@@ -62,3 +92,5 @@ declare global {
setTimeout(() => window.syncDeckyPlugins(), 5000); setTimeout(() => window.syncDeckyPlugins(), 5000);
})(); })();
export default i18n;
+32 -6
View File
@@ -98,7 +98,9 @@ class PluginLoader extends Logger {
const versionInfo = await this.updateVersion(); const versionInfo = await this.updateVersion();
if (versionInfo?.remote && versionInfo?.remote?.tag_name != versionInfo?.current) { if (versionInfo?.remote && versionInfo?.remote?.tag_name != versionInfo?.current) {
this.toaster.toast({ this.toaster.toast({
//title: t('PluginLoader.decky_title'),
title: 'Decky', title: 'Decky',
//body: t('PluginLoader.decky_update_available', { tag_name: versionInfo?.remote?.tag_name }),
body: `Update to ${versionInfo?.remote?.tag_name} available!`, body: `Update to ${versionInfo?.remote?.tag_name} available!`,
onClick: () => Router.Navigate('/decky/settings'), onClick: () => Router.Navigate('/decky/settings'),
}); });
@@ -118,26 +120,35 @@ class PluginLoader extends Logger {
const updates = await this.checkPluginUpdates(); const updates = await this.checkPluginUpdates();
if (updates?.size > 0) { if (updates?.size > 0) {
this.toaster.toast({ this.toaster.toast({
//title: t('PluginLoader.decky_title'),
title: 'Decky', title: 'Decky',
//body: t('PluginLoader.plugin_update', { count: updates.size }),
body: `Updates available for ${updates.size} plugin${updates.size > 1 ? 's' : ''}!`, body: `Updates available for ${updates.size} plugin${updates.size > 1 ? 's' : ''}!`,
onClick: () => Router.Navigate('/decky/settings/plugins'), 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( showModal(
<PluginInstallModal <PluginInstallModal
artifact={artifact} artifact={artifact}
version={version} version={version}
hash={hash} hash={hash}
installType={install_type}
onOK={() => this.callServerMethod('confirm_plugin_install', { request_id })} onOK={() => this.callServerMethod('confirm_plugin_install', { request_id })}
onCancel={() => this.callServerMethod('cancel_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( showModal(
<ConfirmModal <ConfirmModal
onOK={async () => { onOK={async () => {
@@ -146,10 +157,10 @@ class PluginLoader extends Logger {
onCancel={() => { onCancel={() => {
// do nothing // do nothing
}} }}
strTitle={`Uninstall ${name}`} strTitle={title}
strOKButtonText={'Uninstall'} strOKButtonText={button_text}
> >
Are you sure you want to uninstall {name}? {description}
</ConfirmModal>, </ConfirmModal>,
); );
} }
@@ -242,7 +253,17 @@ class PluginLoader extends Logger {
version: version, version: version,
}); });
} catch (e) { } catch (e) {
//this.error(t('PluginLoader.plugin_load_error.message', { name: name }), e);
this.error('Error loading plugin ' + 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<{}> = () => ( const TheError: FC<{}> = () => (
<> <>
Error:{' '} Error:{' '}
@@ -261,7 +282,12 @@ class PluginLoader extends Logger {
content: <TheError />, content: <TheError />,
icon: <FaExclamationCircle />, 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`); } else throw new Error(`${name} frontend_bundle not OK`);
} }
+6
View File
@@ -6,3 +6,9 @@ export interface Plugin {
onDismount?(): void; onDismount?(): void;
alwaysRender?: boolean; 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'; import { getSetting, setSetting } from './utils/settings';
export enum Store { 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 = const artifactUrl =
selectedVer.artifact ?? `https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/versions/${selectedVer.hash}.zip`; selectedVer.artifact ?? `https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/versions/${selectedVer.hash}.zip`;
await window.DeckyPluginLoader.callServerMethod('install_plugin', { await window.DeckyPluginLoader.callServerMethod('install_plugin', {
@@ -81,6 +81,7 @@ export async function requestPluginInstall(plugin: string, selectedVer: StorePlu
artifact: artifactUrl, artifact: artifactUrl,
version: selectedVer.name, version: selectedVer.name,
hash: selectedVer.hash, hash: selectedVer.hash,
install_type: installType,
}); });
} }
+2 -1
View File
@@ -16,7 +16,8 @@
"strict": true, "strict": true,
"suppressImplicitAnyIndexErrors": true, "suppressImplicitAnyIndexErrors": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"skipLibCheck": true "skipLibCheck": true,
"resolveJsonModule": true
}, },
"include": ["src", "index.d.ts"], "include": ["src", "index.d.ts"],
"exclude": ["node_modules"] "exclude": ["node_modules"]