Compare commits

...

20 Commits

Author SHA1 Message Date
Gio 00e866c39e feat: add backend Mac support (#913)
Adds a Mac localplatform based on the Linux one

Signed-off-by: Gianni Spadoni <me@gio.blue>
2026-06-07 11:25:20 -04:00
Rudhra Raveendran 5490961056 Update directory path to use $HOME variable in plugin info script (#869) 2026-06-06 11:35:46 -04:00
jurassicplayer d4442f6b0d Change hardcoded ~ to HOME env var (#847) 2026-06-06 11:23:34 -04:00
WerWolvTranslationBot 7a629f1cef Translations update from Weblate (#814)
* Added translation using Weblate (Romanian)

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/

* Translated using Weblate (Romanian)

Currently translated at 100.0% (176 of 176 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/ro/

* Translated using Weblate (Japanese)

Currently translated at 100.0% (176 of 176 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/ja/

* Translated using Weblate (Italian)

Currently translated at 100.0% (176 of 176 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/it/

* Translated using Weblate (Spanish)

Currently translated at 100.0% (186 of 186 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/es/

* Translated using Weblate (Russian)

Currently translated at 100.0% (186 of 186 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/ru/

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (186 of 186 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/uk/

---------

Co-authored-by: Pien <amechandotcenter@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Tak-attack <tak.bts@gmail.com>
Co-authored-by: M4ttiA <sensoriale_echidna_0w@icloud.com>
Co-authored-by: recoletosmb <recoletosmb@gmail.com>
Co-authored-by: Yevhen Chefranov <zheka767@gmail.com>
2026-06-06 11:22:21 -04:00
Chen Ye 7a7ecdac55 docs: Add Chinese (Simplified) README translation (#912)
* Add Chinese README translation and link badge

- Add README_ZH.md with full Chinese translation
- Add README badge linking to Chinese version

* docs(zh-Hans): rename to README_zh-Hans.md, replace shields.io badge with text link

Addressing review feedback from AkazaRenn:
- Rename README_ZH.md to README_zh-Hans.md for standard IETF language tag
- Remove shields.io external badge, use plain text language switcher instead
- Add bidirectional language links in both READMEs

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: juncheng.ye <yechen_0@foxmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 11:00:57 -04:00
AAGaming 232fadd796 fixes for june 2026 beta errorboundary (#916)
Co-authored-by: Beebles <102569435+beebls@users.noreply.github.com>
2026-06-05 22:29:11 -04:00
jbofill acaf165219 Improved error screen (#841)
* improve the error screen visuals

* comment out placeholder buttons

* run formatter

* Refactor DeckyErrorBoundary styles and text

- Removed gray text class usage
- Removed styles reminiscent of Steam BPM
- Fixed typos

* Further refactor of DeckyErrorBoundary.tsx

- Change background/text of buttons to be closer to Steam Deck UI
- Make panel background not reliant on transparency and have a neutral gray
- Bold "likely occurred" text
- Make swipe prompt appear in the center of a horizontal bar, drawing more attention to it
- Make "An error occurred" text smaller, as it isn't helpful for troubleshooting
- Add text clarifying solutions are in recommended order and how to get more help
- Add "Retry the action or restart" to the left of Retry, Restart Steam, and Restart Decky buttons
- Move disabling Decky to beneath the Decky update checking

* Revert header boldness change

* add disable plugin buttons to error screen

* Set background to black

---------

Co-authored-by: EMERALD <info@eme.wtf>
2026-05-25 18:43:17 -05:00
AAGaming bef7ede91f fix(ci): microsoft 2026-05-15 00:13:37 -04:00
AAGaming 511dd121bd Fix missing Field component and dropdown styling on beta (#907) 2026-05-15 00:09:13 -04:00
Michael T. DeGuzis d31c2bf034 fix(toaster): Forward playSound prop to ProcessNotification info (#901)
Builder Win / Build PluginLoader for Win (push) Has been cancelled
Builder / Build PluginLoader (push) Has been cancelled
Push Updated Plugin Stub to Template / copy-stub (push) Has been cancelled
Lint / Run linters (push) Has been cancelled
Type Check / Run type checkers (push) Has been cancelled
Co-authored-by: Michael DeGuzis <deguzim@amazon.com>
2026-04-30 12:58:53 -04:00
Kirill Nikiforov b7a884f26f fix setuid/setgid when running rootless (#892)
Builder Win / Build PluginLoader for Win (push) Has been cancelled
Builder / Build PluginLoader (push) Has been cancelled
Push Updated Plugin Stub to Template / copy-stub (push) Has been cancelled
Lint / Run linters (push) Has been cancelled
Type Check / Run type checkers (push) Has been cancelled
2026-04-11 15:35:54 -07:00
AAGaming a477bf6829 show sponsors now! [ci skip] 2026-03-24 21:08:42 -04:00
ynhhoJ 1e8bf43e5f Add key prop inside map (#867)
Builder Win / Build PluginLoader for Win (push) Has been cancelled
Builder / Build PluginLoader (push) Has been cancelled
Push Updated Plugin Stub to Template / copy-stub (push) Has been cancelled
Lint / Run linters (push) Has been cancelled
Type Check / Run type checkers (push) Has been cancelled
* fix: add unique key to list items in TestingVersionList component

* fix: add unique key to PluginCard components in BrowseTab
2026-03-23 10:43:00 -04:00
AAGaming 259d01d7ec Remove incorrect padding in the decky menu (#891)
Builder Win / Build PluginLoader for Win (push) Has been cancelled
Builder / Build PluginLoader (push) Has been cancelled
Push Updated Plugin Stub to Template / copy-stub (push) Has been cancelled
Lint / Run linters (push) Has been cancelled
Type Check / Run type checkers (push) Has been cancelled
2026-03-21 21:09:13 -04:00
AAGaming a13887a13a fix(deps): bump @decky/ui to fix tabs component on bta 2026-03-21 20:39:27 -04:00
AAGaming b97c27aac4 Fixes for march 19th 2026 beta (#890)
Builder Win / Build PluginLoader for Win (push) Has been cancelled
Builder / Build PluginLoader (push) Has been cancelled
Push Updated Plugin Stub to Template / copy-stub (push) Has been cancelled
Lint / Run linters (push) Has been cancelled
Type Check / Run type checkers (push) Has been cancelled
2026-03-20 22:08:44 -04:00
EMERALD0874 8b8a1cc4d8 Removing FUNDING.yml
Builder Win / Build PluginLoader for Win (push) Has been cancelled
Builder / Build PluginLoader (push) Has been cancelled
Push Updated Plugin Stub to Template / copy-stub (push) Has been cancelled
Lint / Run linters (push) Has been cancelled
Type Check / Run type checkers (push) Has been cancelled
2026-02-08 15:29:28 -06:00
EMERALD0874 7a283c7608 Funding update - README, FUINDING.yml 2026-02-08 15:27:07 -06:00
jbofill 9f586a1b97 Feat: Disable plugins (#850)
Builder Win / Build PluginLoader for Win (push) Has been cancelled
Builder / Build PluginLoader (push) Has been cancelled
Push Updated Plugin Stub to Template / copy-stub (push) Has been cancelled
Lint / Run linters (push) Has been cancelled
Type Check / Run type checkers (push) Has been cancelled
* implement base frontend changes necessary for plugin disabling

* implement frontend diisable functions/ modal

* plugin disable boilerplate / untested

* Feat disable plugins (#810)

* implement base frontend changes necessary for plugin disabling

* implement frontend diisable functions/ modal

---------

Co-authored-by: Jesse Bofill <jesse_bofill@yahoo.com>

* fix mistakes

* add frontend

* working plugin disable, not tested extensively

* fix uninstalled hidden plugins remaining in list

* hide plugin irrelevant plugin setting menu option when disabled

* fix hidden plugin issues

* reset disabled plugin on uninstall

* fix plugin load on reenable

* move disable settings uninstall cleanup

* add engilsh tranlsations for enable/ disable elements

* fix bug where wrong loadType can get passed to importPlugin

* show correct number of hidden plugins if plugin is both hidden and disabled

* fix: get fresh list of plugin updates when changed in settings plugin list

* fix: fix invalid semver plugin version from preventing latest updates

* retain x position when changing focus in list items  that have multiple horizontal focusables

* correction to pluging version checking validation

* make sure disabled plugins get checked for updates

* show number of disabled plugins at bottom of plugin view

* add notice to update modals that disabled plugins will be enabled upon installation

* run formatter

* Update backend/decky_loader/locales/en-US.json

Co-authored-by: EMERALD <hudson.samuels@gmail.com>

* chore: correct filename typo

* chore: change disabled icon

* chore: revert accidental defsettings changes

* format

* add timeout to frontend importPlugin

if a request hangs this prevent it from blocking other plugin loads.
backend diaptch_plugin which calls this for individual plugin load (as opposed to batch) is set to 15s.
other callers of importPlugin are not using timeout, same as before.

* fix plugin update checking loop

---------

Co-authored-by: marios <marios8543@gmail.com>
Co-authored-by: EMERALD <hudson.samuels@gmail.com>
2025-12-30 13:29:08 -06:00
Sims 789851579b Fix settings import under windows (#858)
Builder Win / Build PluginLoader for Win (push) Has been cancelled
Builder / Build PluginLoader (push) Has been cancelled
Push Updated Plugin Stub to Template / copy-stub (push) Has been cancelled
Lint / Run linters (push) Has been cancelled
Type Check / Run type checkers (push) Has been cancelled
* test

* fix linting
2025-12-20 19:12:04 +00:00
44 changed files with 1520 additions and 487 deletions
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install semver-tool asdf - name: Install semver-tool asdf
uses: asdf-vm/actions/install@v3 uses: asdf-vm/actions/install@v4
with: with:
tool_versions: | tool_versions: |
semver 3.4.0 semver 3.4.0
+17 -2
View File
@@ -3,7 +3,7 @@
<br> <br>
Decky Loader Decky Loader
<br> <br>
<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> <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="150px" style="padding-top: 15px;"></a>
</h1> </h1>
<p align="center"> <p align="center">
@@ -15,9 +15,21 @@
<a href="https://deckbrew.xyz/discord"><img src="https://img.shields.io/discord/960281551428522045?color=%235865F2&label=discord" /></a> <a href="https://deckbrew.xyz/discord"><img src="https://img.shields.io/discord/960281551428522045?color=%235865F2&label=discord" /></a>
<br> <br>
<br> <br>
🌐 <a href="./README.md">English</a> · <a href="./README_zh-Hans.md">简体中文</a>
<br>
<br>
<!-- <img src="https://media.discordapp.net/attachments/966017112244125756/1012466063893610506/main.jpg" alt="Decky screenshot" width="80%">--> <!-- <img src="https://media.discordapp.net/attachments/966017112244125756/1012466063893610506/main.jpg" alt="Decky screenshot" width="80%">-->
</p> </p>
## 🩵 Backers and Sponsors
[Become a backer or sponsor](https://opencollective.com/steamdeckhomebrew) to support our work! Contributing to our collective effort will help Decky Loader developers cover the costs of web servers, acquire new development hardware, and more.
<!-- SPONSORS COMMENTED OUT UNTIL WE GET SOME SPONSORS TO AVOID BLANK SVG SPACE -->
<a href="https://opencollective.com/steamdeckhomebrew"><img alt="Steam Deck Homebrew sponsors on Open Collective" src="https://opencollective.com/steamdeckhomebrew/sponsors.svg?button=true&avatarHeight=46&width=750"></a>
<a href="https://opencollective.com/steamdeckhomebrew"><img alt="Steam Deck Homebrew backers on Open Collective" src="https://opencollective.com/steamdeckhomebrew/backers.svg?button=false&avatarHeight=46&width=750"></a>
## 📖 About ## 📖 About
Decky Loader is a homebrew plugin launcher for the Steam Deck. It can be used to [stylize your menus](https://github.com/suchmememanyskill/SDH-CssLoader), [change system sounds](https://github.com/EMERALD0874/SDH-AudioLoader), [adjust your screen saturation](https://github.com/libvibrant/vibrantDeck), [change additional system settings](https://github.com/NGnius/PowerTools), and [more](https://plugins.deckbrew.xyz/). Decky Loader is a homebrew plugin launcher for the Steam Deck. It can be used to [stylize your menus](https://github.com/suchmememanyskill/SDH-CssLoader), [change system sounds](https://github.com/EMERALD0874/SDH-AudioLoader), [adjust your screen saturation](https://github.com/libvibrant/vibrantDeck), [change additional system settings](https://github.com/NGnius/PowerTools), and [more](https://plugins.deckbrew.xyz/).
@@ -40,7 +52,9 @@ For more information about Decky Loader as well as documentation and development
- 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). - 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 ## 💾 Installation
- This installation can be done without an admin/sudo password set. - This installation can be done without an admin/sudo password set.
1. Prepare a mouse and keyboard if possible. 1. Prepare a mouse and keyboard if possible.
- Keyboards and mice can be connected to the Steam Deck via USB-C or Bluetooth. - 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. (KDE connect is preinstalled on the steam deck) - Many Bluetooth keyboard and mouse apps are available for iOS and Android. (KDE connect is preinstalled on the steam deck)
@@ -54,7 +68,7 @@ For more information about Decky Loader as well as documentation and development
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. 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. 1. Choose the version of Decky Loader you want to install.
- **Latest Release** - **Latest Release**
Intended for most users. This is the latest stable version of Decky Loader. Intended for most users. This is the latest stable version of Decky Loader.
- **Latest Pre-Release** - **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://wiki.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. 1. Open the Return to Gaming Mode shortcut on your desktop.
@@ -68,6 +82,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. 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. 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. - 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 ## 🚀 Getting Started
+129
View File
@@ -0,0 +1,129 @@
<h1 align="center">
<a name="logo" href="https://deckbrew.xyz/"><img src="https://deckbrew.xyz/static/icon-45ca1f5aea376a9ad37e92db906f283e.png" alt="Deckbrew logo" width="200"></a>
<br>
Decky Loader
<br>
<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="150px" style="padding-top: 15px;"></a>
</h1>
<p align="center">
<a href="https://github.com/SteamDeckHomebrew/decky-loader/releases"><img src="https://img.shields.io/github/downloads/SteamDeckHomebrew/decky-loader/total" /></a>
<a href="https://github.com/SteamDeckHomebrew/decky-loader/stargazers"><img src="https://img.shields.io/github/stars/SteamDeckHomebrew/decky-loader" /></a>
<a href="https://github.com/SteamDeckHomebrew/decky-loader/commits/main"><img src="https://img.shields.io/github/last-commit/SteamDeckHomebrew/decky-loader.svg" /></a>
<a href="https://weblate.werwolv.net/engage/decky/"><img src="https://weblate.werwolv.net/widgets/decky/-/decky/svg-badge.svg" alt="Translation status" /></a>
<a href="https://github.com/SteamDeckHomebrew/decky-loader/blob/main/LICENSE"><img src="https://img.shields.io/github/license/SteamDeckHomebrew/decky-loader" /></a>
<a href="https://deckbrew.xyz/discord"><img src="https://img.shields.io/discord/960281551428522045?color=%235865F2&label=discord" /></a>
<br>
<br>
🌐 <a href="./README.md">English</a> · <a href="./README_zh-Hans.md">简体中文</a>
<br>
<br>
<!-- <img src="https://media.discordapp.net/attachments/966017112244125756/1012466063893610506/main.jpg" alt="Decky screenshot" width="80%">-->
</p>
## 🩵 赞助者
[成为赞助者](https://opencollective.com/steamdeckhomebrew)来支持我们的工作!向我们的集体项目捐款将帮助 Decky Loader 开发者支付网络服务器费用、购买新的开发硬件等。
<a href="https://opencollective.com/steamdeckhomebrew"><img alt="Steam Deck Homebrew sponsors on Open Collective" src="https://opencollective.com/steamdeckhomebrew/sponsors.svg?button=true&avatarHeight=46&width=750"></a>
<a href="https://opencollective.com/steamdeckhomebrew"><img alt="Steam Deck Homebrew backers on Open Collective" src="https://opencollective.com/steamdeckhomebrew/backers.svg?button=false&avatarHeight=46&width=750"></a>
## 📖 关于
Decky Loader 是一款用于 Steam Deck 的自制插件启动器。它可以用来[美化菜单界面](https://github.com/suchmememanyskill/SDH-CssLoader)、[更改系统音效](https://github.com/EMERALD0874/SDH-AudioLoader)、[调整屏幕饱和度](https://github.com/libvibrant/vibrantDeck)、[修改更多系统设置](https://github.com/NGnius/PowerTools),以及[更多功能](https://plugins.deckbrew.xyz/)。
有关 Decky Loader 的更多信息、文档和开发工具,请访问[我们的维基](https://wiki.deckbrew.xyz)。
### 🎨 功能特性
🧹 干净地注入和加载多个插件。
🔒 在系统更新和重启后仍然保持安装状态。
🔗 允许插件与启动器之间进行双向通信。
🐍 支持从 TypeScript React 中运行 Python 函数。
🌐 允许插件发起完全绕过 CORS 的 fetch 请求。
### 🤔 常见问题
- Syncthing 可能会占用 Steam Deck 上的 8080 端口,而 Decky Loader 需要该端口才能运行。如果您将 Syncthing 作为服务使用,请将其端口更改为其他端口。
- 建议将 Syncthing 的端口改为 8384。
- 如果您使用的任何软件占用了 1337 或 8080 端口,请将其端口更改为其他端口或卸载该软件。
- 有时 Decky 会在 SteamOS 更新后消失。只需重新运行安装程序并再次安装稳定版即可轻松解决。如果这不起作用,请尝试安装预发布版。如果还是不行,请[查看现有问题](https://github.com/SteamDeckHomebrew/decky-loader/issues),如果没有相关问题,您可以[提交一个新问题](https://github.com/SteamDeckHomebrew/decky-loader/issues/new?assignees=&labels=bug&template=bug_report.yml&title=%5BBUG%5D+%3Ctitle%3E)。
## 💾 安装
- 安装过程无需设置管理员/sudo 密码。
1. 如果可能,请准备鼠标和键盘。
- 键盘和鼠标可以通过 USB-C 或蓝牙连接到 Steam Deck。
- iOS 和 Android 上有许多蓝牙键盘和鼠标应用可用。(Steam Deck 上预安装了 KDE Connect
- Steam Link 应用可在 [Windows](https://media.steampowered.com/steamlink/windows/latest/SteamLink.zip)、[macOS](https://apps.apple.com/us/app/steam-link/id1246969117) 和 [Linux](https://flathub.org/apps/details/com.valvesoftware.SteamLink) 上使用。它可以很好地替代远程桌面。
- 如果您没有其他选择,可以使用右侧触控板作为鼠标,并按 <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> 打开屏幕键盘。
1. 按下 <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> 按钮并打开电源菜单。
1. 选择"切换到桌面模式"。
1. 在您选择的浏览器中访问此 GitHub 页面。
1. 下载[安装程序文件](https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/decky_installer.desktop)。(如果使用 Firefox,文件将命名为 `decky_installer.desktop.download`,请在运行前将其重命名为 `decky_installer.desktop`
1. 将文件拖到桌面上,然后双击运行。
1. 输入您的管理员密码或允许 Decky 临时将您的管理员密码设置为 `Decky!`(安装程序完成后将删除此密码)。
1. 选择您要安装的 Decky Loader 版本。
- **最新正式版**
面向大多数用户。这是 Decky Loader 的最新稳定版本。
- **最新预发布版**
面向插件开发者。预发布版可能尚未完全稳定,但包含最新更改。有关插件开发的更多信息,请参阅[维基页面](https://wiki.deckbrew.xyz/en/loader-dev/development)。
1. 打开桌面上的"返回游戏模式"快捷方式。
- 对于可以使用 Konsole 的用户,还有一种快速安装方式。运行 `curl -L https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/install_release.sh | sh` 并在提示时输入密码。
### 👋 卸载
很抱歉看到您离开!如果您因为遇到问题而考虑卸载,请考虑[提交问题](https://github.com/SteamDeckHomebrew/decky-loader/issues)或[加入我们的 Discord](https://deckbrew.xyz/discord),以便我们帮助您和其他用户。
1. 按下 <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> 按钮并打开电源菜单。
1. 选择"切换到桌面模式"。
1. 再次运行安装程序文件,然后选择 `uninstall decky loader`
- 对于可以使用 Konsole 的用户,还有一种快速卸载方式。运行 `curl -L https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/uninstall.sh | sh` 并在提示时输入密码。
## 🚀 入门指南
现在您已经安装了 Decky Loader,可以开始使用插件了。每个插件由不同的开发者维护,具有各自的用途,但大多数遵循以下通用结构。
### 📦 插件
1. 按下 <img src="./docs/images/light/qam.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/qam.svg#gh-light-mode-only" height=16> 按钮并导航到 <img src="./docs/images/light/plug.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/plug.svg#gh-light-mode-only" height=16> 图标。这是 Decky 菜单,用于与插件和启动器本身交互。
1. 选择 <img src="./docs/images/light/store.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/store.svg#gh-light-mode-only" height=16> 图标打开插件浏览器。在这里您可以查找和安装插件。
- 您也可以在设置菜单中通过 URL 安装。我们不建议安装来自不可信来源的插件。
1. 要安装插件,请在您想要的插件上选择"安装"按钮。您也可以从下拉菜单中选择一个版本,但不建议这样做。
1. 要更新、卸载和重新加载插件,请导航到 Decky 菜单并选择 <img src="./docs/images/light/gear.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/gear.svg#gh-light-mode-only" height=16> 图标。
- 请注意,卸载插件只会移除其插件文件,而不会删除它可能创建的任何其他文件。
### 🛠️ 插件开发
目前还没有完整的插件开发文档。不过,一个好的起点是[插件模板仓库](https://github.com/SteamDeckHomebrew/decky-plugin-template)。如果您有任何问题,请考虑[加入我们的 Discord](https://deckbrew.xyz/discord)。
### 🤝 贡献
有关安装 Decky Loader 开发版本的更多信息,请参阅[有关开发的维基页面](https://wiki.deckbrew.xyz/en/loader-dev/development)。您还可以通过观看[此 YouTube 教程](https://youtu.be/1IAbZte8e7E?t=112)在 Windows 或 Linux 计算机上安装 Steam Deck UI 进行测试。
1. 在开始您的 PR 之前,使用最新的 main 分支提交克隆仓库。
1. 在您的仓库克隆中,运行以下命令。
```bash
cd frontend
pnpm i # 注意:您可能需要使用 pnpm approve-builds 批准 esbuild 的构建脚本
pnpm run build
```
1. 如果您正在修改 UI,则需要在部署更改到 Steam Deck 之前运行这些命令。
1. 使用 VS Code 任务或 `deck.sh` 脚本将您的更改部署到 Steam Deck 以进行测试。
1. 您将使用 Python 脚本版本测试您的更改。每次都需要构建、部署和重新加载。
⚠️ 如果您因库过时而收到构建错误,请在仓库内运行此命令。
```bash
pnpm update @decky/ui --latest
```
源代码管理和插件部署留给克隆仓库的各自贡献者处理,以保持依赖项为最新版本。
## 📜 鸣谢
插件加载器概念的最初想法基于 [marios8543 的 Steam Deck UI Inject 项目](https://github.com/marios8543/steamdeck-ui-inject)的工作。
+6
View File
@@ -150,6 +150,7 @@ class PluginBrowser:
# plugins_snapshot = self.plugins.copy() # plugins_snapshot = self.plugins.copy()
# snapshot_string = pformat(plugins_snapshot) # snapshot_string = pformat(plugins_snapshot)
# logger.debug("current plugins: %s", snapshot_string) # logger.debug("current plugins: %s", snapshot_string)
if name in self.plugins: if name in self.plugins:
logger.debug("Plugin %s was found", name) logger.debug("Plugin %s was found", name)
await self.plugins[name].stop(uninstall=True) await self.plugins[name].stop(uninstall=True)
@@ -345,5 +346,10 @@ class PluginBrowser:
if name in plugin_order: if name in plugin_order:
plugin_order.remove(name) plugin_order.remove(name)
self.settings.setSetting("pluginOrder", plugin_order) self.settings.setSetting("pluginOrder", plugin_order)
disabled_plugins: List[str] = self.settings.getSetting("disabled_plugins", [])
if name in disabled_plugins:
disabled_plugins.remove(name)
self.settings.setSetting("disabled_plugins", disabled_plugins)
logger.debug("Removed any settings for plugin %s", name) logger.debug("Removed any settings for plugin %s", name)
+7 -2
View File
@@ -78,6 +78,7 @@ class Loader:
self.live_reload = live_reload self.live_reload = live_reload
self.reload_queue: ReloadQueue = Queue() self.reload_queue: ReloadQueue = Queue()
self.loop.create_task(self.handle_reloads()) self.loop.create_task(self.handle_reloads())
self.context: PluginManager = server_instance
if live_reload: if live_reload:
self.observer = Observer() self.observer = Observer()
@@ -130,7 +131,7 @@ class Loader:
async def get_plugins(self): async def get_plugins(self):
plugins = list(self.plugins.values()) plugins = list(self.plugins.values())
return [{"name": str(i), "version": i.version, "load_type": i.load_type} for i in plugins] return [{"name": str(i), "version": i.version, "load_type": i.load_type, "disabled": i.disabled} for i in plugins]
async def handle_plugin_dist(self, request: web.Request): async def handle_plugin_dist(self, request: web.Request):
plugin = self.plugins[request.match_info["plugin_name"]] plugin = self.plugins[request.match_info["plugin_name"]]
@@ -164,6 +165,10 @@ class Loader:
await self.ws.emit(f"loader/plugin_event", {"plugin": plugin.name, "event": event, "args": args}) await self.ws.emit(f"loader/plugin_event", {"plugin": plugin.name, "event": event, "args": args})
plugin = PluginWrapper(file, plugin_directory, self.plugin_path, plugin_emitted_event) plugin = PluginWrapper(file, plugin_directory, self.plugin_path, plugin_emitted_event)
if hasattr(self.context, "utilities") and plugin.name in await self.context.utilities.get_setting("disabled_plugins",[]):
plugin.disabled = True
self.plugins[plugin.name] = plugin
return
if plugin.name in self.plugins: if plugin.name in self.plugins:
if not "debug" in plugin.flags and refresh: if not "debug" in plugin.flags and refresh:
self.logger.info(f"Plugin {plugin.name} is already loaded and has requested to not be re-loaded") self.logger.info(f"Plugin {plugin.name} is already loaded and has requested to not be re-loaded")
@@ -183,7 +188,7 @@ class Loader:
print_exc() print_exc()
async def dispatch_plugin(self, name: str, version: str | None, load_type: int = PluginLoadType.ESMODULE_V1.value): async def dispatch_plugin(self, name: str, version: str | None, load_type: int = PluginLoadType.ESMODULE_V1.value):
await self.ws.emit("loader/import_plugin", name, version, load_type) await self.ws.emit("loader/import_plugin", name, version, load_type, True, 15000)
async def import_plugins(self): async def import_plugins(self):
self.logger.info(f"import plugins from {self.plugin_path}") self.logger.info(f"import plugins from {self.plugin_path}")
+18 -3
View File
@@ -102,6 +102,7 @@
}, },
"no_hash": "This plugin does not have a hash, you are installing it at your own risk.", "no_hash": "This plugin does not have a hash, you are installing it at your own risk.",
"not_installed": "(not installed)", "not_installed": "(not installed)",
"disabled": "The plugin will be re-enabled after installation",
"overwrite": { "overwrite": {
"button_idle": "Overwrite", "button_idle": "Overwrite",
"button_processing": "Overwriting", "button_processing": "Overwriting",
@@ -133,10 +134,13 @@
"uninstall": "Uninstall", "uninstall": "Uninstall",
"update_all_one": "Update 1 plugin", "update_all_one": "Update 1 plugin",
"update_all_other": "Update {{count}} plugins", "update_all_other": "Update {{count}} plugins",
"update_to": "Update to {{name}}" "update_to": "Update to {{name}}",
"disable": "Disable",
"enable": "Enable"
}, },
"PluginListLabel": { "PluginListLabel": {
"hidden": "Hidden from the quick access menu" "hidden": "Hidden from the quick access menu",
"disabled": "Plugin disabled"
}, },
"PluginLoader": { "PluginLoader": {
"decky_title": "Decky", "decky_title": "Decky",
@@ -152,12 +156,23 @@
"desc": "Are you sure you want to uninstall {{name}}?", "desc": "Are you sure you want to uninstall {{name}}?",
"title": "Uninstall {{name}}" "title": "Uninstall {{name}}"
}, },
"plugin_disable": {
"button": "Disable",
"desc": "Are you sure you want to disable {{name}}?",
"title": "Disable {{name}}",
"error": "Error disabling {{name}}"
},
"plugin_enable": {
"error": "Error enabling {{name}}"
},
"plugin_update_one": "Updates available for 1 plugin!", "plugin_update_one": "Updates available for 1 plugin!",
"plugin_update_other": "Updates available for {{count}} plugins!" "plugin_update_other": "Updates available for {{count}} plugins!"
}, },
"PluginView": { "PluginView": {
"hidden_one": "1 plugin is hidden from this list", "hidden_one": "1 plugin is hidden from this list",
"hidden_other": "{{count}} plugins are hidden from this list" "hidden_other": "{{count}} plugins are hidden from this list",
"disabled_one": "1 plugin is disabled",
"disabled_other": "{{count}} plugins are disabled"
}, },
"RemoteDebugging": { "RemoteDebugging": {
"remote_cef": { "remote_cef": {
+27 -1
View File
@@ -52,7 +52,9 @@
"MultiplePluginsInstallModal": { "MultiplePluginsInstallModal": {
"confirm": "¿Estás seguro de que quieres hacer las siguientes modificaciones?", "confirm": "¿Estás seguro de que quieres hacer las siguientes modificaciones?",
"description": { "description": {
"downgrade": "Downgrade {{name}} a la {{version}}",
"install": "Instalar {{name}} {{version}}", "install": "Instalar {{name}} {{version}}",
"overwrite": "Sobrescribir {{name}} con {{version}}",
"reinstall": "Reinstalar {{name}} {{version}}", "reinstall": "Reinstalar {{name}} {{version}}",
"update": "Actualizar {{name}} a {{version}}" "update": "Actualizar {{name}} a {{version}}"
}, },
@@ -61,12 +63,18 @@
"loading": "Trabajando" "loading": "Trabajando"
}, },
"title": { "title": {
"downgrade_many": "Downgrade {{count}} plugins",
"downgrade_one": "Downgrade 1 plugin",
"downgrade_other": "Downgrade {{count}} plugins",
"install_many": "Instalar {{count}} plugins", "install_many": "Instalar {{count}} plugins",
"install_one": "Instalar 1 plugin", "install_one": "Instalar 1 plugin",
"install_other": "Instalar {{count}} plugins", "install_other": "Instalar {{count}} plugins",
"mixed_many": "Modificar {{count}} plugins", "mixed_many": "Modificar {{count}} plugins",
"mixed_one": "Modificar 1 plugin", "mixed_one": "Modificar 1 plugin",
"mixed_other": "Modificar {{count}} plugins", "mixed_other": "Modificar {{count}} plugins",
"overwrite_many": "Sobrescribir {{count}} plugins",
"overwrite_one": "Sobrescribir 1 plugin",
"overwrite_other": "Sobrescribr {{count}} plugins",
"reinstall_many": "Reinstalar {{count}} plugins", "reinstall_many": "Reinstalar {{count}} plugins",
"reinstall_one": "Reinstalar 1 plugin", "reinstall_one": "Reinstalar 1 plugin",
"reinstall_other": "Reinstalar {{count}} plugins", "reinstall_other": "Reinstalar {{count}} plugins",
@@ -76,12 +84,22 @@
} }
}, },
"PluginCard": { "PluginCard": {
"plugin_downgrade": "Downgrade",
"plugin_full_access": "Este plugin tiene acceso completo a tu Steam Deck.", "plugin_full_access": "Este plugin tiene acceso completo a tu Steam Deck.",
"plugin_install": "Instalar", "plugin_install": "Instalar",
"plugin_no_desc": "No se ha proporcionado una descripción.", "plugin_no_desc": "No se ha proporcionado una descripción.",
"plugin_overwrite": "Sobrescribir",
"plugin_reinstall": "Reinstalar",
"plugin_update": "Actualizar",
"plugin_version_label": "Versión de Plugin" "plugin_version_label": "Versión de Plugin"
}, },
"PluginInstallModal": { "PluginInstallModal": {
"downgrade": {
"button_idle": "Downgrade",
"button_processing": "Downgrading",
"desc": "¿Estas seguro de que quieres realizar el downgrade de {{artifact}} a la versión {{version}}?",
"title": "Downgrade {{artifact}}"
},
"install": { "install": {
"button_idle": "Instalar", "button_idle": "Instalar",
"button_processing": "Instalando", "button_processing": "Instalando",
@@ -89,6 +107,13 @@
"title": "Instalar {{artifact}}" "title": "Instalar {{artifact}}"
}, },
"no_hash": "Este plugin no tiene un hash, lo estás instalando bajo tu propia responsabilidad.", "no_hash": "Este plugin no tiene un hash, lo estás instalando bajo tu propia responsabilidad.",
"not_installed": "(no instalado)",
"overwrite": {
"button_idle": "Sobrescribir",
"button_processing": "Sobrescribiendo",
"desc": "¿Estas seguro de que quieres sobrescribir {{artifact}} con la versión {{version}}?",
"title": "Sobrescribir {{artifact}}"
},
"reinstall": { "reinstall": {
"button_idle": "Reinstalar", "button_idle": "Reinstalar",
"button_processing": "Reinstalando", "button_processing": "Reinstalando",
@@ -98,7 +123,7 @@
"update": { "update": {
"button_idle": "Actualizar", "button_idle": "Actualizar",
"button_processing": "Actualizando", "button_processing": "Actualizando",
"desc": "¿Estás seguro de que quieres actualizar {{artifact}} {{version}}?", "desc": "¿Estás seguro de que quieres actualizar {{artifact}} a la {{version}}?",
"title": "Actualizar {{artifact}}" "title": "Actualizar {{artifact}}"
} }
}, },
@@ -206,6 +231,7 @@
}, },
"Store": { "Store": {
"download_progress_info": { "download_progress_info": {
"download_remote": "Descargando los binarios externos",
"download_zip": "Descargando plugin", "download_zip": "Descargando plugin",
"increment_count": "Incrementando el contador de descargas", "increment_count": "Incrementando el contador de descargas",
"installing_plugin": "Instalando plugin", "installing_plugin": "Instalando plugin",
+9 -9
View File
@@ -52,7 +52,7 @@
"MultiplePluginsInstallModal": { "MultiplePluginsInstallModal": {
"confirm": "Sei sicuro di voler effettuare le modifiche seguenti?", "confirm": "Sei sicuro di voler effettuare le modifiche seguenti?",
"description": { "description": {
"downgrade": "Downgrada {{name}} a versione {{version}}", "downgrade": "Declassa {{name}} a versione {{version}}",
"install": "Installa {{name}} {{version}}", "install": "Installa {{name}} {{version}}",
"overwrite": "Sovrascrive {{name}} con {{version}}", "overwrite": "Sovrascrive {{name}} con {{version}}",
"reinstall": "Reinstalla {{name}} {{version}}", "reinstall": "Reinstalla {{name}} {{version}}",
@@ -63,9 +63,9 @@
"loading": "Elaboro" "loading": "Elaboro"
}, },
"title": { "title": {
"downgrade_many": "Downgrada {{count}} plugins", "downgrade_many": "Declassa {{count}} plugins",
"downgrade_one": "Downgrada un plugin", "downgrade_one": "Declassa un plugin",
"downgrade_other": "Downgrada {{count}} plugins", "downgrade_other": "Declassa {{count}} plugins",
"install_many": "Installa {{count}} plugins", "install_many": "Installa {{count}} plugins",
"install_one": "Installa un plugin", "install_one": "Installa un plugin",
"install_other": "Installa {{count}} plugins", "install_other": "Installa {{count}} plugins",
@@ -84,7 +84,7 @@
} }
}, },
"PluginCard": { "PluginCard": {
"plugin_downgrade": "Downgrada", "plugin_downgrade": "Declassa",
"plugin_full_access": "Questo plugin ha accesso completo al tuo Steam Deck.", "plugin_full_access": "Questo plugin ha accesso completo al tuo Steam Deck.",
"plugin_install": "Installa", "plugin_install": "Installa",
"plugin_no_desc": "Nessuna descrizione fornita.", "plugin_no_desc": "Nessuna descrizione fornita.",
@@ -95,10 +95,10 @@
}, },
"PluginInstallModal": { "PluginInstallModal": {
"downgrade": { "downgrade": {
"button_idle": "Downgrada", "button_idle": "Declassa",
"button_processing": "Downgradando", "button_processing": "Declassando",
"desc": "Sei sicuro di voler downgradare {{artifact}} alla versione {{version}}?", "desc": "Sei sicuro di voler declassare {{artifact}} alla versione {{version}}?",
"title": "Downgrada {{artifact}}" "title": "Declassa {{artifact}}"
}, },
"install": { "install": {
"button_idle": "Installa", "button_idle": "Installa",
+1 -1
View File
@@ -215,7 +215,7 @@
"download_progress_info": { "download_progress_info": {
"download_remote": "外部バイナリのダウンロード", "download_remote": "外部バイナリのダウンロード",
"download_zip": "プラグインのダウンロード中", "download_zip": "プラグインのダウンロード中",
"increment_count": "ダウンロード数の増加", "increment_count": "ダウンロード進行中",
"installing_plugin": "プラグインのインストール中", "installing_plugin": "プラグインのインストール中",
"open_zip": "zipファイルを展開中", "open_zip": "zipファイルを展開中",
"parse_zip": "zipファイルの解析中", "parse_zip": "zipファイルの解析中",
+316
View File
@@ -0,0 +1,316 @@
{
"BranchSelect": {
"update_channel": {
"label": "Candalul de Actualizare",
"prerelease": "Pre-lansare",
"stable": "Stabil",
"testing": "Testare"
}
},
"Developer": {
"5secreload": "Reîncărcare în 5 secunde",
"disabling": "Dezactivare React DevTools",
"enabling": "Activare React DevTools"
},
"DropdownMultiselect": {
"button": {
"back": "Înapoi"
}
},
"FilePickerError": {
"errors": {
"file_not_found": "Calea specificată nu este validă. Vă rugăm să o verificați și să o reintroduceți corect.",
"perm_denied": "Nu aveți acces la fișierul specificat. Te rugăm să verifici dacă utilizatorul tău (deck pe Steam Deck) are permisiunea corespunzătoare de a accesa folderul/fișierul specific.",
"unknown": "A apărut o eroare necunoscută. Eroarea brută este: {{raw_error}}"
}
},
"FilePickerIndex": {
"file": {
"select": "Selectați acest fișier"
},
"files": {
"all_files": "Toate Fișierele",
"file_type": "Tip de Fișier",
"show_hidden": "Afișați Fișierele Ascunse"
},
"filter": {
"created_asce": "Create (Cele mai vechi)",
"created_desc": "Create (Cele mai noi)",
"modified_asce": "Modificate (Cele mai vechi)",
"modified_desc": "Modificate (Cele mai noi)",
"name_asce": "Z-A",
"name_desc": "A-Z",
"size_asce": "Dimensiune (Cea mai mică)",
"size_desc": "Dimensiune (Cea mai mare)"
},
"folder": {
"label": "Fișier",
"select": "Folosiți acest fișier",
"show_more": "Afișați mai multe fișiere"
}
},
"MultiplePluginsInstallModal": {
"confirm": "Sigur doriți să faceți următoarele modificări?",
"description": {
"downgrade": "Retrogradare {{name}} la {{version}}",
"install": "Instalați {{name}} {{version}}",
"overwrite": "Suprascrie {{name}} cu {{version}}",
"reinstall": "Reinstalați {{name}} {{version}}",
"update": "Actualizați {{name}} la {{version}}"
},
"ok_button": {
"idle": "Confirmați",
"loading": "Încărcare"
},
"title": {
"downgrade_few": "Retrogradează {{count}} pluginuri",
"downgrade_one": "Retrogradează un plugin",
"downgrade_other": "Retrogradează {{count}} pluginuri",
"install_few": "Instalați {{count}} pluginuri",
"install_one": "Instalați un plugin",
"install_other": "Instalați {{count}} pluginuri",
"mixed_few": "Modificați {{count}} pluginuri",
"mixed_one": "Modificați un plugin",
"mixed_other": "Modificați {{count}} pluginuri",
"overwrite_few": "Suprascrie {{count}} pluginuri",
"overwrite_one": "Suprascrie un plugin",
"overwrite_other": "Suprascrie {{count}} pluginuri",
"reinstall_few": "Reinstalați {{count}} pluginuri",
"reinstall_one": "Reinstalați un plugin",
"reinstall_other": "Reinstalați {{count}} pluginuri",
"update_few": "Actualizați {{count}} pluginuri",
"update_one": "Actualizați un plugin",
"update_other": "Actualizați {{count}} pluginuri"
}
},
"PluginCard": {
"plugin_downgrade": "Retrogradare",
"plugin_full_access": "Acest plugin are acces complet la Steam Deck-ul tău.",
"plugin_install": "Instalați",
"plugin_no_desc": "Nici-o descriere pusă.",
"plugin_overwrite": "Suprascriere",
"plugin_reinstall": "Reinstalați",
"plugin_update": "Actualizare",
"plugin_version_label": "Versiunea Plugin"
},
"PluginInstallModal": {
"downgrade": {
"button_idle": "Retrogradare",
"button_processing": "Retrogradare",
"desc": "Sunteți sigur că vreți să retrogradați {{artifact}} la versiunea {{version}}?",
"title": "Retrogradare {{artifact}}"
},
"install": {
"button_idle": "Instalați",
"button_processing": "Instalare",
"desc": "Sigur vreți să instalați {{artifact}} {{version}}?",
"title": "Instalați {{artifact}}"
},
"no_hash": "Acest plugin nu are hash, îl instalați pe propriul risc.",
"not_installed": "(neinstalat)",
"overwrite": {
"button_idle": "Suprascriere",
"button_processing": "Suprascriere",
"desc": "Sigur vreți să suprascrii {{artifact}} cu versiunea {{version}}?",
"title": "Suprascrie {{artifact}}"
},
"reinstall": {
"button_idle": "Reinstalare",
"button_processing": "Reinstalare",
"desc": "Sigur vreți să reinstalați {{artifact}} {{version}}?",
"title": "Reinstalare {{artifact}}"
},
"update": {
"button_idle": "Actualizare",
"button_processing": "Se Actualizează",
"desc": "Sigur vreți să actualizați {{artifact}} la versiunea {{version}}?",
"title": "Actualizare {{artifact}}"
}
},
"PluginListIndex": {
"freeze": "Pauză actualizări",
"hide": "Acces rapid: Ascunde",
"no_plugin": "Niciun plugin instalat!",
"plugin_actions": "Acțiuni plugin",
"reinstall": "Reinstalare",
"reload": "Reîncărcare",
"show": "Acces rapid: Afișare",
"unfreeze": "Permiteți actualizări",
"uninstall": "Dezinstalare",
"update_all_few": "Actualizați {{count}} pluginuri",
"update_all_one": "Actualizare un plugin",
"update_all_other": "Actualizați {{count}} pluginuri",
"update_to": "Actualizare pentru {{name}}"
},
"PluginListLabel": {
"hidden": "Ascuns din meniul de acces rapid"
},
"PluginLoader": {
"decky_title": "Decky",
"decky_update_available": "Actualizare disponibilă pentru {{tag_name}}!",
"error": "Eroare",
"plugin_error_uninstall": "Încărcarea pluginului {{name}} a cauzat o excepție, așa cum se arată mai sus. Aceasta înseamnă de obicei că pluginul necesită o actualizare pentru noua versiune de SteamUI. Verificați dacă există o actualizare sau evaluați eliminarea acesteia în setările Decky, în secțiunea Plugin-uri.",
"plugin_load_error": {
"message": "Eroare la încărcarea pluginului {{name}}",
"toast": "Eroare la încărcarea {{name}}"
},
"plugin_uninstall": {
"button": "Dezinstalare",
"desc": "Sigur vreți să dezinstalați {{name}}?",
"title": "Dezinstalați {{name}}"
},
"plugin_update_few": "Actualizări disponibile pentru {{count}} plugin-uri!",
"plugin_update_one": "Actualizare disponibilă pentru un plugin!",
"plugin_update_other": "Actualizări disponibile pentru {{count}} plugin-uri!"
},
"PluginView": {
"hidden_few": "{{count}} pluginuri sunt ascunse din această listă",
"hidden_one": "Un plugin este ascuns din această listă",
"hidden_other": "{{count}} pluginuri sunt ascunse din această listă"
},
"RemoteDebugging": {
"remote_cef": {
"desc": "Permiteți accesul neautentificat la debugger CEF oricui din rețeaua dumneavoastră",
"label": "Permiteți Deugging CEF la distanță"
}
},
"SettingsDeveloperIndex": {
"cef_console": {
"button": "Deschideți Consola",
"desc": "Deschide consola CEF. Utilă doar în scopuri de debugging. Informațiile de aici sunt potențial periculoase și ar trebui utilizate doar dacă ești dezvoltator de plugin-uri sau dacă ești îndrumat aici de unul dintre ei.",
"label": "Consola CEF"
},
"header": "Alte",
"react_devtools": {
"desc": "Permite conectarea la un calculator care rulează React DevTools. Modificarea acestei setări va reîncărca Steam. Setați adresa IP înainte de activare.",
"ip_label": "IP",
"label": "Activați React DevTools"
},
"third_party_plugins": {
"button_install": "Instalați",
"button_zip": "Răsfoiți",
"header": "Pluginuri terțe",
"label_desc": "URL",
"label_url": "Instalați pluginul de la URL",
"label_zip": "Instalați pluginul din fișierul ZIP"
},
"valve_internal": {
"desc1": "Activează meniul intern pentru dezvoltatori Valve.",
"desc2": "Nu atingeți nimic din acest meniu decât dacă știți ce face.",
"label": "Activare Valve Internă"
}
},
"SettingsGeneralIndex": {
"about": {
"decky_version": "Versiunea Decky",
"header": "Despre"
},
"beta": {
"header": "Participarea la beta"
},
"developer_mode": {
"label": "Mod dezvoltator"
},
"notifications": {
"decky_updates_label": "Actualizare Decky disponibilă",
"header": "Notificări",
"plugin_updates_label": "Actualizări de pluginuri disponibile"
},
"other": {
"header": "Alte"
},
"updates": {
"header": "Actualizări"
}
},
"SettingsIndex": {
"developer_title": "Dezvoltator",
"general_title": "General",
"plugins_title": "Pluginuri",
"testing_title": "Testare"
},
"Store": {
"download_progress_info": {
"download_remote": "Descărcarea oricăror binări externe",
"download_zip": "Se descarcă pluginul",
"increment_count": "Se mărește numărul de descărcări",
"installing_plugin": "Se instalează plugin",
"open_zip": "Deschiderea fișierului zip",
"parse_zip": "Analizarea fișierului zip",
"start": "Inițializare",
"uninstalling_previous": "Se dezinstalează copia anterioară"
},
"store_contrib": {
"desc": "Dacă doriți să contribuiți la Magazinul de Pluginuri Decky, verificați depozitul SteamDeckHomebrew/decky-plugin-template de pe GitHub. Informații despre dezvoltare și distribuție sunt disponibile în fișierul README.",
"label": "Contribuţii"
},
"store_filter": {
"label": "Filtru",
"label_def": "Toate"
},
"store_search": {
"label": "Căutare"
},
"store_sort": {
"label": "Sortează",
"label_def": "Ultima actualizare (cele mai noi)"
},
"store_source": {
"desc": "Tot codul sursă al pluginului este disponibil în depozitul SteamDeckHomebrew/decky-plugin-database de pe GitHub.",
"label": "Cod sursă"
},
"store_tabs": {
"about": "Despre",
"alph_asce": "Alfabetic (de la Z la A)",
"alph_desc": "Alfabetic (de la A la Z)",
"date_asce": "Cele mai vechi primele",
"date_desc": "Cele mai noi primele",
"downloads_asce": "Cele mai puțin descărcate primele",
"downloads_desc": "Cele mai descărcate primele",
"title": "Răsfoiți"
},
"store_testing_cta": "Vă rugăm să luați în considerare testarea la noi plugin-uri pentru a ajuta echipa Decky Loader!",
"store_testing_warning": {
"desc": "Puteți folosi acest canal de magazin pentru a testa versiuni de pluginuri de ultimă generație. Asigură-te că lași feedback pe GitHub, astfel încât pluginul să poată fi actualizat pentru toți utilizatorii.",
"label": "Bun venit la Canalul Magazinului de Testare"
}
},
"StoreSelect": {
"custom_store": {
"label": "Magazin personalizat",
"url_label": "URL"
},
"store_channel": {
"custom": "Personalizat",
"default": "Standard",
"label": "Canalul Magazinului",
"testing": "Testare"
}
},
"Testing": {
"download": "Descărcare",
"error": "Eroare la instalarea PR-ului",
"header": "Următoarele versiuni de Decky Loader sunt construite pe baza unor solicitări de extragere (Pull Requests) deschise de la terți. Echipa Decky Loader nu a verificat funcționalitatea sau securitatea acestora și este posibil să fie învechite.",
"loading": "Se încarcă cererile de extragere deschise...",
"start_download_toast": "Se descarcă PR #{{id}}"
},
"TitleView": {
"decky_store_desc": "Deschide Magazinul Decky",
"settings_desc": "Deschide Setările Decky"
},
"Updater": {
"decky_updates": "Actualizări Decky",
"no_patch_notes_desc": "Nu există note de patch pentru această versiune",
"patch_notes_desc": "Note de patch",
"updates": {
"check_button": "Verificare pentru actualizări",
"checking": "Verificare",
"cur_version": "Versiunea curentă: {{ver}}",
"install_button": "Instalați actualizarea",
"label": "Actualizări",
"lat_version": "La zi cu actualizările: rulează {{ver}}",
"reloading": "Reîncărcare",
"updating": "Actualizare"
}
}
}
+14 -2
View File
@@ -63,15 +63,18 @@
"loading": "В процессе" "loading": "В процессе"
}, },
"title": { "title": {
"downgrade_few": "Откатить {{count}} плагинов", "downgrade_few": "Откатить {{count}} плагина",
"downgrade_many": "Откатить {{count}} плагинов", "downgrade_many": "Откатить {{count}} плагинов",
"downgrade_one": "Откатить 1 плагин", "downgrade_one": "Откатить {{count}} плагин",
"install_few": "Установить {{count}} плагинов", "install_few": "Установить {{count}} плагинов",
"install_many": "Установить {{count}} плагинов", "install_many": "Установить {{count}} плагинов",
"install_one": "Установить {{count}} плагин", "install_one": "Установить {{count}} плагин",
"mixed_few": "Изменить {{count}} плагинов", "mixed_few": "Изменить {{count}} плагинов",
"mixed_many": "Изменить {{count}} плагинов", "mixed_many": "Изменить {{count}} плагинов",
"mixed_one": "Изменить {{count}} плагин", "mixed_one": "Изменить {{count}} плагин",
"overwrite_few": "Перезаписать {{count}} плагина",
"overwrite_many": "Перезаписать {{count}} плагинов",
"overwrite_one": "Перезаписать {{count}} плагин",
"reinstall_few": "Переустановить {{count}} плагинов", "reinstall_few": "Переустановить {{count}} плагинов",
"reinstall_many": "Переустановить {{count}} плагинов", "reinstall_many": "Переустановить {{count}} плагинов",
"reinstall_one": "Переустановить {{count}} плагин", "reinstall_one": "Переустановить {{count}} плагин",
@@ -93,6 +96,7 @@
"PluginInstallModal": { "PluginInstallModal": {
"downgrade": { "downgrade": {
"button_idle": "Откат", "button_idle": "Откат",
"button_processing": "Откатывание",
"desc": "Вы уверенны, что хотите откатить {{artifact}} до версии {{version}}?", "desc": "Вы уверенны, что хотите откатить {{artifact}} до версии {{version}}?",
"title": "Откатить {{artifact}}" "title": "Откатить {{artifact}}"
}, },
@@ -103,6 +107,13 @@
"title": "Установить {{artifact}}" "title": "Установить {{artifact}}"
}, },
"no_hash": "У данного плагина отсутствует хэш, устанавливайте на свой страх и риск.", "no_hash": "У данного плагина отсутствует хэш, устанавливайте на свой страх и риск.",
"not_installed": "(не установлено)",
"overwrite": {
"button_idle": "Перезаписать",
"button_processing": "Перезаписывание",
"desc": "Вы уверены, что хотите перезаписать {{artifact}} версией {{version}}?",
"title": "Перезаписать {{artifact}}"
},
"reinstall": { "reinstall": {
"button_idle": "Переустановить", "button_idle": "Переустановить",
"button_processing": "Переустановка", "button_processing": "Переустановка",
@@ -220,6 +231,7 @@
}, },
"Store": { "Store": {
"download_progress_info": { "download_progress_info": {
"download_remote": "Загрузка внешних бинарных файлов",
"download_zip": "Скачивание плагина", "download_zip": "Скачивание плагина",
"increment_count": "Увеличение количества загрузок", "increment_count": "Увеличение количества загрузок",
"installing_plugin": "Установка плагина", "installing_plugin": "Установка плагина",
+111 -17
View File
@@ -12,15 +12,49 @@
"disabling": "Вимкнення React DevTools", "disabling": "Вимкнення React DevTools",
"enabling": "Увімкнення React DevTools" "enabling": "Увімкнення React DevTools"
}, },
"DropdownMultiselect": {
"button": {
"back": "Назад"
}
},
"FilePickerError": {
"errors": {
"file_not_found": "Вказаний шлях недійсний. Будь ласка, перевірте його та введіть правильно.",
"perm_denied": "У вас немає доступу до вказаного каталогу. Будь ласка, перевірте, чи має ваш користувач (deck на Steam Deck) відповідні права доступу до зазначеної папки/файлу.",
"unknown": "Сталася невідома помилка. Деталі помилки: {{raw_error}}"
}
},
"FilePickerIndex": { "FilePickerIndex": {
"file": {
"select": "Виберіть цей файл"
},
"files": {
"all_files": "Усі файли",
"file_type": "Тип файлу",
"show_hidden": "Показати приховані файли"
},
"filter": {
"created_asce": "Створено (спочатку найстаріші)",
"created_desc": "Створено (спочатку найновіші)",
"modified_asce": "Змінено (спочатку найстаріші)",
"modified_desc": "Змінено (спочатку найновіші)",
"name_asce": "Я-А",
"name_desc": "А-Я",
"size_asce": "Розмір (спочатку найменші)",
"size_desc": "Розмір (спочатку найбільші)"
},
"folder": { "folder": {
"select": "Використовувати цю папку" "label": "Папка",
"select": "Використовувати цю папку",
"show_more": "Показати більше файлів"
} }
}, },
"MultiplePluginsInstallModal": { "MultiplePluginsInstallModal": {
"confirm": "Ви впевнені, що хочете застосувати такі модифікації?", "confirm": "Ви впевнені, що хочете застосувати такі модифікації?",
"description": { "description": {
"downgrade": "Понизити {{name}} до версії {{version}}",
"install": "Встановити {{name}} {{version}}", "install": "Встановити {{name}} {{version}}",
"overwrite": "Перезаписати {{name}} версією {{version}}",
"reinstall": "Перевстановити {{name}} {{version}}", "reinstall": "Перевстановити {{name}} {{version}}",
"update": "Оновити {{name}} до {{version}}" "update": "Оновити {{name}} до {{version}}"
}, },
@@ -29,27 +63,43 @@
"loading": "Опрацювання" "loading": "Опрацювання"
}, },
"title": { "title": {
"install_few": "Встановити {{count}} плагінів", "downgrade_few": "Понизити {{count}} плагіни",
"downgrade_many": "Понизити {{count}} плагінів",
"downgrade_one": "Понизити {{count}} плагін",
"install_few": "Встановити {{count}} плагіни",
"install_many": "Встановити {{count}} плагінів", "install_many": "Встановити {{count}} плагінів",
"install_one": "Встановити 1 плагін", "install_one": "Встановити {{count}} плагін",
"mixed_few": "Модифікувати {{count}} плагінів", "mixed_few": "Модифікувати {{count}} плагіни",
"mixed_many": "", "mixed_many": "Модифікувати {{count}} плагінів",
"mixed_one": "Модифікувати 1 плагін", "mixed_one": "Модифікувати {{count}} плагін",
"reinstall_few": "Перевстановити {{count}} плагінів", "overwrite_few": "Перезаписати {{count}} плагіни",
"overwrite_many": "Перезаписати {{count}} плагінів",
"overwrite_one": "Перезаписати {{count}} плагін",
"reinstall_few": "Перевстановити {{count}} плагіни",
"reinstall_many": "Перевстановити {{count}} плагінів", "reinstall_many": "Перевстановити {{count}} плагінів",
"reinstall_one": "Перевстановити 1 плагін", "reinstall_one": "Перевстановити {{count}} плагін",
"update_few": "Оновити {{count}} плагінів", "update_few": "Оновити {{count}} плагіни",
"update_many": "Оновити {{count}} плагінів", "update_many": "Оновити {{count}} плагінів",
"update_one": "Оновити 1 плагін" "update_one": "Оновити {{count}} плагін"
} }
}, },
"PluginCard": { "PluginCard": {
"plugin_downgrade": "Понизити",
"plugin_full_access": "Цей плагін має повний доступ до вашого Steam Deck.", "plugin_full_access": "Цей плагін має повний доступ до вашого Steam Deck.",
"plugin_install": "Встановити", "plugin_install": "Встановити",
"plugin_no_desc": "Опис не надано.", "plugin_no_desc": "Опис не надано.",
"plugin_overwrite": "Перезаписати",
"plugin_reinstall": "Перевстановити",
"plugin_update": "Оновити",
"plugin_version_label": "Версія плагіна" "plugin_version_label": "Версія плагіна"
}, },
"PluginInstallModal": { "PluginInstallModal": {
"downgrade": {
"button_idle": "Понизити",
"button_processing": "Пониження",
"desc": "Ви впевнені, що хочете понизити {{artifact}} до версії {{version}}?",
"title": "Понизити {{artifact}}"
},
"install": { "install": {
"button_idle": "Встановити", "button_idle": "Встановити",
"button_processing": "Встановлення", "button_processing": "Встановлення",
@@ -57,6 +107,13 @@
"title": "Встановити {{artifact}}" "title": "Встановити {{artifact}}"
}, },
"no_hash": "Цей плагін не має хешу, ви встановлюєте його на власний ризик.", "no_hash": "Цей плагін не має хешу, ви встановлюєте його на власний ризик.",
"not_installed": "(не встановлено)",
"overwrite": {
"button_idle": "Перезаписати",
"button_processing": "Перезаписування",
"desc": "Ви впевнені, що хочете перезаписати {{artifact}} версією {{version}}?",
"title": "Перезаписати {{artifact}}"
},
"reinstall": { "reinstall": {
"button_idle": "Перевстановити", "button_idle": "Перевстановити",
"button_processing": "Перевстановлення", "button_processing": "Перевстановлення",
@@ -66,21 +123,23 @@
"update": { "update": {
"button_idle": "Оновити", "button_idle": "Оновити",
"button_processing": "Оновлення", "button_processing": "Оновлення",
"desc": "Ви впевнені, що хочете оновити {{artifact}} {{version}}?", "desc": "Ви впевнені, що хочете оновити {{artifact}} до версії {{version}}?",
"title": "Оновити {{artifact}}" "title": "Оновити {{artifact}}"
} }
}, },
"PluginListIndex": { "PluginListIndex": {
"freeze": "Заморозити оновлення",
"hide": "Швидкий доступ: Приховати", "hide": "Швидкий доступ: Приховати",
"no_plugin": "Плагінів не встановлено!", "no_plugin": "Плагінів не встановлено!",
"plugin_actions": "Дії плагінів", "plugin_actions": "Дії плагінів",
"reinstall": "Перевстановити", "reinstall": "Перевстановити",
"reload": "Перезавантажити", "reload": "Перезавантажити",
"show": "Швидкий доступ: Показати", "show": "Швидкий доступ: Показати",
"unfreeze": "Дозволити оновлення",
"uninstall": "Видалити", "uninstall": "Видалити",
"update_all_few": "Оновити {{count}} плагінів", "update_all_few": "Оновити {{count}} плагіни",
"update_all_many": "Оновити {{count}} плагінів", "update_all_many": "Оновити {{count}} плагінів",
"update_all_one": "Оновити 1 плагін", "update_all_one": "Оновити {{count}} плагін",
"update_to": "Оновити {{name}}" "update_to": "Оновити {{name}}"
}, },
"PluginListLabel": { "PluginListLabel": {
@@ -100,9 +159,9 @@
"desc": "Ви впевнені, що хочете видалити {{name}}?", "desc": "Ви впевнені, що хочете видалити {{name}}?",
"title": "Видалити {{name}}" "title": "Видалити {{name}}"
}, },
"plugin_update_few": "Доступне оновлення для {{count}} плагінів!", "plugin_update_few": "Доступне оновлення для {{count}} плагіни!",
"plugin_update_many": "Доступне оновлення для {{count}} плагінів!", "plugin_update_many": "Доступне оновлення для {{count}} плагінів!",
"plugin_update_one": "Доступне оновлення для 1 плагіна!" "plugin_update_one": "Доступне оновлення для {{count}} плагіна!"
}, },
"PluginView": { "PluginView": {
"hidden_few": "{{count}} плагінів приховано з цього списку", "hidden_few": "{{count}} плагінів приховано з цього списку",
@@ -152,6 +211,11 @@
"developer_mode": { "developer_mode": {
"label": "Розробницький режим" "label": "Розробницький режим"
}, },
"notifications": {
"decky_updates_label": "Доступне оновлення Decky",
"header": "Сповіщення",
"plugin_updates_label": "Доступні оновлення плагінів"
},
"other": { "other": {
"header": "Інше" "header": "Інше"
}, },
@@ -162,9 +226,20 @@
"SettingsIndex": { "SettingsIndex": {
"developer_title": "Розробник", "developer_title": "Розробник",
"general_title": "Загальне", "general_title": "Загальне",
"plugins_title": "Плагіни" "plugins_title": "Плагіни",
"testing_title": "Тестування"
}, },
"Store": { "Store": {
"download_progress_info": {
"download_remote": "Завантаження будь-яких зовнішніх бінарних файлів",
"download_zip": "Завантаження плагіна",
"increment_count": "Збільшення лічильника завантажень",
"installing_plugin": "Встановлення плагіна",
"open_zip": "Відкриття ZIP-файлу",
"parse_zip": "Обробка ZIP-файлу",
"start": "Ініціалізація",
"uninstalling_previous": "Видалення попередньої копії"
},
"store_contrib": { "store_contrib": {
"desc": "Якщо ви бажаєте додати щось у Decky Plugin Store, завітайте у репозиторій SteamDeckHomebrew/decky-plugin-template на GitHub. Інформація про розробку та поширення доступна у README.", "desc": "Якщо ви бажаєте додати щось у Decky Plugin Store, завітайте у репозиторій SteamDeckHomebrew/decky-plugin-template на GitHub. Інформація про розробку та поширення доступна у README.",
"label": "Зробити внесок" "label": "Зробити внесок"
@@ -188,9 +263,17 @@
"about": "Інформація", "about": "Інформація",
"alph_asce": "За алфавітом (Z до A)", "alph_asce": "За алфавітом (Z до A)",
"alph_desc": "За алфавітом (A до Z)", "alph_desc": "За алфавітом (A до Z)",
"date_asce": "Спочатку найстаріші",
"date_desc": "Спочатку найновіші",
"downloads_asce": "Спочатку найменш завантажені",
"downloads_desc": "Спочатку найчастіше завантажувані",
"title": "Огляд" "title": "Огляд"
}, },
"store_testing_cta": "Розгляньте можливість тестування нових плагінів, щоб допомогти команді Decky Loader!" "store_testing_cta": "Розгляньте можливість тестування нових плагінів, щоб допомогти команді Decky Loader!",
"store_testing_warning": {
"desc": "Ви можете використовувати цей канал магазину для тестування найновіших (експериментальних) версій плагінів. Обов’язково залишайте відгук на GitHub, щоб плагін можна було оновити для всіх користувачів.",
"label": "Ласкаво просимо до каналу тестування магазину"
}
}, },
"StoreSelect": { "StoreSelect": {
"custom_store": { "custom_store": {
@@ -204,6 +287,17 @@
"testing": "Тестування" "testing": "Тестування"
} }
}, },
"Testing": {
"download": "Завантажити",
"error": "Помилка встановлення PR",
"header": "Наведені нижче версії Decky Loader зібрані з відкритих сторонніх Pull Request. Команда Decky Loader не перевіряла їхню функціональність або безпеку, і вони можуть бути застарілими.",
"loading": "Завантаження відкритих Pull Request...",
"start_download_toast": "Завантаження PR #{{id}}"
},
"TitleView": {
"decky_store_desc": "Відкрити Decky Store",
"settings_desc": "Відкрити налаштування Decky"
},
"Updater": { "Updater": {
"decky_updates": "Оновлення Decky", "decky_updates": "Оновлення Decky",
"no_patch_notes_desc": "Немає нотаток до цієї версії", "no_patch_notes_desc": "Немає нотаток до цієї версії",
@@ -1,11 +1,15 @@
import platform, os import platform, os
ON_WINDOWS = platform.system() == "Windows" ON_WINDOWS = platform.system() == "Windows"
ON_LINUX = not ON_WINDOWS ON_MAC = platform.system() == "Darwin"
ON_LINUX = not ON_WINDOWS and not ON_MAC
if ON_WINDOWS: if ON_WINDOWS:
from .localplatformwin import * from .localplatformwin import *
from . import localplatformwin as localplatform from . import localplatformwin as localplatform
elif ON_MAC:
from .localplatformmac import *
from . import localplatformmac as localplatform
else: else:
from .localplatformlinux import * from .localplatformlinux import *
from . import localplatformlinux as localplatform from . import localplatformlinux as localplatform
@@ -116,28 +116,26 @@ def get_username() -> str:
return _get_user() return _get_user()
def setgid(user : UserType = UserType.HOST_USER): def setgid(user : UserType = UserType.HOST_USER):
user_id = 0 host_user_group_id, effective_user_group_id = _get_user_group_id(), _get_effective_user_group_id()
if host_user_group_id == effective_user_group_id:
if user == UserType.HOST_USER: pass
user_id = _get_user_group_id() elif user == UserType.HOST_USER:
os.setgid(host_user_group_id)
elif user == UserType.EFFECTIVE_USER: elif user == UserType.EFFECTIVE_USER:
pass # we already are os.setgid(effective_user_group_id)
else: else:
raise Exception("Unknown user type") raise Exception("Unknown user type")
os.setgid(user_id)
def setuid(user : UserType = UserType.HOST_USER): def setuid(user : UserType = UserType.HOST_USER):
user_id = 0 host_user_id, effective_user_id = _get_user_id(), _get_effective_user_id()
if host_user_id == effective_user_id:
if user == UserType.HOST_USER: pass
user_id = _get_user_id() elif user == UserType.HOST_USER:
os.setuid(host_user_id)
elif user == UserType.EFFECTIVE_USER: elif user == UserType.EFFECTIVE_USER:
pass # we already are os.setuid(effective_user_id)
else: else:
raise Exception("Unknown user type") raise Exception("Unknown user type")
os.setuid(user_id)
async def service_active(service_name : str) -> bool: async def service_active(service_name : str) -> bool:
res, _, _ = await run(["systemctl", "is-active", service_name], stdout=DEVNULL, stderr=DEVNULL) res, _, _ = await run(["systemctl", "is-active", service_name], stdout=DEVNULL, stderr=DEVNULL)
@@ -0,0 +1,59 @@
from ..enums import UserType
import os, sys
from . import localplatformlinux
# this should be public
def _get_effective_user_id() -> int:
return os.geteuid()
def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool = True) -> bool:
return localplatformlinux.chown(path, user, recursive)
def chmod(path : str, permissions : int, recursive : bool = True) -> bool:
return localplatformlinux.chmod(path, permissions, recursive)
def file_owner(path : str) -> UserType|None:
return localplatformlinux.file_owner(path)
def get_home_path(user : UserType = UserType.HOST_USER) -> str:
return localplatformlinux.get_home_path(user)
def setgid(user : UserType = UserType.HOST_USER):
return localplatformlinux.setgid(user)
def setuid(user : UserType = UserType.HOST_USER):
return localplatformlinux.setuid(user)
async def service_active(service_name : str) -> bool:
return True # Stubbed
async def service_stop(service_name : str) -> bool:
return True # Stubbed
async def service_start(service_name : str) -> bool:
return True # Stubbed
async def service_restart(service_name : str, block : bool = True) -> bool:
return True # Stubbed
def get_effective_username() -> str:
return localplatformlinux.get_effective_username()
def get_username() -> str:
return localplatformlinux.get_username()
def get_privileged_path() -> str:
'''On Mac, privileged_path is equal to unprivileged_path'''
return get_unprivileged_path()
def get_unprivileged_path() -> str:
return localplatformlinux.get_unprivileged_path()
def get_unprivileged_user() -> str:
return localplatformlinux.get_unprivileged_user()
async def restart_webhelper() -> bool:
return await localplatformlinux.restart_webhelper()
async def close_cef_socket():
return # Stubbed
+1
View File
@@ -41,6 +41,7 @@ class PluginWrapper:
self.author = json["author"] self.author = json["author"]
self.flags = json["flags"] self.flags = json["flags"]
self.api_version = json["api_version"] if "api_version" in json else 0 self.api_version = json["api_version"] if "api_version" in json else 0
self.disabled = False
self.passive = not path.isfile(self.file) self.passive = not path.isfile(self.file)
@@ -14,6 +14,7 @@ from ..localplatform.localsocket import LocalSocket
from ..localplatform.localplatform import setgid, setuid, get_username, get_home_path, ON_LINUX from ..localplatform.localplatform import setgid, setuid, get_username, get_home_path, ON_LINUX
from ..enums import UserType from ..enums import UserType
from .. import helpers from .. import helpers
from .. import settings # pyright: ignore [reportUnusedImport]
from typing import List, TypeVar, Any from typing import List, TypeVar, Any
+44 -4
View File
@@ -1,5 +1,5 @@
from __future__ import annotations from __future__ import annotations
from os import stat_result from os import path, stat_result
import uuid import uuid
from urllib.parse import unquote from urllib.parse import unquote
from json.decoder import JSONDecodeError from json.decoder import JSONDecodeError
@@ -8,7 +8,7 @@ import re
from traceback import format_exc from traceback import format_exc
from stat import FILE_ATTRIBUTE_HIDDEN # pyright: ignore [reportAttributeAccessIssue, reportUnknownVariableType] from stat import FILE_ATTRIBUTE_HIDDEN # pyright: ignore [reportAttributeAccessIssue, reportUnknownVariableType]
from asyncio import StreamReader, StreamWriter, start_server, gather, open_connection from asyncio import StreamReader, StreamWriter, sleep, start_server, gather, open_connection
from aiohttp import ClientSession, hdrs from aiohttp import ClientSession, hdrs
from aiohttp.web import Request, StreamResponse, Response, json_response, post from aiohttp.web import Request, StreamResponse, Response, json_response, post
from typing import TYPE_CHECKING, Callable, Coroutine, Dict, Any, List, TypedDict from typing import TYPE_CHECKING, Callable, Coroutine, Dict, Any, List, TypedDict
@@ -80,6 +80,9 @@ class Utilities:
context.ws.add_route("utilities/restart_webhelper", self.restart_webhelper) context.ws.add_route("utilities/restart_webhelper", self.restart_webhelper)
context.ws.add_route("utilities/close_cef_socket", self.close_cef_socket) context.ws.add_route("utilities/close_cef_socket", self.close_cef_socket)
context.ws.add_route("utilities/_call_legacy_utility", self._call_legacy_utility) context.ws.add_route("utilities/_call_legacy_utility", self._call_legacy_utility)
context.ws.add_route("utilities/enable_plugin", self.enable_plugin)
context.ws.add_route("utilities/disable_plugin", self.disable_plugin)
context.ws.add_route("utilities/set_all_plugins_disabled", self.set_all_plugins_disabled)
context.web_app.add_routes([ context.web_app.add_routes([
post("/methods/{method_name}", self._handle_legacy_server_method_call) post("/methods/{method_name}", self._handle_legacy_server_method_call)
@@ -214,7 +217,7 @@ class Utilities:
async def http_request_legacy(self, method: str, url: str, extra_opts: Any = {}, timeout: int | None = None): async def http_request_legacy(self, method: str, url: str, extra_opts: Any = {}, timeout: int | None = None):
async with ClientSession() as web: async with ClientSession() as web:
res = await web.request(method, url, ssl=helpers.get_ssl_context(), timeout=timeout, **extra_opts) res = await web.request(method, url, ssl=helpers.get_ssl_context(), timeout=timeout, **extra_opts) # type: ignore
text = await res.text() text = await res.text()
return { return {
"status": res.status, "status": res.status,
@@ -390,7 +393,6 @@ class Utilities:
"total": len(all), "total": len(all),
} }
# Based on https://stackoverflow.com/a/46422554/13174603 # Based on https://stackoverflow.com/a/46422554/13174603
def start_rdt_proxy(self, ip: str, port: int): def start_rdt_proxy(self, ip: str, port: int):
async def pipe(reader: StreamReader, writer: StreamWriter): async def pipe(reader: StreamReader, writer: StreamWriter):
@@ -474,3 +476,41 @@ class Utilities:
async def get_tab_id(self, name: str): async def get_tab_id(self, name: str):
return (await get_tab(name)).id return (await get_tab(name)).id
async def disable_plugin(self, name: str):
disabled_plugins: List[str] = await self.get_setting("disabled_plugins", [])
if name not in disabled_plugins:
disabled_plugins.append(name)
await self.set_setting("disabled_plugins", disabled_plugins)
await self.context.plugin_loader.plugins[name].stop()
await self.context.ws.emit("loader/disable_plugin", name)
async def enable_plugin(self, name: str):
plugin_folder = self.context.plugin_browser.find_plugin_folder(name)
assert plugin_folder is not None
plugin_dir = path.join(self.context.plugin_browser.plugin_path, plugin_folder)
if name in self.context.plugin_loader.plugins:
plugin = self.context.plugin_loader.plugins[name]
if plugin.proc and plugin.proc.is_alive():
await plugin.stop()
self.context.plugin_loader.plugins.pop(name, None)
await sleep(1)
disabled_plugins: List[str] = await self.get_setting("disabled_plugins", [])
if name in disabled_plugins:
disabled_plugins.remove(name)
await self.set_setting("disabled_plugins", disabled_plugins)
await self.context.plugin_loader.import_plugin(path.join(plugin_dir, "main.py"), plugin_folder)
async def set_all_plugins_disabled(self):
disabled_plugins: List[str] = await self.get_setting("disabled_plugins", [])
for name, _ in self.context.plugin_loader.plugins.items():
if name not in disabled_plugins:
disabled_plugins.append(name)
await self.set_setting("disabled_plugins", disabled_plugins)
+24 -159
View File
@@ -1,162 +1,27 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Inkscape (http://www.inkscape.org/) --> <svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 176.36 38">
<!-- Generator: Adobe Illustrator 29.2.1, SVG Export Plug-In . SVG Version: 2.1.0 Build 116) -->
<defs>
<style>
.st0 {
fill: #3fafa8;
}
<svg .st1 {
width="81.700577mm" fill: #fff;
height="24.334814mm" }
viewBox="0 0 81.700577 24.334814" </style>
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> </defs>
<g <rect class="st0" x="0" y="0" width="176.36" height="38" rx="19" ry="19"/>
inkscape:label="Layer 1" <g>
inkscape:groupmode="layer" <path class="st1" d="M59.4,26.66v-15.77h4.92c2.76,0,4.85.63,6.25,1.9,1.4,1.27,2.11,3.2,2.11,5.79s-.76,4.47-2.29,5.92c-1.53,1.45-3.58,2.17-6.17,2.17h-4.82ZM62.01,13.13v11.28h2.09c1.83,0,3.25-.5,4.28-1.49,1.03-.99,1.54-2.43,1.54-4.31s-.49-3.21-1.46-4.12c-.98-.91-2.41-1.37-4.31-1.37h-2.13Z"/>
id="layer1" <path class="st1" d="M80.12,26.92c-1.78,0-3.2-.52-4.25-1.57-1.05-1.05-1.57-2.46-1.57-4.23,0-1.8.56-3.24,1.67-4.34s2.54-1.64,4.31-1.64,3.17.54,4.2,1.62c1.03,1.08,1.54,2.47,1.54,4.18s-.54,3.18-1.62,4.31c-1.08,1.13-2.51,1.69-4.28,1.69ZM80.22,24.84c1.01,0,1.8-.35,2.35-1.04.56-.69.84-1.63.84-2.81s-.28-2.05-.84-2.74c-.56-.69-1.34-1.04-2.35-1.04s-1.81.36-2.41,1.07c-.6.71-.9,1.64-.9,2.78s.3,2.11.89,2.78,1.4,1.01,2.42,1.01Z"/>
transform="translate(-64.149712,-136.3326)"> <path class="st1" d="M103.61,15.4l-3.32,11.26h-2.67l-2.02-7.33c-.05-.19-.09-.34-.11-.45-.02-.11-.05-.25-.08-.43h-.05c-.03.18-.06.32-.09.43s-.07.25-.12.41l-2.19,7.36h-2.64l-3.31-11.26h2.6l2.01,7.71c.04.13.07.27.09.41.02.14.05.31.08.49h.07c.04-.19.07-.36.1-.5.03-.14.07-.29.12-.43l2.29-7.68h2.43l2.05,7.72c.02.09.05.21.08.36.03.15.07.33.1.54h.08c.04-.21.07-.36.09-.47.02-.11.06-.25.1-.43l1.95-7.72h2.39Z"/>
<rect <path class="st1" d="M115.36,26.66h-2.55v-6.59c0-.93-.19-1.64-.56-2.13-.37-.49-.93-.73-1.66-.73-.8,0-1.45.29-1.95.86-.5.57-.75,1.29-.75,2.17v6.42h-2.56v-11.26h2.56v1.53h.04c.4-.57.91-1.01,1.55-1.33.63-.31,1.32-.47,2.06-.47,1.25,0,2.2.4,2.85,1.19.65.79.98,1.92.98,3.4v6.94Z"/>
style="mix-blend-mode:normal;fill:url(#linearGradient13802);fill-opacity:1;stroke:none;stroke-width:0.271121" <path class="st1" d="M118.22,26.66V9.98h2.56v16.67h-2.56Z"/>
id="rect111" <path class="st1" d="M128.95,26.92c-1.78,0-3.2-.52-4.25-1.57s-1.57-2.46-1.57-4.23c0-1.8.56-3.24,1.67-4.34,1.11-1.1,2.54-1.64,4.31-1.64s3.17.54,4.2,1.62c1.03,1.08,1.54,2.47,1.54,4.18s-.54,3.18-1.62,4.31c-1.08,1.13-2.51,1.69-4.28,1.69ZM129.05,24.84c1.01,0,1.8-.35,2.35-1.04.56-.69.84-1.63.84-2.81s-.28-2.05-.84-2.74c-.56-.69-1.34-1.04-2.35-1.04s-1.81.36-2.41,1.07c-.6.71-.9,1.64-.9,2.78s.3,2.11.89,2.78c.59.67,1.4,1.01,2.42,1.01Z"/>
width="81.700577" <path class="st1" d="M144.71,26.66h-2.48v-1.4h-.04c-.4.54-.88.96-1.45,1.24-.57.28-1.21.42-1.91.42-1.04,0-1.89-.3-2.56-.89-.66-.59-1-1.37-1-2.33,0-1.03.33-1.86,1-2.49.66-.63,1.62-1.01,2.85-1.15l3.12-.35v-.54c0-.7-.19-1.22-.58-1.57s-.9-.52-1.53-.52-1.15.14-1.57.42c-.43.28-.78.68-1.06,1.2l-1.91-.98c.38-.76.98-1.38,1.8-1.86s1.8-.73,2.93-.73c1.42,0,2.51.37,3.26,1.12.75.74,1.13,1.82,1.13,3.24v7.17ZM142.25,22.08v-.62l-2.72.3c-.62.07-1.08.24-1.36.52-.29.28-.43.65-.43,1.12s.16.86.49,1.16c.33.3.75.45,1.27.45.82,0,1.49-.28,1.99-.83s.76-1.25.76-2.09Z"/>
height="24.334814" <path class="st1" d="M155.4,25.1c-.41.6-.93,1.06-1.55,1.36-.62.31-1.33.46-2.12.46-1.51,0-2.7-.5-3.57-1.5-.87-1-1.3-2.38-1.3-4.13,0-1.89.49-3.39,1.46-4.5.97-1.11,2.27-1.66,3.89-1.66.7,0,1.34.14,1.91.42.57.28,1,.63,1.29,1.06h.04v-6.62h2.56v16.67h-2.56v-1.56h-.04ZM149.46,21.19c0,1.14.26,2.04.78,2.68.52.65,1.24.97,2.16.97s1.69-.32,2.24-.97c.56-.64.84-1.47.84-2.49v-1.29c0-.82-.27-1.51-.81-2.06s-1.24-.83-2.1-.83c-.96,0-1.72.34-2.28,1.03s-.84,1.67-.84,2.95Z"/>
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> </g>
</svg> <path class="st1" d="M29.96,6.28h3.98c.66,0,1.19.53,1.19,1.19v8.35h4.36c.88,0,1.33,1.07.7,1.69l-7.56,7.56c-.37.37-.98.37-1.36,0l-7.57-7.56c-.63-.63-.18-1.69.7-1.69h4.36V7.47c0-.66.53-1.19,1.19-1.19ZM44.67,24.96v5.57c0,.66-.53,1.19-1.19,1.19h-23.06c-.66,0-1.19-.53-1.19-1.19v-5.57c0-.66.53-1.19,1.19-1.19h7.29l2.44,2.44c1,1,2.61,1,3.61,0l2.44-2.44h7.29c.66,0,1.19.53,1.19,1.19ZM38.5,29.34c0-.55-.45-.99-.99-.99s-.99.45-.99.99.45.99.99.99.99-.45.99-.99ZM41.68,29.34c0-.55-.45-.99-.99-.99s-.99.45-.99.99.45.99.99.99.99-.45.99-.99Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

+1 -1
View File
@@ -47,7 +47,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@decky/ui": "^4.11.1", "@decky/ui": "^4.11.6",
"compare-versions": "^6.1.1", "compare-versions": "^6.1.1",
"filesize": "^10.1.2", "filesize": "^10.1.2",
"i18next": "^25.6.0", "i18next": "^25.6.0",
+5 -5
View File
@@ -9,8 +9,8 @@ importers:
.: .:
dependencies: dependencies:
'@decky/ui': '@decky/ui':
specifier: ^4.11.1 specifier: ^4.11.6
version: 4.11.1 version: 4.11.6
compare-versions: compare-versions:
specifier: ^6.1.1 specifier: ^6.1.1
version: 6.1.1 version: 6.1.1
@@ -222,8 +222,8 @@ packages:
'@decky/api@1.1.3': '@decky/api@1.1.3':
resolution: {integrity: sha512-XsPCZxfxk5I1UtylIUN3qaWQI31siQbKfbLIskkI5innEatY1m4NQqBv/6hwPaO9mKMbdqYpnh5PSJDeMEOOBA==} resolution: {integrity: sha512-XsPCZxfxk5I1UtylIUN3qaWQI31siQbKfbLIskkI5innEatY1m4NQqBv/6hwPaO9mKMbdqYpnh5PSJDeMEOOBA==}
'@decky/ui@4.11.1': '@decky/ui@4.11.6':
resolution: {integrity: sha512-+x+rJB0MBQSQGp0UpsRJ4BOv7zlHzcBPT76enopjGf56H41beR8VZua9F4upLtDgPku4Zh8z3uB53nFlvTilXQ==} resolution: {integrity: sha512-vPCr2/KODeM6DAzIL/XN2e/RY7vhebXoWoh8e0VvB5QJU59Usb1z/cIpNmqe/GEMd1P3om6DFMcpEW5v8Se95Q==}
'@esbuild/aix-ppc64@0.20.2': '@esbuild/aix-ppc64@0.20.2':
resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==}
@@ -2311,7 +2311,7 @@ snapshots:
'@decky/api@1.1.3': {} '@decky/api@1.1.3': {}
'@decky/ui@4.11.1': {} '@decky/ui@4.11.6': {}
'@esbuild/aix-ppc64@0.20.2': '@esbuild/aix-ppc64@0.20.2':
optional: true optional: true
+3
View File
@@ -0,0 +1,3 @@
minimumReleaseAgeExclude:
- "@decky/api"
- "@decky/ui"
+334 -170
View File
@@ -1,7 +1,7 @@
import { sleep } from '@decky/ui'; import { joinClassNames, sleep } from '@decky/ui';
import { FunctionComponent, useEffect, useReducer, useState } from 'react'; import { FunctionComponent, useEffect, useReducer, useState } from 'react';
import { uninstallPlugin } from '../plugin'; import { disablePlugin, uninstallPlugin } from '../plugin';
import { VerInfo, doRestart, doShutdown } from '../updater'; import { VerInfo, doRestart, doShutdown } from '../updater';
import { ValveReactErrorInfo, getLikelyErrorSourceFromValveReactError } from '../utils/errors'; import { ValveReactErrorInfo, getLikelyErrorSourceFromValveReactError } from '../utils/errors';
import { useSetting } from '../utils/hooks/useSetting'; import { useSetting } from '../utils/hooks/useSetting';
@@ -20,6 +20,26 @@ declare global {
} }
} }
const classes = {
root: 'deckyErrorBoundary',
likelyOccurred: 'likely-occured-msg',
panel: 'panel-section',
panelHeader: 'panel-header',
trace: 'trace',
rowList: 'row-list',
rowItem: 'row-item',
buttonDescRow: 'button-description-row',
flexRowWGap: 'flex-row',
marginBottom: 'margin-bottom',
swipePrompt: 'swipe-prompt',
};
const vars = {
scrollBarwidth: '18px',
rootMarginLeft: '15px',
panelXPadding: '20px',
};
export const startSSH = DeckyBackend.callable('utilities/start_ssh'); export const startSSH = DeckyBackend.callable('utilities/start_ssh');
export const starrCEFForwarding = DeckyBackend.callable('utilities/allow_remote_debugging'); export const starrCEFForwarding = DeckyBackend.callable('utilities/allow_remote_debugging');
@@ -64,39 +84,130 @@ const DeckyErrorBoundary: FunctionComponent<DeckyErrorBoundaryProps> = ({ error,
<> <>
<style> <style>
{` {`
*:has(> .deckyErrorBoundary) { *:has(> .${classes.root}) {
margin-top: var(--basicui-header-height);
overflow: scroll !important; overflow: scroll !important;
background: #000;
}
*:has(> .${classes.root})::-webkit-scrollbar {
display: initial !important;
width: ${vars.scrollBarwidth};
height: 0px;
}
*:has(> .${classes.root})::-webkit-scrollbar-thumb {
background: #4349535e;
}
.${classes.root} {
color: #93929e;
font-size: 15px;
margin: 10px 0px 40px ${vars.rootMarginLeft};
overflow: visible;
}
.${classes.root} button,
.${classes.root} select {
border: none;
padding: 4px 16px !important;
background: #333;
color: #ddd;
font-size: 12px;
border-radius: 3px;
outline: none;
height: 28px;
}
.${classes.panel} {
background: #080808;
padding: 8px ${vars.panelXPadding};
border-radius: 3px;
/* box-shadow: 9px 9px 20px -5px rgb(0 0 0 / 89%); */
}
.${classes.panelHeader} {
font-size: 18px;
font-weight: bolder;
text-transform: uppercase;
}
.${classes.likelyOccurred} {
font-size: 22px;
font-weight: bold;
color: #588fb4;
}
.${classes.rowItem} {
position: relative;
}
.${classes.rowItem}:not(:last-child)::after {
content: '';
position: absolute;
bottom: -4.5px;
left: 5px;
right: 15px;
height: 0.5px;
background: #3c3c3c47;
}
.${classes.flexRowWGap},
.${classes.buttonDescRow},
.${classes.rowList},
.${classes.panel} {
display: flex;
}
.${classes.rowList},
.${classes.panel} {
flex-direction: column;
}
.${classes.flexRowWGap},
.${classes.rowList} {
gap: 8px;
}
.${classes.marginBottom} {
margin-bottom: 10px;
}
.${classes.buttonDescRow} {
justify-content: space-between;
align-items: center;
}
.${classes.swipePrompt} {
display: flex;
align-items: center;
text-align: center;
position: relative;
font-style: italic;
font-size: small;
margin: 16px 0;
}
.${classes.swipePrompt} span {
padding: 0 8px;
background-color: #000;
position: relative;
z-index: 1;
}
.${classes.swipePrompt}::before,
.${classes.swipePrompt}::after {
content: "";
flex-grow: 1;
border-bottom: 1px solid #474752;
top: 50%;
}
.${classes.swipePrompt}::before {
right: 50%;
margin-right: 8px;
}
.${classes.swipePrompt}::after {
left: 50%;
margin-left: 8px;
} }
`} `}
</style> </style>
<div <div className={classes.root}>
style={{ <div className={classes.marginBottom}>An error occurred while rendering this content.</div>
overflow: 'auto', <pre className={joinClassNames(classes.marginBottom)} style={{ marginTop: '0px' }}>
marginLeft: '15px',
color: 'white',
fontSize: '16px',
userSelect: 'auto',
backgroundColor: 'black',
marginTop: '48px', // Incase this is a page
}}
className="deckyErrorBoundary"
>
<h1
style={{
fontSize: '20px',
display: 'inline-block',
userSelect: 'auto',
}}
>
An error occured while rendering this content.
</h1>
<pre style={{}}>
<code> <code>
{identifier && `Error Reference: ${identifier}`} {identifier && `Error Reference: ${identifier}`}
{versionInfo?.current && `\nDecky Version: ${versionInfo.current}`} {versionInfo?.current && `\nDecky Version: ${versionInfo.current}`}
</code> </code>
</pre> </pre>
<p>This error likely occured in {errorSource}.</p> <div className={joinClassNames(classes.likelyOccurred, classes.marginBottom)}>
This error likely occurred in {errorSource}.
</div>
{actionLog?.length > 0 && ( {actionLog?.length > 0 && (
<pre> <pre>
<code> <code>
@@ -106,142 +217,88 @@ const DeckyErrorBoundary: FunctionComponent<DeckyErrorBoundaryProps> = ({ error,
</pre> </pre>
)} )}
{actionsEnabled && ( {actionsEnabled && (
<> <div className={classes.panel}>
<h3>Actions: </h3> <div className={classes.flexRowWGap} style={{ alignItems: 'center', marginBottom: '8px' }}>
<p>Use the touch screen.</p> <div className={classes.panelHeader}>Actions</div>
<div style={{ display: 'block', marginBottom: '5px' }}> <div style={{ fontSize: 'small', fontStyle: 'italic' }}>
<button style={{ marginRight: '5px', padding: '5px' }} onClick={reset}> Use the touch screen. Solutions are listed in the recommended order. If you are still experiencing
Retry issues, please post in the #loader-support channel at decky.xyz/discord.
</button>
<button
style={{ marginRight: '5px', padding: '5px' }}
onClick={() => {
addLogLine('Restarting Steam...');
SteamClient.User.StartRestart(false);
}}
>
Restart Steam
</button>
</div>
<div style={{ display: 'block', marginBottom: '5px' }}>
<button
style={{ marginRight: '5px', padding: '5px' }}
onClick={async () => {
setActionsEnabled(false);
addLogLine('Restarting Decky...');
doRestart();
await sleep(2000);
addLogLine('Reloading UI...');
}}
>
Restart Decky
</button>
<button
style={{ marginRight: '5px', padding: '5px' }}
onClick={async () => {
setActionsEnabled(false);
addLogLine('Stopping Decky...');
doShutdown();
await sleep(5000);
addLogLine('Restarting Steam...');
SteamClient.User.StartRestart(false);
}}
>
Disable Decky until next boot
</button>
</div>
{debugAllowed && (
<div style={{ display: 'block', marginBottom: '5px' }}>
<button
style={{ marginRight: '5px', padding: '5px' }}
onClick={async () => {
setDebugAllowed(false);
addLogLine('Enabling CEF debugger forwarding...');
await starrCEFForwarding();
addLogLine('Enabling SSH...');
await startSSH();
addLogLine('Ready for debugging!');
if (window?.SystemNetworkStore?.wirelessNetworkDevice?.ip4?.addresses?.[0]?.ip) {
const ip = ipToString(window.SystemNetworkStore.wirelessNetworkDevice.ip4.addresses[0].ip);
addLogLine(`CEF Debugger: http://${ip}:8081`);
addLogLine(`SSH: deck@${ip}`);
}
}}
>
Allow remote debugging and SSH until next boot
</button>
</div> </div>
)} </div>
{ <div className={classes.rowList}>
<div style={{ display: 'block', marginBottom: '5px' }}> <div className={joinClassNames(classes.rowItem, classes.buttonDescRow)}>
{updateProgress > -1 Retry the action or restart
? 'Update in progress... ' + updateProgress + '%' <div className={classes.flexRowWGap}>
: updateProgress == -2 <button onClick={reset}>Retry</button>
? 'Update complete. Restarting...' <button
: 'Changing your Decky Loader branch and/or \n checking for updates might help!\n'} onClick={() => {
{updateProgress == -1 && ( addLogLine('Restarting Steam...');
<div style={{ height: '30px' }}> SteamClient.User.StartRestart(false);
<select }}
style={{ height: '100%' }} >
onChange={async (e) => { Restart Steam
const branch = parseInt(e.target.value); </button>
setSelectedBranch(branch); <button
setSetVersionToUpdateTo(''); onClick={async () => {
}} setActionsEnabled(false);
> addLogLine('Restarting Decky...');
<option value="0" selected={selectedBranch == UpdateBranch.Stable}> doRestart();
Stable await sleep(2000);
</option> addLogLine('Reloading UI...');
<option value="1" selected={selectedBranch == UpdateBranch.Prerelease}> }}
Pre-Release >
</option> Restart Decky
<option value="2" selected={selectedBranch == UpdateBranch.Testing}> </button>
Testing </div>
</option> </div>
</select> {wasCausedByPlugin && (
<div className={joinClassNames(classes.rowItem, classes.buttonDescRow)}>
Disable or uninstall the suspected plugin
<div className={classes.flexRowWGap}>
<button <button
style={{ height: '100%' }}
disabled={updateProgress != -1 || isChecking}
onClick={async () => { onClick={async () => {
if (versionToUpdateTo == '') { setActionsEnabled(false);
setIsChecking(true); addLogLine(`Disabling ${errorSource}...`);
const versionInfo = (await DeckyBackend.callable( await disablePlugin(errorSource);
'updater/check_for_updates', await sleep(1000);
)()) as unknown as VerInfo; addLogLine('Restarting Decky...');
setIsChecking(false); doRestart();
if (versionInfo?.remote && versionInfo?.remote?.tag_name != versionInfo?.current) { await sleep(2000);
setSetVersionToUpdateTo(versionInfo.remote.tag_name); addLogLine('Restarting Steam...');
} else { await sleep(500);
setSetVersionToUpdateTo(''); SteamClient.User.StartRestart(false);
}
} else {
DeckyBackend.callable('updater/do_update')();
setUpdateProgress(0);
}
}} }}
> >
{' '} Disable {errorSource}
{isChecking </button>
? 'Checking for updates...' <button
: versionToUpdateTo != '' onClick={async () => {
? 'Update to ' + versionToUpdateTo setActionsEnabled(false);
: 'Check for updates'} addLogLine(`Uninstalling ${errorSource}...`);
await uninstallPlugin(errorSource);
await DeckyPluginLoader.frozenPluginsService.invalidate();
await DeckyPluginLoader.hiddenPluginsService.invalidate();
await sleep(1000);
addLogLine('Restarting Decky...');
doRestart();
await sleep(2000);
addLogLine('Restarting Steam...');
await sleep(500);
SteamClient.User.StartRestart(false);
}}
>
Uninstall {errorSource}
</button> </button>
</div> </div>
)} </div>
</div> )}
} <div className={joinClassNames(classes.rowItem, classes.buttonDescRow)}>
{wasCausedByPlugin && ( Disable all plugins
<div style={{ display: 'block', marginBottom: '5px' }}>
{'\n'}
<button <button
style={{ marginRight: '5px', padding: '5px' }}
onClick={async () => { onClick={async () => {
setActionsEnabled(false); setActionsEnabled(false);
addLogLine(`Uninstalling ${errorSource}...`); addLogLine(`Disabling plugins...`);
await uninstallPlugin(errorSource); await DeckyBackend.call('utilities/set_all_plugins_disabled');
await DeckyPluginLoader.frozenPluginsService.invalidate();
await DeckyPluginLoader.hiddenPluginsService.invalidate();
await sleep(1000); await sleep(1000);
addLogLine('Restarting Decky...'); addLogLine('Restarting Decky...');
doRestart(); doRestart();
@@ -251,27 +308,134 @@ const DeckyErrorBoundary: FunctionComponent<DeckyErrorBoundaryProps> = ({ error,
SteamClient.User.StartRestart(false); SteamClient.User.StartRestart(false);
}} }}
> >
Uninstall {errorSource} and restart Decky Disable All Plugins
</button> </button>
</div> </div>
)} {
</> <div className={joinClassNames(classes.rowItem, classes.buttonDescRow)}>
{updateProgress > -1
? 'Update in progress... ' + updateProgress + '%'
: updateProgress == -2
? 'Update complete. Restarting...'
: 'Check for Decky updates'}
{
<div className={classes.flexRowWGap}>
{updateProgress == -1 && (
<>
<select
onChange={async (e) => {
const branch = parseInt(e.target.value);
setSelectedBranch(branch);
setSetVersionToUpdateTo('');
}}
>
<option value="0" selected={selectedBranch == UpdateBranch.Stable}>
Stable
</option>
<option value="1" selected={selectedBranch == UpdateBranch.Prerelease}>
Pre-Release
</option>
<option value="2" selected={selectedBranch == UpdateBranch.Testing}>
Testing
</option>
</select>
<button
disabled={updateProgress != -1 || isChecking}
onClick={async () => {
if (versionToUpdateTo == '') {
setIsChecking(true);
const versionInfo = (await DeckyBackend.callable(
'updater/check_for_updates',
)()) as unknown as VerInfo;
setIsChecking(false);
if (versionInfo?.remote && versionInfo?.remote?.tag_name != versionInfo?.current) {
setSetVersionToUpdateTo(versionInfo.remote.tag_name);
} else {
setSetVersionToUpdateTo('');
}
} else {
DeckyBackend.callable('updater/do_update')();
setUpdateProgress(0);
}
}}
>
{' '}
{isChecking
? 'Checking for updates...'
: versionToUpdateTo != ''
? 'Update to ' + versionToUpdateTo
: 'Check for updates'}
</button>
</>
)}
</div>
}
</div>
}
<div className={joinClassNames(classes.rowItem, classes.buttonDescRow)}>
Disable Decky until next boot
<button
onClick={async () => {
setActionsEnabled(false);
addLogLine('Stopping Decky...');
doShutdown();
await sleep(5000);
addLogLine('Restarting Steam...');
SteamClient.User.StartRestart(false);
}}
>
Disable Decky
</button>
</div>
{debugAllowed && (
<div className={joinClassNames(classes.rowItem, classes.buttonDescRow)}>
Enable remote debugging and SSH until next boot (for developers)
<button
onClick={async () => {
setDebugAllowed(false);
addLogLine('Enabling CEF debugger forwarding...');
await starrCEFForwarding();
addLogLine('Enabling SSH...');
await startSSH();
addLogLine('Ready for debugging!');
if (window?.SystemNetworkStore?.wirelessNetworkDevice?.ip4?.addresses?.[0]?.ip) {
const ip = ipToString(window.SystemNetworkStore.wirelessNetworkDevice.ip4.addresses[0].ip);
addLogLine(`CEF Debugger: http://${ip}:8081`);
addLogLine(`SSH: deck@${ip}`);
}
}}
>
Enable
</button>
</div>
)}
</div>
</div>
)} )}
{actionsEnabled && (
<pre <div className={classes.swipePrompt}>
style={{ <span>Swipe to scroll</span>
marginTop: '15px', </div>
opacity: 0.7, )}
userSelect: 'auto', <div className={classes.panel}>
}} <div className={classes.panelHeader}>Trace</div>
> <pre
<code> style={{
{error.error.stack} margin: `8px calc(-1 * ${vars.panelXPadding})`,
{'\n\n'} userSelect: 'auto',
Component Stack: overflowX: 'scroll',
{error.info.componentStack} padding: `0px ${vars.panelXPadding}`,
</code> maskImage: `linear-gradient(to right, transparent, black ${vars.panelXPadding}, black calc(100% - ${vars.panelXPadding}), transparent)`,
</pre> }}
>
<code>
{error.error.stack}
{'\n\n'}
Component Stack:
{error.info.componentStack}
</code>
</pre>
</div>
</div> </div>
</> </>
); );
+17 -1
View File
@@ -1,12 +1,14 @@
import { FC, ReactNode, createContext, useContext, useEffect, useState } from 'react'; import { FC, ReactNode, createContext, useContext, useEffect, useState } from 'react';
import { DEFAULT_NOTIFICATION_SETTINGS, NotificationSettings } from '../notification-service'; import { DEFAULT_NOTIFICATION_SETTINGS, NotificationSettings } from '../notification-service';
import { Plugin } from '../plugin'; import { DisabledPlugin, Plugin } from '../plugin';
import { PluginUpdateMapping } from '../store'; import { PluginUpdateMapping } from '../store';
import { VerInfo } from '../updater'; import { VerInfo } from '../updater';
interface PublicDeckyState { interface PublicDeckyState {
plugins: Plugin[]; plugins: Plugin[];
disabledPlugins: DisabledPlugin[];
installedPlugins: (Plugin | DisabledPlugin)[];
pluginOrder: string[]; pluginOrder: string[];
frozenPlugins: string[]; frozenPlugins: string[];
hiddenPlugins: string[]; hiddenPlugins: string[];
@@ -26,6 +28,8 @@ export interface UserInfo {
export class DeckyState { export class DeckyState {
private _plugins: Plugin[] = []; private _plugins: Plugin[] = [];
private _disabledPlugins: DisabledPlugin[] = [];
private _installedPlugins: (Plugin | DisabledPlugin)[] = [];
private _pluginOrder: string[] = []; private _pluginOrder: string[] = [];
private _frozenPlugins: string[] = []; private _frozenPlugins: string[] = [];
private _hiddenPlugins: string[] = []; private _hiddenPlugins: string[] = [];
@@ -42,6 +46,8 @@ export class DeckyState {
publicState(): PublicDeckyState { publicState(): PublicDeckyState {
return { return {
plugins: this._plugins, plugins: this._plugins,
disabledPlugins: this._disabledPlugins,
installedPlugins: this._installedPlugins,
pluginOrder: this._pluginOrder, pluginOrder: this._pluginOrder,
frozenPlugins: this._frozenPlugins, frozenPlugins: this._frozenPlugins,
hiddenPlugins: this._hiddenPlugins, hiddenPlugins: this._hiddenPlugins,
@@ -62,6 +68,13 @@ export class DeckyState {
setPlugins(plugins: Plugin[]) { setPlugins(plugins: Plugin[]) {
this._plugins = plugins; this._plugins = plugins;
this._installedPlugins = [...plugins, ...this._disabledPlugins];
this.notifyUpdate();
}
setDisabledPlugins(disabledPlugins: DisabledPlugin[]) {
this._disabledPlugins = disabledPlugins;
this._installedPlugins = [...this._plugins, ...disabledPlugins];
this.notifyUpdate(); this.notifyUpdate();
} }
@@ -125,6 +138,7 @@ interface DeckyStateContext extends PublicDeckyState {
setIsLoaderUpdating(hasUpdate: boolean): void; setIsLoaderUpdating(hasUpdate: boolean): void;
setActivePlugin(name: string): void; setActivePlugin(name: string): void;
setPluginOrder(pluginOrder: string[]): void; setPluginOrder(pluginOrder: string[]): void;
setDisabledPlugins(disabled: DisabledPlugin[]): void;
closeActivePlugin(): void; closeActivePlugin(): void;
} }
@@ -163,6 +177,7 @@ export const DeckyStateContextProvider: FC<Props> = ({ children, deckyState }) =
const setActivePlugin = deckyState.setActivePlugin.bind(deckyState); const setActivePlugin = deckyState.setActivePlugin.bind(deckyState);
const closeActivePlugin = deckyState.closeActivePlugin.bind(deckyState); const closeActivePlugin = deckyState.closeActivePlugin.bind(deckyState);
const setPluginOrder = deckyState.setPluginOrder.bind(deckyState); const setPluginOrder = deckyState.setPluginOrder.bind(deckyState);
const setDisabledPlugins = deckyState.setDisabledPlugins.bind(deckyState);
return ( return (
<DeckyStateContext.Provider <DeckyStateContext.Provider
@@ -173,6 +188,7 @@ export const DeckyStateContextProvider: FC<Props> = ({ children, deckyState }) =
setActivePlugin, setActivePlugin,
closeActivePlugin, closeActivePlugin,
setPluginOrder, setPluginOrder,
setDisabledPlugins,
}} }}
> >
{children} {children}
+36 -10
View File
@@ -1,7 +1,7 @@
import { ButtonItem, ErrorBoundary, Focusable, PanelSection, PanelSectionRow } from '@decky/ui'; import { ButtonItem, ErrorBoundary, Focusable, PanelSection, PanelSectionRow } from '@decky/ui';
import { FC, useMemo } from 'react'; import { FC, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaEyeSlash } from 'react-icons/fa'; import { FaBan, FaEyeSlash } from 'react-icons/fa';
import { useDeckyState } from './DeckyState'; import { useDeckyState } from './DeckyState';
import NotificationBadge from './NotificationBadge'; import NotificationBadge from './NotificationBadge';
@@ -9,8 +9,16 @@ import { useQuickAccessVisible } from './QuickAccessVisibleState';
import TitleView from './TitleView'; import TitleView from './TitleView';
const PluginView: FC = () => { const PluginView: FC = () => {
const { plugins, hiddenPlugins, updates, activePlugin, pluginOrder, setActivePlugin, closeActivePlugin } = const {
useDeckyState(); plugins,
disabledPlugins,
hiddenPlugins,
updates,
activePlugin,
pluginOrder,
setActivePlugin,
closeActivePlugin,
} = useDeckyState();
const visible = useQuickAccessVisible(); const visible = useQuickAccessVisible();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -21,7 +29,9 @@ const PluginView: FC = () => {
.sort((a, b) => pluginOrder.indexOf(a.name) - pluginOrder.indexOf(b.name)) .sort((a, b) => pluginOrder.indexOf(a.name) - pluginOrder.indexOf(b.name))
.filter((p) => p.content) .filter((p) => p.content)
.filter(({ name }) => !hiddenPlugins.includes(name)); .filter(({ name }) => !hiddenPlugins.includes(name));
}, [plugins, pluginOrder]); }, [plugins, pluginOrder, hiddenPlugins]);
const numberOfHidden = hiddenPlugins.filter((name) => !!plugins.find((p) => p.name === name)).length;
if (activePlugin) { if (activePlugin) {
return ( return (
@@ -53,12 +63,28 @@ const PluginView: FC = () => {
</ButtonItem> </ButtonItem>
</PanelSectionRow> </PanelSectionRow>
))} ))}
{hiddenPlugins.length > 0 && ( <div
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', fontSize: '0.8rem', marginTop: '10px' }}> style={{
<FaEyeSlash /> display: 'flex',
<div>{t('PluginView.hidden', { count: hiddenPlugins.length })}</div> flexDirection: 'column',
</div> position: 'absolute',
)} justifyContent: 'center',
padding: '5px 0px',
}}
>
{numberOfHidden > 0 && (
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', fontSize: '0.8rem' }}>
<FaEyeSlash />
<div>{t('PluginView.hidden', { count: numberOfHidden })}</div>
</div>
)}
{disabledPlugins.length > 0 && (
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', fontSize: '0.8rem' }}>
<FaBan />
<div>{t('PluginView.disabled', { count: disabledPlugins.length })}</div>
</div>
)}
</div>
</PanelSection> </PanelSection>
</div> </div>
</> </>
-1
View File
@@ -8,7 +8,6 @@ import { useDeckyState } from './DeckyState';
const titleStyles: CSSProperties = { const titleStyles: CSSProperties = {
display: 'flex', display: 'flex',
paddingTop: '3px',
paddingRight: '16px', paddingRight: '16px',
position: 'sticky', position: 'sticky',
top: '0px', top: '0px',
@@ -3,10 +3,11 @@ import { FC, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaCheck, FaDownload } from 'react-icons/fa'; import { FaCheck, FaDownload } from 'react-icons/fa';
import { InstallType, InstallTypeTranslationMapping } from '../../plugin'; import { DisabledPlugin, InstallType, InstallTypeTranslationMapping } from '../../plugin';
interface MultiplePluginsInstallModalProps { interface MultiplePluginsInstallModalProps {
requests: { name: string; version: string; hash: string; install_type: InstallType }[]; requests: { name: string; version: string; hash: string; install_type: InstallType }[];
disabledPlugins: DisabledPlugin[];
onOK(): void | Promise<void>; onOK(): void | Promise<void>;
onCancel(): void | Promise<void>; onCancel(): void | Promise<void>;
closeModal?(): void; closeModal?(): void;
@@ -17,6 +18,7 @@ type TitleTranslationMapping = 'mixed' | (typeof InstallTypeTranslationMapping)[
const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({ const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({
requests, requests,
disabledPlugins,
onOK, onOK,
onCancel, onCancel,
closeModal, closeModal,
@@ -116,10 +118,11 @@ const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({
version, version,
}); });
const disabled = disabledPlugins.some((p) => p.name === name);
return ( return (
<li key={i} style={{ display: 'flex', flexDirection: 'column' }}> <li key={i} style={{ display: 'flex', flexDirection: 'column' }}>
<span> <span>
{description}{' '} {disabled ? `${description} - ${t('PluginInstallModal.disabled')}` : description}{' '}
{(pluginsCompleted.includes(name) && <FaCheck />) || (name === pluginInProgress && <FaDownload />)} {(pluginsCompleted.includes(name) && <FaCheck />) || (name === pluginInProgress && <FaDownload />)}
</span> </span>
{hash === 'False' && ( {hash === 'False' && (
@@ -0,0 +1,39 @@
import { ConfirmModal, Spinner } from '@decky/ui';
import { FC, useState } from 'react';
import { disablePlugin } from '../../plugin';
interface PluginDisableModalProps {
name: string;
title: string;
buttonText: string;
description: string;
closeModal?(): void;
}
const PluginDisableModal: FC<PluginDisableModalProps> = ({ name, title, buttonText, description, closeModal }) => {
const [disabling, setDisabling] = useState<boolean>(false);
return (
<ConfirmModal
closeModal={closeModal}
onOK={async () => {
setDisabling(true);
await disablePlugin(name);
closeModal?.();
}}
bOKDisabled={disabling}
bCancelDisabled={disabling}
strTitle={
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', width: '100%' }}>
{title}
{disabling && <Spinner width="24px" height="24px" style={{ marginLeft: 'auto' }} />}
</div>
}
strOKButtonText={buttonText}
>
{description}
</ConfirmModal>
);
};
export default PluginDisableModal;
@@ -9,6 +9,7 @@ interface PluginInstallModalProps {
version: string; version: string;
hash: string; hash: string;
installType: InstallType; installType: InstallType;
disabled?: boolean;
onOK(): void; onOK(): void;
onCancel(): void; onCancel(): void;
closeModal?(): void; closeModal?(): void;
@@ -19,6 +20,7 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
version, version,
hash, hash,
installType, installType,
disabled,
onOK, onOK,
onCancel, onCancel,
closeModal, closeModal,
@@ -45,6 +47,10 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
}, []); }, []);
const installTypeTranslationKey = InstallTypeTranslationMapping[installType]; const installTypeTranslationKey = InstallTypeTranslationMapping[installType];
const description = t(`PluginInstallModal.${installTypeTranslationKey}.desc`, {
artifact: artifact,
version: version,
});
return ( return (
<ConfirmModal <ConfirmModal
@@ -118,10 +124,7 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
// t('PluginInstallModal.update.desc') // t('PluginInstallModal.update.desc')
// t('PluginInstallModal.downgrade.desc') // t('PluginInstallModal.downgrade.desc')
// t('PluginInstallModal.overwrite.desc') // t('PluginInstallModal.overwrite.desc')
t(`PluginInstallModal.${installTypeTranslationKey}.desc`, { disabled ? `${description} ${t('PluginInstallModal.disabled')}` : description
artifact: artifact,
version: version,
})
} }
</div> </div>
{hash == 'False' && <span style={{ color: 'red' }}>{t('PluginInstallModal.no_hash')}</span>} {hash == 'False' && <span style={{ color: 'red' }}>{t('PluginInstallModal.no_hash')}</span>}
@@ -2,8 +2,10 @@ import { ConfirmModal, Spinner } from '@decky/ui';
import { FC, useState } from 'react'; import { FC, useState } from 'react';
import { uninstallPlugin } from '../../plugin'; import { uninstallPlugin } from '../../plugin';
import { DeckyState } from '../DeckyState';
interface PluginUninstallModalProps { interface PluginUninstallModalProps {
deckyState: DeckyState;
name: string; name: string;
title: string; title: string;
buttonText: string; buttonText: string;
@@ -11,7 +13,14 @@ interface PluginUninstallModalProps {
closeModal?(): void; closeModal?(): void;
} }
const PluginUninstallModal: FC<PluginUninstallModalProps> = ({ name, title, buttonText, description, closeModal }) => { const PluginUninstallModal: FC<PluginUninstallModalProps> = ({
name,
title,
buttonText,
description,
deckyState,
closeModal,
}) => {
const [uninstalling, setUninstalling] = useState<boolean>(false); const [uninstalling, setUninstalling] = useState<boolean>(false);
return ( return (
<ConfirmModal <ConfirmModal
@@ -19,6 +28,7 @@ const PluginUninstallModal: FC<PluginUninstallModalProps> = ({ name, title, butt
onOK={async () => { onOK={async () => {
setUninstalling(true); setUninstalling(true);
await uninstallPlugin(name); await uninstallPlugin(name);
deckyState.setDisabledPlugins(deckyState.publicState().disabledPlugins.filter((d) => d.name !== name));
// uninstalling a plugin resets the hidden setting for it server-side // uninstalling a plugin resets the hidden setting for it server-side
// we invalidate here so if you re-install it, you won't have an out-of-date hidden filter // we invalidate here so if you re-install it, you won't have an out-of-date hidden filter
await DeckyPluginLoader.frozenPluginsService.invalidate(); await DeckyPluginLoader.frozenPluginsService.invalidate();
@@ -1,4 +1,14 @@
import { Carousel, DialogButton, Field, Focusable, ProgressBarWithInfo, Spinner, findSP, showModal } from '@decky/ui'; import {
Carousel,
DialogButton,
Field,
Focusable,
ProgressBarWithInfo,
Spinner,
findSP,
gamepadDialogClasses,
showModal,
} from '@decky/ui';
import { Suspense, lazy, useCallback, useEffect, useState } from 'react'; import { Suspense, lazy, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaExclamation } from 'react-icons/fa'; import { FaExclamation } from 'react-icons/fa';
@@ -153,13 +163,22 @@ export default function UpdaterSettings() {
: t('Updater.updates.install_button')} : t('Updater.updates.install_button')}
</DialogButton> </DialogButton>
) : ( ) : (
<ProgressBarWithInfo <div id="decky-hide-left">
layout="inline" <style>
bottomSeparator="none" {`
nProgress={updateProgress} #decky-hide-left .${gamepadDialogClasses.FieldLeftColumn} {
indeterminate={reloading} display: none;
sOperationText={reloading ? t('Updater.updates.reloading') : t('Updater.updates.updating')} }
/> `}
</style>
<ProgressBarWithInfo
layout="inline"
bottomSeparator="none"
nProgress={updateProgress}
indeterminate={reloading}
sOperationText={reloading ? t('Updater.updates.reloading') : t('Updater.updates.updating')}
/>
</div>
)} )}
</Field> </Field>
{versionInfo?.remote && versionInfo?.remote?.tag_name != versionInfo?.current && ( {versionInfo?.remote && versionInfo?.remote?.tag_name != versionInfo?.current && (
@@ -1,15 +1,16 @@
import { FC } from 'react'; import { FC } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaEyeSlash, FaLock } from 'react-icons/fa'; import { FaBan, FaEyeSlash, FaLock } from 'react-icons/fa';
interface PluginListLabelProps { interface PluginListLabelProps {
frozen: boolean; frozen: boolean;
hidden: boolean; hidden: boolean;
disabled: boolean;
name: string; name: string;
version?: string; version?: string;
} }
const PluginListLabel: FC<PluginListLabelProps> = ({ name, frozen, hidden, version }) => { const PluginListLabel: FC<PluginListLabelProps> = ({ name, frozen, hidden, version, disabled }) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
@@ -43,6 +44,20 @@ const PluginListLabel: FC<PluginListLabelProps> = ({ name, frozen, hidden, versi
{t('PluginListLabel.hidden')} {t('PluginListLabel.hidden')}
</div> </div>
)} )}
{disabled && (
<div
style={{
fontSize: '0.8rem',
color: '#dcdedf',
display: 'flex',
alignItems: 'center',
gap: '10px',
}}
>
<FaBan />
{t('PluginListLabel.disabled')}
</div>
)}
</div> </div>
); );
}; };
@@ -2,9 +2,11 @@ import {
DialogBody, DialogBody,
DialogButton, DialogButton,
DialogControlsSection, DialogControlsSection,
Focusable,
GamepadEvent, GamepadEvent,
Menu, Menu,
MenuItem, MenuItem,
NavEntryPositionPreferences,
ReorderableEntry, ReorderableEntry,
ReorderableList, ReorderableList,
showContextMenu, showContextMenu,
@@ -13,7 +15,7 @@ import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; 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 { InstallType, enablePlugin } from '../../../../plugin';
import { import {
StorePluginVersion, StorePluginVersion,
getPluginList, getPluginList,
@@ -35,6 +37,7 @@ async function reinstallPlugin(pluginName: string, currentVersion?: string) {
type PluginTableData = PluginData & { type PluginTableData = PluginData & {
name: string; name: string;
disabled: boolean;
frozen: boolean; frozen: boolean;
onFreeze(): void; onFreeze(): void;
onUnfreeze(): void; onUnfreeze(): void;
@@ -54,22 +57,25 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> }
return null; return null;
} }
const { name, update, version, onHide, onShow, hidden, onFreeze, onUnfreeze, frozen, isDeveloper } = props.entry.data; const { name, update, version, onHide, onShow, hidden, onFreeze, onUnfreeze, frozen, isDeveloper, disabled } =
props.entry.data;
const showCtxMenu = (e: MouseEvent | GamepadEvent) => { const showCtxMenu = (e: MouseEvent | GamepadEvent) => {
showContextMenu( showContextMenu(
<Menu label={t('PluginListIndex.plugin_actions')}> <Menu label={t('PluginListIndex.plugin_actions')}>
<MenuItem {!disabled && (
onSelected={async () => { <MenuItem
try { onSelected={async () => {
await reloadPluginBackend(name); try {
} catch (err) { await reloadPluginBackend(name);
console.error('Error Reloading Plugin Backend', err); } catch (err) {
} console.error('Error Reloading Plugin Backend', err);
}} }
> }}
{t('PluginListIndex.reload')} >
</MenuItem> {t('PluginListIndex.reload')}
</MenuItem>
)}
<MenuItem <MenuItem
onSelected={() => onSelected={() =>
DeckyPluginLoader.uninstallPlugin( DeckyPluginLoader.uninstallPlugin(
@@ -82,11 +88,28 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> }
> >
{t('PluginListIndex.uninstall')} {t('PluginListIndex.uninstall')}
</MenuItem> </MenuItem>
{hidden ? ( {disabled ? (
<MenuItem onSelected={onShow}>{t('PluginListIndex.show')}</MenuItem> <MenuItem onSelected={() => enablePlugin(name)}>{t('PluginListIndex.enable')}</MenuItem>
) : ( ) : (
<MenuItem onSelected={onHide}>{t('PluginListIndex.hide')}</MenuItem> <MenuItem
onSelected={() =>
DeckyPluginLoader.disablePlugin(
name,
t('PluginLoader.plugin_disable.title', { name }),
t('PluginLoader.plugin_disable.button'),
t('PluginLoader.plugin_disable.desc', { name }),
)
}
>
{t('PluginListIndex.disable')}
</MenuItem>
)} )}
{!disabled &&
(hidden ? (
<MenuItem onSelected={onShow}>{t('PluginListIndex.show')}</MenuItem>
) : (
<MenuItem onSelected={onHide}>{t('PluginListIndex.hide')}</MenuItem>
))}
{frozen ? ( {frozen ? (
<MenuItem onSelected={onUnfreeze}>{t('PluginListIndex.unfreeze')}</MenuItem> <MenuItem onSelected={onUnfreeze}>{t('PluginListIndex.unfreeze')}</MenuItem>
) : ( ) : (
@@ -98,7 +121,7 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> }
}; };
return ( return (
<> <Focusable navEntryPreferPosition={NavEntryPositionPreferences.MAINTAIN_X} style={{ display: 'flex' }}>
{update ? ( {update ? (
<DialogButton <DialogButton
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }} style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
@@ -137,7 +160,7 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> }
> >
<FaEllipsisH /> <FaEllipsisH />
</DialogButton> </DialogButton>
</> </Focusable>
); );
} }
@@ -147,16 +170,18 @@ type PluginData = {
}; };
export default function PluginList({ isDeveloper }: { isDeveloper: boolean }) { export default function PluginList({ isDeveloper }: { isDeveloper: boolean }) {
const { plugins, updates, pluginOrder, setPluginOrder, frozenPlugins, hiddenPlugins } = useDeckyState(); const { installedPlugins, disabledPlugins, updates, pluginOrder, setPluginOrder, frozenPlugins, hiddenPlugins } =
useDeckyState();
const [_, setPluginOrderSetting] = useSetting<string[]>( const [_, setPluginOrderSetting] = useSetting<string[]>(
'pluginOrder', 'pluginOrder',
plugins.map((plugin) => plugin.name), installedPlugins.map((plugin) => plugin.name),
); );
const { t } = useTranslation(); const { t } = useTranslation();
useEffect(() => { useEffect(() => {
DeckyPluginLoader.checkPluginUpdates(); DeckyPluginLoader.checkPluginUpdates();
}, []); }, [installedPlugins, frozenPlugins]);
const [pluginEntries, setPluginEntries] = useState<ReorderableEntry<PluginTableData>[]>([]); const [pluginEntries, setPluginEntries] = useState<ReorderableEntry<PluginTableData>[]>([]);
const hiddenPluginsService = DeckyPluginLoader.hiddenPluginsService; const hiddenPluginsService = DeckyPluginLoader.hiddenPluginsService;
@@ -164,15 +189,24 @@ export default function PluginList({ isDeveloper }: { isDeveloper: boolean }) {
useEffect(() => { useEffect(() => {
setPluginEntries( setPluginEntries(
plugins.map(({ name, version }) => { installedPlugins.map(({ name, version }) => {
const frozen = frozenPlugins.includes(name); const frozen = frozenPlugins.includes(name);
const hidden = hiddenPlugins.includes(name); const hidden = hiddenPlugins.includes(name);
return { return {
label: <PluginListLabel name={name} frozen={frozen} hidden={hidden} version={version} />, label: (
<PluginListLabel
name={name}
frozen={frozen}
hidden={hidden}
version={version}
disabled={disabledPlugins.find((p) => p.name == name) !== undefined}
/>
),
position: pluginOrder.indexOf(name), position: pluginOrder.indexOf(name),
data: { data: {
name, name,
disabled: disabledPlugins.some((disabledPlugin) => disabledPlugin.name === name),
frozen, frozen,
hidden, hidden,
isDeveloper, isDeveloper,
@@ -186,9 +220,9 @@ export default function PluginList({ isDeveloper }: { isDeveloper: boolean }) {
}; };
}), }),
); );
}, [plugins, updates, hiddenPlugins]); }, [installedPlugins, updates, hiddenPlugins, disabledPlugins]);
if (plugins.length === 0) { if (installedPlugins.length === 0) {
return ( return (
<div> <div>
<p>{t('PluginListIndex.no_plugin')}</p> <p>{t('PluginListIndex.no_plugin')}</p>
@@ -4,6 +4,7 @@ import {
DialogControlsSection, DialogControlsSection,
Field, Field,
Focusable, Focusable,
NavEntryPositionPreferences,
Navigation, Navigation,
ProgressBar, ProgressBar,
SteamSpinner, SteamSpinner,
@@ -65,9 +66,9 @@ export default function TestingVersionList() {
if (testingVersions.length === 0) { if (testingVersions.length === 0) {
return ( return (
<div> <DialogBody>
<p>No open PRs found</p> <p>No open PRs found</p>
</div> </DialogBody>
); );
} }
@@ -79,15 +80,21 @@ export default function TestingVersionList() {
<ul style={{ listStyleType: 'none', padding: '0' }}> <ul style={{ listStyleType: 'none', padding: '0' }}>
{testingVersions.map((version) => { {testingVersions.map((version) => {
return ( return (
<li> <li key={`${version.id}_${version.name}`}>
<Field <Field
label={ label={
<> <>
{version.name} <span style={{ opacity: '50%' }}>{'#' + version.id}</span> {version.name}{' '}
<span style={{ opacity: '50%', whiteSpace: 'nowrap', marginLeft: 'auto', alignSelf: 'center' }}>
{'#' + version.id}
</span>
</> </>
} }
> >
<Focusable style={{ height: '40px', marginLeft: 'auto', display: 'flex' }}> <Focusable
style={{ height: '40px', marginLeft: 'auto', display: 'flex' }}
navEntryPreferPosition={NavEntryPositionPreferences.MAINTAIN_X}
>
<DialogButton <DialogButton
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }} style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
onClick={async () => { onClick={async () => {
+15 -4
View File
@@ -1,15 +1,23 @@
import { ButtonItem, Dropdown, Focusable, PanelSectionRow, SingleDropdownOption, SuspensefulImage } from '@decky/ui'; import {
ButtonItem,
Dropdown,
Focusable,
NavEntryPositionPreferences,
PanelSectionRow,
SingleDropdownOption,
SuspensefulImage,
} from '@decky/ui';
import { CSSProperties, FC, useState } from 'react'; import { CSSProperties, FC, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaArrowDown, FaArrowUp, FaCheck, FaDownload, FaRecycle } from 'react-icons/fa'; import { FaArrowDown, FaArrowUp, FaCheck, FaDownload, FaRecycle } from 'react-icons/fa';
import { InstallType, Plugin } from '../../plugin'; import { DisabledPlugin, InstallType, Plugin } from '../../plugin';
import { StorePlugin, requestPluginInstall } from '../../store'; import { StorePlugin, requestPluginInstall } from '../../store';
import ExternalLink from '../ExternalLink'; import ExternalLink from '../ExternalLink';
interface PluginCardProps { interface PluginCardProps {
storePlugin: StorePlugin; storePlugin: StorePlugin;
installedPlugin: Plugin | undefined; installedPlugin: Plugin | DisabledPlugin | undefined;
} }
const PluginCard: FC<PluginCardProps> = ({ storePlugin, installedPlugin }) => { const PluginCard: FC<PluginCardProps> = ({ storePlugin, installedPlugin }) => {
@@ -139,7 +147,10 @@ const PluginCard: FC<PluginCardProps> = ({ storePlugin, installedPlugin }) => {
</div> </div>
<div className="deckyStoreCardButtonRow"> <div className="deckyStoreCardButtonRow">
<PanelSectionRow> <PanelSectionRow>
<Focusable style={{ display: 'flex', gap: '5px', padding: 0 }}> <Focusable
style={{ display: 'flex', gap: '5px', padding: 0 }}
navEntryPreferPosition={NavEntryPositionPreferences.MAINTAIN_X}
>
<div <div
className="deckyStoreCardInstallContainer" className="deckyStoreCardInstallContainer"
style={ style={
+2 -1
View File
@@ -105,7 +105,7 @@ const BrowseTab: FC<{ setPluginCount: Dispatch<SetStateAction<number | null>> }>
})(); })();
}, []); }, []);
const { plugins: installedPlugins } = useDeckyState(); const { installedPlugins } = useDeckyState();
return ( return (
<> <>
@@ -240,6 +240,7 @@ const BrowseTab: FC<{ setPluginCount: Dispatch<SetStateAction<number | null>> }>
}) })
.map((plugin: StorePlugin) => ( .map((plugin: StorePlugin) => (
<PluginCard <PluginCard
key={`${plugin.id}_${plugin.name}`}
storePlugin={plugin} storePlugin={plugin}
installedPlugin={installedPlugins.find((installedPlugin) => installedPlugin.name === plugin.name)} installedPlugin={installedPlugins.find((installedPlugin) => installedPlugin.name === plugin.name)}
/> />
+1 -1
View File
@@ -24,7 +24,7 @@ class ErrorBoundaryHook extends Logger {
window.__ERRORBOUNDARY_HOOK_INSTANCE = this; window.__ERRORBOUNDARY_HOOK_INSTANCE = this;
// valve writes only the sanest of code // valve writes only the sanest of code
const exp = /^\(\)=>\(.\|\|.\(new .\),.\)$/; const exp = /^\(\)=>\(.\|\|(?:.\(|\(.=)new .\),.\)$/;
const initErrorReportingStore = findModuleExport( const initErrorReportingStore = findModuleExport(
(e) => typeof e == 'function' && e?.toString && exp.test(e.toString()), (e) => typeof e == 'function' && e?.toString && exp.test(e.toString()),
); );
+6 -4
View File
@@ -22,11 +22,13 @@
DFLWebpack.findModule((m) => m.createPortal && m.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE); DFLWebpack.findModule((m) => m.createPortal && m.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE);
console.debug('[Decky:Boot] Setting up JSX internals...'); console.debug('[Decky:Boot] Setting up JSX internals...');
const jsx = DFLWebpack.findModule((m) => m.jsx && Object.keys(m).length == 1)?.jsx; const jsxModule = DFLWebpack.findModule((m) => (m.jsx && m.jsxs) || (m.jsx && Object.keys(m).length == 1));
if (jsx) { if (jsxModule.jsxs) {
window.SP_JSX = jsxModule;
} else {
window.SP_JSX = { window.SP_JSX = {
jsx, jsx: jsxModule.jsx,
jsxs: jsx, jsxs: jsxModule.jsx,
Fragment: window.SP_REACT.Fragment, Fragment: window.SP_REACT.Fragment,
}; };
} }
+89 -15
View File
@@ -19,6 +19,7 @@ import { DeckyState, DeckyStateContextProvider, UserInfo, useDeckyState } from '
import { File, FileSelectionType } from './components/modals/filepicker'; import { File, FileSelectionType } from './components/modals/filepicker';
import { deinitFilepickerPatches, initFilepickerPatches } from './components/modals/filepicker/patches'; import { deinitFilepickerPatches, initFilepickerPatches } from './components/modals/filepicker/patches';
import MultiplePluginsInstallModal from './components/modals/MultiplePluginsInstallModal'; import MultiplePluginsInstallModal from './components/modals/MultiplePluginsInstallModal';
import PluginDisableModal from './components/modals/PluginDisableModal';
import PluginInstallModal from './components/modals/PluginInstallModal'; import PluginInstallModal from './components/modals/PluginInstallModal';
import PluginUninstallModal from './components/modals/PluginUninstallModal'; import PluginUninstallModal from './components/modals/PluginUninstallModal';
import NotificationBadge from './components/NotificationBadge'; import NotificationBadge from './components/NotificationBadge';
@@ -30,7 +31,7 @@ import { FrozenPluginService } from './frozen-plugins-service';
import { HiddenPluginsService } from './hidden-plugins-service'; import { HiddenPluginsService } from './hidden-plugins-service';
import Logger from './logger'; import Logger from './logger';
import { NotificationService } from './notification-service'; import { NotificationService } from './notification-service';
import { InstallType, Plugin, PluginLoadType } from './plugin'; import { DisabledPlugin, InstallType, Plugin, PluginLoadType } from './plugin';
import RouterHook from './router-hook'; import RouterHook from './router-hook';
import { deinitSteamFixes, initSteamFixes } from './steamfixes'; import { deinitSteamFixes, initSteamFixes } from './steamfixes';
import { checkForPluginUpdates } from './store'; import { checkForPluginUpdates } from './store';
@@ -91,6 +92,7 @@ class PluginLoader extends Logger {
DeckyBackend.addEventListener('loader/notify_updates', this.notifyUpdates.bind(this)); DeckyBackend.addEventListener('loader/notify_updates', this.notifyUpdates.bind(this));
DeckyBackend.addEventListener('loader/import_plugin', this.importPlugin.bind(this)); DeckyBackend.addEventListener('loader/import_plugin', this.importPlugin.bind(this));
DeckyBackend.addEventListener('loader/unload_plugin', this.unloadPlugin.bind(this)); DeckyBackend.addEventListener('loader/unload_plugin', this.unloadPlugin.bind(this));
DeckyBackend.addEventListener('loader/disable_plugin', this.doDisablePlugin.bind(this));
DeckyBackend.addEventListener('loader/add_plugin_install_prompt', this.addPluginInstallPrompt.bind(this)); DeckyBackend.addEventListener('loader/add_plugin_install_prompt', this.addPluginInstallPrompt.bind(this));
DeckyBackend.addEventListener( DeckyBackend.addEventListener(
'loader/add_multiple_plugins_install_prompt', 'loader/add_multiple_plugins_install_prompt',
@@ -175,7 +177,7 @@ class PluginLoader extends Logger {
private getPluginsFromBackend = DeckyBackend.callable< private getPluginsFromBackend = DeckyBackend.callable<
[], [],
{ name: string; version: string; load_type: PluginLoadType }[] { name: string; version: string; load_type: PluginLoadType; disabled: boolean }[]
>('loader/get_plugins'); >('loader/get_plugins');
private restartWebhelper = DeckyBackend.callable<[], void>('utilities/restart_webhelper'); private restartWebhelper = DeckyBackend.callable<[], void>('utilities/restart_webhelper');
@@ -198,10 +200,16 @@ class PluginLoader extends Logger {
this.runCrashChecker(); this.runCrashChecker();
const plugins = await this.getPluginsFromBackend(); const plugins = await this.getPluginsFromBackend();
const pluginLoadPromises = []; const pluginLoadPromises = [];
const disabledPlugins: DisabledPlugin[] = [];
const loadStart = performance.now(); const loadStart = performance.now();
for (const plugin of plugins) { for (const plugin of plugins) {
if (!this.hasPlugin(plugin.name)) if (plugin.disabled) {
pluginLoadPromises.push(this.importPlugin(plugin.name, plugin.version, plugin.load_type, false)); disabledPlugins.push({ name: plugin.name, version: plugin.version });
this.deckyState.setDisabledPlugins(disabledPlugins);
} else {
if (!this.hasPlugin(plugin.name))
pluginLoadPromises.push(this.importPlugin(plugin.name, plugin.version, plugin.load_type, false));
}
} }
await Promise.all(pluginLoadPromises); await Promise.all(pluginLoadPromises);
const loadEnd = performance.now(); const loadEnd = performance.now();
@@ -252,7 +260,9 @@ class PluginLoader extends Logger {
public async checkPluginUpdates() { public async checkPluginUpdates() {
const frozenPlugins = this.deckyState.publicState().frozenPlugins; const frozenPlugins = this.deckyState.publicState().frozenPlugins;
const updates = await checkForPluginUpdates(this.plugins.filter((p) => !frozenPlugins.includes(p.name))); const updates = await checkForPluginUpdates(
this.deckyState.publicState().installedPlugins.filter((p) => !frozenPlugins.includes(p.name)),
);
this.deckyState.setUpdates(updates); this.deckyState.setUpdates(updates);
return updates; return updates;
} }
@@ -290,6 +300,7 @@ class PluginLoader extends Logger {
version={version} version={version}
hash={hash} hash={hash}
installType={install_type} installType={install_type}
disabled={this.deckyState.publicState().disabledPlugins.some((p) => p.name === artifact)}
onOK={() => DeckyBackend.call<[string]>('utilities/confirm_plugin_install', request_id)} onOK={() => DeckyBackend.call<[string]>('utilities/confirm_plugin_install', request_id)}
onCancel={() => DeckyBackend.call<[string]>('utilities/cancel_plugin_install', request_id)} onCancel={() => DeckyBackend.call<[string]>('utilities/cancel_plugin_install', request_id)}
/>, />,
@@ -303,6 +314,7 @@ class PluginLoader extends Logger {
showModal( showModal(
<MultiplePluginsInstallModal <MultiplePluginsInstallModal
requests={requests} requests={requests}
disabledPlugins={this.deckyState.publicState().disabledPlugins}
onOK={() => DeckyBackend.call<[string]>('utilities/confirm_plugin_install', request_id)} onOK={() => DeckyBackend.call<[string]>('utilities/confirm_plugin_install', request_id)}
onCancel={() => DeckyBackend.call<[string]>('utilities/cancel_plugin_install', request_id)} onCancel={() => DeckyBackend.call<[string]>('utilities/cancel_plugin_install', request_id)}
/>, />,
@@ -310,7 +322,19 @@ class PluginLoader extends Logger {
} }
public uninstallPlugin(name: string, title: string, buttonText: string, description: string) { public uninstallPlugin(name: string, title: string, buttonText: string, description: string) {
showModal(<PluginUninstallModal name={name} title={title} buttonText={buttonText} description={description} />); showModal(
<PluginUninstallModal
name={name}
title={title}
buttonText={buttonText}
description={description}
deckyState={this.deckyState}
/>,
);
}
public disablePlugin(name: string, title: string, buttonText: string, description: string) {
showModal(<PluginDisableModal name={name} title={title} buttonText={buttonText} description={description} />);
} }
public hasPlugin(name: string) { public hasPlugin(name: string) {
@@ -351,6 +375,19 @@ class PluginLoader extends Logger {
this.errorBoundaryHook.deinit(); this.errorBoundaryHook.deinit();
} }
public doDisablePlugin(name: string) {
const plugin = this.plugins.find((plugin) => plugin.name === name);
if (plugin == undefined) return;
plugin?.onDismount?.();
this.plugins = this.plugins.filter((p) => p !== plugin);
this.deckyState.setDisabledPlugins([
...this.deckyState.publicState().disabledPlugins,
{ name: plugin.name, version: plugin.version },
]);
this.deckyState.setPlugins(this.plugins);
}
public unloadPlugin(name: string, skipStateUpdate: boolean = false) { public unloadPlugin(name: string, skipStateUpdate: boolean = false) {
const plugin = this.plugins.find((plugin) => plugin.name === name); const plugin = this.plugins.find((plugin) => plugin.name === name);
plugin?.onDismount?.(); plugin?.onDismount?.();
@@ -363,6 +400,7 @@ class PluginLoader extends Logger {
version?: string | undefined, version?: string | undefined,
loadType: PluginLoadType = PluginLoadType.ESMODULE_V1, loadType: PluginLoadType = PluginLoadType.ESMODULE_V1,
useQueue: boolean = true, useQueue: boolean = true,
timeoutMS?: number,
) { ) {
if (useQueue && this.reloadLock) { if (useQueue && this.reloadLock) {
this.log('Reload currently in progress, adding to queue', name); this.log('Reload currently in progress, adding to queue', name);
@@ -376,9 +414,11 @@ class PluginLoader extends Logger {
this.unloadPlugin(name, true); this.unloadPlugin(name, true);
const startTime = performance.now(); const startTime = performance.now();
await this.importReactPlugin(name, version, loadType);
await this.importReactPlugin(name, version, loadType, timeoutMS);
const endTime = performance.now(); const endTime = performance.now();
this.deckyState.setDisabledPlugins(this.deckyState.publicState().disabledPlugins.filter((d) => d.name !== name));
this.deckyState.setPlugins(this.plugins); this.deckyState.setPlugins(this.plugins);
this.log(`Loaded ${name} in ${endTime - startTime}ms`); this.log(`Loaded ${name} in ${endTime - startTime}ms`);
} catch (e) { } catch (e) {
@@ -388,7 +428,7 @@ class PluginLoader extends Logger {
this.reloadLock = false; this.reloadLock = false;
const nextPlugin = this.pluginReloadQueue.shift(); const nextPlugin = this.pluginReloadQueue.shift();
if (nextPlugin) { if (nextPlugin) {
this.importPlugin(nextPlugin.name, nextPlugin.version, loadType); this.importPlugin(nextPlugin.name, nextPlugin.version, nextPlugin.loadType, true, timeoutMS);
} }
} }
} }
@@ -398,12 +438,28 @@ class PluginLoader extends Logger {
name: string, name: string,
version?: string, version?: string,
loadType: PluginLoadType = PluginLoadType.ESMODULE_V1, loadType: PluginLoadType = PluginLoadType.ESMODULE_V1,
timeoutMS?: number,
) { ) {
let spExists = this.checkForSP(); let spExists = this.checkForSP();
const timeoutException = new Error(
`${name} failed to load within ${timeoutMS ? `${timeoutMS / 1000} second` : ''} time limit`,
);
let timeout: number | undefined;
try { try {
switch (loadType) { switch (loadType) {
case PluginLoadType.ESMODULE_V1: case PluginLoadType.ESMODULE_V1:
const plugin_exports = await import(`http://127.0.0.1:1337/plugins/${name}/dist/index.js?t=${Date.now()}`); const importJS = () => import(`http://127.0.0.1:1337/plugins/${name}/dist/index.js?t=${Date.now()}`);
const promise =
timeoutMS === undefined
? importJS()
: Promise.race([
importJS(),
new Promise((_, reject) => (timeout = setTimeout(() => reject(timeoutException), timeoutMS))),
]);
const plugin_exports = await promise;
let plugin = plugin_exports.default(); let plugin = plugin_exports.default();
this.plugins.push({ this.plugins.push({
@@ -415,12 +471,26 @@ class PluginLoader extends Logger {
break; break;
case PluginLoadType.LEGACY_EVAL_IIFE: case PluginLoadType.LEGACY_EVAL_IIFE:
let res = await fetch(`http://127.0.0.1:1337/plugins/${name}/frontend_bundle`, { const fetchJS = async () => {
credentials: 'include', const controller = new AbortController();
headers: { const { signal } = controller;
'X-Decky-Auth': deckyAuthToken,
}, if (timeoutMS !== undefined) timeout = setTimeout(() => controller.abort(), timeoutMS);
});
try {
return await fetch(`http://127.0.0.1:1337/plugins/${name}/frontend_bundle`, {
credentials: 'include',
headers: {
'X-Decky-Auth': deckyAuthToken,
},
signal,
});
} catch (e: any) {
throw 'name' in e && e.name === 'AbortError' ? timeoutException : e;
}
};
let res = await fetchJS();
if (res.ok) { if (res.ok) {
let plugin_export: (serverAPI: any) => Plugin = await eval( let plugin_export: (serverAPI: any) => Plugin = await eval(
(await res.text()) + `\n//# sourceURL=decky://decky/legacy_plugin/${encodeURIComponent(name)}/index.js`, (await res.text()) + `\n//# sourceURL=decky://decky/legacy_plugin/${encodeURIComponent(name)}/index.js`,
@@ -439,6 +509,8 @@ class PluginLoader extends Logger {
throw new Error(`${name} has no defined loadType.`); throw new Error(`${name} has no defined loadType.`);
} }
} catch (e) { } catch (e) {
if (e === timeoutException) throw timeoutException;
this.error('Error loading plugin ' + name, e); this.error('Error loading plugin ' + name, e);
const TheError: FC<{}> = () => ( const TheError: FC<{}> = () => (
<PanelSection> <PanelSection>
@@ -481,6 +553,8 @@ class PluginLoader extends Logger {
body: '' + e, body: '' + e,
icon: <FaExclamationCircle />, icon: <FaExclamationCircle />,
}); });
} finally {
if (timeout !== undefined) clearTimeout(timeout);
} }
if (spExists && !this.checkForSP()) { if (spExists && !this.checkForSP()) {
+4
View File
@@ -15,6 +15,8 @@ export interface Plugin {
titleView?: JSX.Element; titleView?: JSX.Element;
} }
export type DisabledPlugin = Pick<Plugin, 'name' | 'version'>;
export enum InstallType { export enum InstallType {
INSTALL, INSTALL,
REINSTALL, REINSTALL,
@@ -56,3 +58,5 @@ type installPluginsArgs = [
export let installPlugins = DeckyBackend.callable<installPluginsArgs>('utilities/install_plugins'); export let installPlugins = DeckyBackend.callable<installPluginsArgs>('utilities/install_plugins');
export let uninstallPlugin = DeckyBackend.callable<[name: string]>('utilities/uninstall_plugin'); export let uninstallPlugin = DeckyBackend.callable<[name: string]>('utilities/uninstall_plugin');
export let enablePlugin = DeckyBackend.callable<[name: string]>('utilities/enable_plugin');
export let disablePlugin = DeckyBackend.callable<[name: string]>('utilities/disable_plugin');
+7 -4
View File
@@ -1,6 +1,6 @@
import { compare } from 'compare-versions'; import { compare, validate } from 'compare-versions';
import { InstallType, Plugin, installPlugin, installPlugins } from './plugin'; import { DisabledPlugin, InstallType, Plugin, installPlugin, installPlugins } from './plugin';
import { getSetting, setSetting } from './utils/settings'; import { getSetting, setSetting } from './utils/settings';
export enum Store { export enum Store {
@@ -113,18 +113,21 @@ export async function requestMultiplePluginInstalls(requests: PluginInstallReque
); );
} }
export async function checkForPluginUpdates(plugins: Plugin[]): Promise<PluginUpdateMapping> { export async function checkForPluginUpdates(plugins: (Plugin | DisabledPlugin)[]): Promise<PluginUpdateMapping> {
const serverData = await getPluginList(); const serverData = await getPluginList();
const updateMap = new Map<string, StorePluginVersion>(); const updateMap = new Map<string, StorePluginVersion>();
for (let plugin of plugins) { for (let plugin of plugins) {
const remotePlugin = serverData?.find((x) => x.name == plugin.name); const remotePlugin = serverData?.find((x) => x.name == plugin.name);
//FIXME: Ugly hack since plugin.version might be null during evaluation, //FIXME: Ugly hack since plugin.version might be null during evaluation,
//so this will set the older version possible //so this will set the older version possible
const curVer = plugin.version ? plugin.version : '0.0'; const curVer = plugin.version ? plugin.version : '0.0.0';
if ( if (
remotePlugin && remotePlugin &&
remotePlugin.versions?.length > 0 && remotePlugin.versions?.length > 0 &&
plugin.version != remotePlugin?.versions?.[0]?.name && plugin.version != remotePlugin?.versions?.[0]?.name &&
validate(remotePlugin.versions?.[0]?.name) &&
validate(curVer) &&
compare(remotePlugin?.versions?.[0]?.name, curVer, '>') compare(remotePlugin?.versions?.[0]?.name, curVer, '>')
) { ) {
updateMap.set(plugin.name, remotePlugin.versions[0]); updateMap.set(plugin.name, remotePlugin.versions[0]);
+19 -6
View File
@@ -29,7 +29,8 @@ interface Tab {
class TabsHook extends Logger { class TabsHook extends Logger {
// private keys = 7; // private keys = 7;
tabs: Tab[] = []; tabs: Tab[] = [];
private qamPatch?: Patch; private qamBrowserViewPatch?: Patch;
private qamEmbeddedPatch?: Patch;
constructor() { constructor() {
super('TabsHook'); super('TabsHook');
@@ -40,11 +41,13 @@ class TabsHook extends Logger {
} }
init() { init() {
// TODO patch the "embedded" renderer in this module too (seems to be for VR? unsure)
const qamModule = findModuleByExport((e) => e?.type?.toString?.()?.includes('QuickAccessMenuBrowserView')); const qamModule = findModuleByExport((e) => e?.type?.toString?.()?.includes('QuickAccessMenuBrowserView'));
const qamRenderer = Object.values(qamModule).find((e: any) => const qamBrowserViewRenderer = Object.values(qamModule).find((e: any) =>
e?.type?.toString?.()?.includes('QuickAccessMenuBrowserView'), e?.type?.toString?.()?.includes('QuickAccessMenuBrowserView'),
); );
const qamEmbeddedRenderer = Object.values(qamModule).find((e: any) =>
e?.type?.toString?.()?.includes('QuickAccessMenuEmbedded'),
);
const patchHandler = createReactTreePatcher( const patchHandler = createReactTreePatcher(
[(tree) => findInReactTree(tree, (node) => node?.props?.onFocusNavDeactivated)], [(tree) => findInReactTree(tree, (node) => node?.props?.onFocusNavDeactivated)],
@@ -56,12 +59,21 @@ class TabsHook extends Logger {
'TabsHook', 'TabsHook',
); );
this.qamPatch = afterPatch(qamRenderer, 'type', patchHandler); this.qamBrowserViewPatch = afterPatch(qamBrowserViewRenderer, 'type', patchHandler);
if (qamEmbeddedRenderer) this.qamEmbeddedPatch = afterPatch(qamEmbeddedRenderer, 'type', patchHandler);
// Patch already rendered qam // Patch already rendered qam
const root = getReactRoot(document.getElementById('root') as any); const root = getReactRoot(document.getElementById('root') as any);
const qamNode = root && findInReactTree(root, (n: any) => n.elementType == qamRenderer); // need elementType, because type is actually mobx wrapper const qamNode =
root &&
findInReactTree(
root,
(n: any) =>
n.elementType == qamBrowserViewRenderer ||
(qamEmbeddedRenderer != null && n.elementType == qamEmbeddedRenderer),
); // need elementType, because type is actually mobx wrapper
if (qamNode) { if (qamNode) {
console.log('patching existing qam');
// Only affects this fiber node so we don't need to unpatch here // Only affects this fiber node so we don't need to unpatch here
qamNode.type = qamNode.elementType.type; qamNode.type = qamNode.elementType.type;
if (qamNode?.alternate) { if (qamNode?.alternate) {
@@ -71,7 +83,8 @@ class TabsHook extends Logger {
} }
deinit() { deinit() {
this.qamPatch?.unpatch(); this.qamBrowserViewPatch?.unpatch();
this.qamEmbeddedPatch?.unpatch();
} }
add(tab: Tab) { add(tab: Tab) {
+1
View File
@@ -81,6 +81,7 @@ class Toaster extends Logger {
const info = { const info = {
showToast: toast.showToast, showToast: toast.showToast,
sound: toast.sound, sound: toast.sound,
playSound: toast.playSound,
eFeature: 0, eFeature: 0,
toastDurationMS: toastData.nToastDurationMS, toastDurationMS: toastData.nToastDurationMS,
bCritical: toast.critical, bCritical: toast.critical,
+1 -1
View File
@@ -2,7 +2,7 @@
# Adapted from a script provided by Jaynator495. # Adapted from a script provided by Jaynator495.
# Make sure to place in home directory, chmod +x plugin-info.sh and then run with ./plugin-info.sh # Make sure to place in home directory, chmod +x plugin-info.sh and then run with ./plugin-info.sh
# Define the directory to scan # Define the directory to scan
directory_to_scan="~/homebrew/plugins" directory_to_scan="$HOME/homebrew/plugins"
# Loop through each subdirectory (one level deep) # Loop through each subdirectory (one level deep)
for dir in "$directory_to_scan"/*/; do for dir in "$directory_to_scan"/*/; do