mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-13 12:15:09 +03:00
Compare commits
23 Commits
main
..
beebls/motd
| Author | SHA1 | Date | |
|---|---|---|---|
| 86d01db2b9 | |||
| 50cb08cce9 | |||
| ef27046143 | |||
| 8bb4ff7118 | |||
| e2f36091e2 | |||
| 6eab1c1e16 | |||
| 002f0db04a | |||
| b85912691f | |||
| 7fff611d55 | |||
| 9b38abd13f | |||
| 120a43e55d | |||
| a5ce24405b | |||
| 3b00e4a792 | |||
| 1709a957f7 | |||
| b8adf165e5 | |||
| 428de00b29 | |||
| 5a02f5fbe7 | |||
| 83ae98a709 | |||
| 1a231bf03e | |||
| edf6b54db4 | |||
| ccdfd53648 | |||
| 267b11c9bf | |||
| 5a212e95fc |
@@ -36,7 +36,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install semver-tool asdf
|
||||
uses: asdf-vm/actions/install@v4
|
||||
uses: asdf-vm/actions/install@v3
|
||||
with:
|
||||
tool_versions: |
|
||||
semver 3.4.0
|
||||
|
||||
@@ -165,3 +165,6 @@ act/.directory
|
||||
act/artifacts/*
|
||||
bin/act
|
||||
/settings/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<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>
|
||||
<a name="download button" href="https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/decky_installer.desktop"><img src="./docs/images/download_button.svg" alt="Download decky" width="350px" style="padding-top: 15px;"></a>
|
||||
</h1>
|
||||
|
||||
<p align="center">
|
||||
@@ -15,21 +15,9 @@
|
||||
<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>
|
||||
|
||||
## 🩵 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
|
||||
|
||||
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/).
|
||||
@@ -52,9 +40,7 @@ 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).
|
||||
|
||||
## 💾 Installation
|
||||
|
||||
- This installation can be done without an admin/sudo password set.
|
||||
|
||||
1. Prepare a mouse and keyboard if possible.
|
||||
- Keyboards and mice can be connected to the Steam Deck via USB-C or Bluetooth.
|
||||
- Many Bluetooth keyboard and mouse apps are available for iOS and Android. (KDE connect is preinstalled on the steam deck)
|
||||
@@ -68,7 +54,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. Choose the version of Decky Loader you want to install.
|
||||
- **Latest Release**
|
||||
Intended for most users. This is the latest stable version of Decky Loader.
|
||||
Intended for most users. This is the latest stable version of Decky Loader.
|
||||
- **Latest Pre-Release**
|
||||
Intended for plugin developers. Pre-releases are unlikely to be fully stable but contain the latest changes. For more information on plugin development, please consult [the wiki page](https://wiki.deckbrew.xyz/en/loader-dev/development).
|
||||
1. Open the Return to Gaming Mode shortcut on your desktop.
|
||||
@@ -82,7 +68,6 @@ We are sorry to see you go! If you are considering uninstalling because you are
|
||||
1. Press the <img src="./docs/images/light/steam.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/steam.svg#gh-light-mode-only" height=16> button and open the Power menu.
|
||||
1. Select "Switch to Desktop".
|
||||
1. Run the installer file again, and select `uninstall decky loader`.
|
||||
|
||||
- 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
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
<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)的工作。
|
||||
@@ -150,7 +150,6 @@ class PluginBrowser:
|
||||
# plugins_snapshot = self.plugins.copy()
|
||||
# snapshot_string = pformat(plugins_snapshot)
|
||||
# logger.debug("current plugins: %s", snapshot_string)
|
||||
|
||||
if name in self.plugins:
|
||||
logger.debug("Plugin %s was found", name)
|
||||
await self.plugins[name].stop(uninstall=True)
|
||||
@@ -346,10 +345,5 @@ class PluginBrowser:
|
||||
if name in plugin_order:
|
||||
plugin_order.remove(name)
|
||||
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)
|
||||
|
||||
@@ -78,7 +78,6 @@ class Loader:
|
||||
self.live_reload = live_reload
|
||||
self.reload_queue: ReloadQueue = Queue()
|
||||
self.loop.create_task(self.handle_reloads())
|
||||
self.context: PluginManager = server_instance
|
||||
|
||||
if live_reload:
|
||||
self.observer = Observer()
|
||||
@@ -131,7 +130,7 @@ class Loader:
|
||||
|
||||
async def get_plugins(self):
|
||||
plugins = list(self.plugins.values())
|
||||
return [{"name": str(i), "version": i.version, "load_type": i.load_type, "disabled": i.disabled} for i in plugins]
|
||||
return [{"name": str(i), "version": i.version, "load_type": i.load_type} for i in plugins]
|
||||
|
||||
async def handle_plugin_dist(self, request: web.Request):
|
||||
plugin = self.plugins[request.match_info["plugin_name"]]
|
||||
@@ -165,10 +164,6 @@ class Loader:
|
||||
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)
|
||||
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 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")
|
||||
@@ -188,7 +183,7 @@ class Loader:
|
||||
print_exc()
|
||||
|
||||
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, True, 15000)
|
||||
await self.ws.emit("loader/import_plugin", name, version, load_type)
|
||||
|
||||
async def import_plugins(self):
|
||||
self.logger.info(f"import plugins from {self.plugin_path}")
|
||||
|
||||
@@ -102,7 +102,6 @@
|
||||
},
|
||||
"no_hash": "This plugin does not have a hash, you are installing it at your own risk.",
|
||||
"not_installed": "(not installed)",
|
||||
"disabled": "The plugin will be re-enabled after installation",
|
||||
"overwrite": {
|
||||
"button_idle": "Overwrite",
|
||||
"button_processing": "Overwriting",
|
||||
@@ -134,13 +133,10 @@
|
||||
"uninstall": "Uninstall",
|
||||
"update_all_one": "Update 1 plugin",
|
||||
"update_all_other": "Update {{count}} plugins",
|
||||
"update_to": "Update to {{name}}",
|
||||
"disable": "Disable",
|
||||
"enable": "Enable"
|
||||
"update_to": "Update to {{name}}"
|
||||
},
|
||||
"PluginListLabel": {
|
||||
"hidden": "Hidden from the quick access menu",
|
||||
"disabled": "Plugin disabled"
|
||||
"hidden": "Hidden from the quick access menu"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
@@ -156,23 +152,12 @@
|
||||
"desc": "Are you sure you want to 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_other": "Updates available for {{count}} plugins!"
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_one": "1 plugin is 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"
|
||||
"hidden_other": "{{count}} plugins are hidden from this list"
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
|
||||
@@ -52,9 +52,7 @@
|
||||
"MultiplePluginsInstallModal": {
|
||||
"confirm": "¿Estás seguro de que quieres hacer las siguientes modificaciones?",
|
||||
"description": {
|
||||
"downgrade": "Downgrade {{name}} a la {{version}}",
|
||||
"install": "Instalar {{name}} {{version}}",
|
||||
"overwrite": "Sobrescribir {{name}} con {{version}}",
|
||||
"reinstall": "Reinstalar {{name}} {{version}}",
|
||||
"update": "Actualizar {{name}} a {{version}}"
|
||||
},
|
||||
@@ -63,18 +61,12 @@
|
||||
"loading": "Trabajando"
|
||||
},
|
||||
"title": {
|
||||
"downgrade_many": "Downgrade {{count}} plugins",
|
||||
"downgrade_one": "Downgrade 1 plugin",
|
||||
"downgrade_other": "Downgrade {{count}} plugins",
|
||||
"install_many": "Instalar {{count}} plugins",
|
||||
"install_one": "Instalar 1 plugin",
|
||||
"install_other": "Instalar {{count}} plugins",
|
||||
"mixed_many": "Modificar {{count}} plugins",
|
||||
"mixed_one": "Modificar 1 plugin",
|
||||
"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_one": "Reinstalar 1 plugin",
|
||||
"reinstall_other": "Reinstalar {{count}} plugins",
|
||||
@@ -84,22 +76,12 @@
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_downgrade": "Downgrade",
|
||||
"plugin_full_access": "Este plugin tiene acceso completo a tu Steam Deck.",
|
||||
"plugin_install": "Instalar",
|
||||
"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"
|
||||
},
|
||||
"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": {
|
||||
"button_idle": "Instalar",
|
||||
"button_processing": "Instalando",
|
||||
@@ -107,13 +89,6 @@
|
||||
"title": "Instalar {{artifact}}"
|
||||
},
|
||||
"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": {
|
||||
"button_idle": "Reinstalar",
|
||||
"button_processing": "Reinstalando",
|
||||
@@ -123,7 +98,7 @@
|
||||
"update": {
|
||||
"button_idle": "Actualizar",
|
||||
"button_processing": "Actualizando",
|
||||
"desc": "¿Estás seguro de que quieres actualizar {{artifact}} a la {{version}}?",
|
||||
"desc": "¿Estás seguro de que quieres actualizar {{artifact}} {{version}}?",
|
||||
"title": "Actualizar {{artifact}}"
|
||||
}
|
||||
},
|
||||
@@ -231,7 +206,6 @@
|
||||
},
|
||||
"Store": {
|
||||
"download_progress_info": {
|
||||
"download_remote": "Descargando los binarios externos",
|
||||
"download_zip": "Descargando plugin",
|
||||
"increment_count": "Incrementando el contador de descargas",
|
||||
"installing_plugin": "Instalando plugin",
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
"MultiplePluginsInstallModal": {
|
||||
"confirm": "Sei sicuro di voler effettuare le modifiche seguenti?",
|
||||
"description": {
|
||||
"downgrade": "Declassa {{name}} a versione {{version}}",
|
||||
"downgrade": "Downgrada {{name}} a versione {{version}}",
|
||||
"install": "Installa {{name}} {{version}}",
|
||||
"overwrite": "Sovrascrive {{name}} con {{version}}",
|
||||
"reinstall": "Reinstalla {{name}} {{version}}",
|
||||
@@ -63,9 +63,9 @@
|
||||
"loading": "Elaboro"
|
||||
},
|
||||
"title": {
|
||||
"downgrade_many": "Declassa {{count}} plugins",
|
||||
"downgrade_one": "Declassa un plugin",
|
||||
"downgrade_other": "Declassa {{count}} plugins",
|
||||
"downgrade_many": "Downgrada {{count}} plugins",
|
||||
"downgrade_one": "Downgrada un plugin",
|
||||
"downgrade_other": "Downgrada {{count}} plugins",
|
||||
"install_many": "Installa {{count}} plugins",
|
||||
"install_one": "Installa un plugin",
|
||||
"install_other": "Installa {{count}} plugins",
|
||||
@@ -84,7 +84,7 @@
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_downgrade": "Declassa",
|
||||
"plugin_downgrade": "Downgrada",
|
||||
"plugin_full_access": "Questo plugin ha accesso completo al tuo Steam Deck.",
|
||||
"plugin_install": "Installa",
|
||||
"plugin_no_desc": "Nessuna descrizione fornita.",
|
||||
@@ -95,10 +95,10 @@
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"downgrade": {
|
||||
"button_idle": "Declassa",
|
||||
"button_processing": "Declassando",
|
||||
"desc": "Sei sicuro di voler declassare {{artifact}} alla versione {{version}}?",
|
||||
"title": "Declassa {{artifact}}"
|
||||
"button_idle": "Downgrada",
|
||||
"button_processing": "Downgradando",
|
||||
"desc": "Sei sicuro di voler downgradare {{artifact}} alla versione {{version}}?",
|
||||
"title": "Downgrada {{artifact}}"
|
||||
},
|
||||
"install": {
|
||||
"button_idle": "Installa",
|
||||
|
||||
@@ -215,7 +215,7 @@
|
||||
"download_progress_info": {
|
||||
"download_remote": "外部バイナリのダウンロード",
|
||||
"download_zip": "プラグインのダウンロード中",
|
||||
"increment_count": "ダウンロード進行中",
|
||||
"increment_count": "ダウンロード数の増加",
|
||||
"installing_plugin": "プラグインのインストール中",
|
||||
"open_zip": "zipファイルを展開中",
|
||||
"parse_zip": "zipファイルの解析中",
|
||||
|
||||
@@ -1,316 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,18 +63,15 @@
|
||||
"loading": "В процессе"
|
||||
},
|
||||
"title": {
|
||||
"downgrade_few": "Откатить {{count}} плагина",
|
||||
"downgrade_few": "Откатить {{count}} плагинов",
|
||||
"downgrade_many": "Откатить {{count}} плагинов",
|
||||
"downgrade_one": "Откатить {{count}} плагин",
|
||||
"downgrade_one": "Откатить 1 плагин",
|
||||
"install_few": "Установить {{count}} плагинов",
|
||||
"install_many": "Установить {{count}} плагинов",
|
||||
"install_one": "Установить {{count}} плагин",
|
||||
"mixed_few": "Изменить {{count}} плагинов",
|
||||
"mixed_many": "Изменить {{count}} плагинов",
|
||||
"mixed_one": "Изменить {{count}} плагин",
|
||||
"overwrite_few": "Перезаписать {{count}} плагина",
|
||||
"overwrite_many": "Перезаписать {{count}} плагинов",
|
||||
"overwrite_one": "Перезаписать {{count}} плагин",
|
||||
"reinstall_few": "Переустановить {{count}} плагинов",
|
||||
"reinstall_many": "Переустановить {{count}} плагинов",
|
||||
"reinstall_one": "Переустановить {{count}} плагин",
|
||||
@@ -96,7 +93,6 @@
|
||||
"PluginInstallModal": {
|
||||
"downgrade": {
|
||||
"button_idle": "Откат",
|
||||
"button_processing": "Откатывание",
|
||||
"desc": "Вы уверенны, что хотите откатить {{artifact}} до версии {{version}}?",
|
||||
"title": "Откатить {{artifact}}"
|
||||
},
|
||||
@@ -107,13 +103,6 @@
|
||||
"title": "Установить {{artifact}}"
|
||||
},
|
||||
"no_hash": "У данного плагина отсутствует хэш, устанавливайте на свой страх и риск.",
|
||||
"not_installed": "(не установлено)",
|
||||
"overwrite": {
|
||||
"button_idle": "Перезаписать",
|
||||
"button_processing": "Перезаписывание",
|
||||
"desc": "Вы уверены, что хотите перезаписать {{artifact}} версией {{version}}?",
|
||||
"title": "Перезаписать {{artifact}}"
|
||||
},
|
||||
"reinstall": {
|
||||
"button_idle": "Переустановить",
|
||||
"button_processing": "Переустановка",
|
||||
@@ -231,7 +220,6 @@
|
||||
},
|
||||
"Store": {
|
||||
"download_progress_info": {
|
||||
"download_remote": "Загрузка внешних бинарных файлов",
|
||||
"download_zip": "Скачивание плагина",
|
||||
"increment_count": "Увеличение количества загрузок",
|
||||
"installing_plugin": "Установка плагина",
|
||||
|
||||
@@ -12,49 +12,15 @@
|
||||
"disabling": "Вимкнення React DevTools",
|
||||
"enabling": "Увімкнення React DevTools"
|
||||
},
|
||||
"DropdownMultiselect": {
|
||||
"button": {
|
||||
"back": "Назад"
|
||||
}
|
||||
},
|
||||
"FilePickerError": {
|
||||
"errors": {
|
||||
"file_not_found": "Вказаний шлях недійсний. Будь ласка, перевірте його та введіть правильно.",
|
||||
"perm_denied": "У вас немає доступу до вказаного каталогу. Будь ласка, перевірте, чи має ваш користувач (deck на Steam Deck) відповідні права доступу до зазначеної папки/файлу.",
|
||||
"unknown": "Сталася невідома помилка. Деталі помилки: {{raw_error}}"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"label": "Папка",
|
||||
"select": "Використовувати цю папку",
|
||||
"show_more": "Показати більше файлів"
|
||||
"select": "Використовувати цю папку"
|
||||
}
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"confirm": "Ви впевнені, що хочете застосувати такі модифікації?",
|
||||
"description": {
|
||||
"downgrade": "Понизити {{name}} до версії {{version}}",
|
||||
"install": "Встановити {{name}} {{version}}",
|
||||
"overwrite": "Перезаписати {{name}} версією {{version}}",
|
||||
"reinstall": "Перевстановити {{name}} {{version}}",
|
||||
"update": "Оновити {{name}} до {{version}}"
|
||||
},
|
||||
@@ -63,43 +29,27 @@
|
||||
"loading": "Опрацювання"
|
||||
},
|
||||
"title": {
|
||||
"downgrade_few": "Понизити {{count}} плагіни",
|
||||
"downgrade_many": "Понизити {{count}} плагінів",
|
||||
"downgrade_one": "Понизити {{count}} плагін",
|
||||
"install_few": "Встановити {{count}} плагіни",
|
||||
"install_few": "Встановити {{count}} плагінів",
|
||||
"install_many": "Встановити {{count}} плагінів",
|
||||
"install_one": "Встановити {{count}} плагін",
|
||||
"mixed_few": "Модифікувати {{count}} плагіни",
|
||||
"mixed_many": "Модифікувати {{count}} плагінів",
|
||||
"mixed_one": "Модифікувати {{count}} плагін",
|
||||
"overwrite_few": "Перезаписати {{count}} плагіни",
|
||||
"overwrite_many": "Перезаписати {{count}} плагінів",
|
||||
"overwrite_one": "Перезаписати {{count}} плагін",
|
||||
"reinstall_few": "Перевстановити {{count}} плагіни",
|
||||
"install_one": "Встановити 1 плагін",
|
||||
"mixed_few": "Модифікувати {{count}} плагінів",
|
||||
"mixed_many": "",
|
||||
"mixed_one": "Модифікувати 1 плагін",
|
||||
"reinstall_few": "Перевстановити {{count}} плагінів",
|
||||
"reinstall_many": "Перевстановити {{count}} плагінів",
|
||||
"reinstall_one": "Перевстановити {{count}} плагін",
|
||||
"update_few": "Оновити {{count}} плагіни",
|
||||
"reinstall_one": "Перевстановити 1 плагін",
|
||||
"update_few": "Оновити {{count}} плагінів",
|
||||
"update_many": "Оновити {{count}} плагінів",
|
||||
"update_one": "Оновити {{count}} плагін"
|
||||
"update_one": "Оновити 1 плагін"
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_downgrade": "Понизити",
|
||||
"plugin_full_access": "Цей плагін має повний доступ до вашого Steam Deck.",
|
||||
"plugin_install": "Встановити",
|
||||
"plugin_no_desc": "Опис не надано.",
|
||||
"plugin_overwrite": "Перезаписати",
|
||||
"plugin_reinstall": "Перевстановити",
|
||||
"plugin_update": "Оновити",
|
||||
"plugin_version_label": "Версія плагіна"
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"downgrade": {
|
||||
"button_idle": "Понизити",
|
||||
"button_processing": "Пониження",
|
||||
"desc": "Ви впевнені, що хочете понизити {{artifact}} до версії {{version}}?",
|
||||
"title": "Понизити {{artifact}}"
|
||||
},
|
||||
"install": {
|
||||
"button_idle": "Встановити",
|
||||
"button_processing": "Встановлення",
|
||||
@@ -107,13 +57,6 @@
|
||||
"title": "Встановити {{artifact}}"
|
||||
},
|
||||
"no_hash": "Цей плагін не має хешу, ви встановлюєте його на власний ризик.",
|
||||
"not_installed": "(не встановлено)",
|
||||
"overwrite": {
|
||||
"button_idle": "Перезаписати",
|
||||
"button_processing": "Перезаписування",
|
||||
"desc": "Ви впевнені, що хочете перезаписати {{artifact}} версією {{version}}?",
|
||||
"title": "Перезаписати {{artifact}}"
|
||||
},
|
||||
"reinstall": {
|
||||
"button_idle": "Перевстановити",
|
||||
"button_processing": "Перевстановлення",
|
||||
@@ -123,23 +66,21 @@
|
||||
"update": {
|
||||
"button_idle": "Оновити",
|
||||
"button_processing": "Оновлення",
|
||||
"desc": "Ви впевнені, що хочете оновити {{artifact}} до версії {{version}}?",
|
||||
"desc": "Ви впевнені, що хочете оновити {{artifact}} {{version}}?",
|
||||
"title": "Оновити {{artifact}}"
|
||||
}
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"freeze": "Заморозити оновлення",
|
||||
"hide": "Швидкий доступ: Приховати",
|
||||
"no_plugin": "Плагінів не встановлено!",
|
||||
"plugin_actions": "Дії плагінів",
|
||||
"reinstall": "Перевстановити",
|
||||
"reload": "Перезавантажити",
|
||||
"show": "Швидкий доступ: Показати",
|
||||
"unfreeze": "Дозволити оновлення",
|
||||
"uninstall": "Видалити",
|
||||
"update_all_few": "Оновити {{count}} плагіни",
|
||||
"update_all_few": "Оновити {{count}} плагінів",
|
||||
"update_all_many": "Оновити {{count}} плагінів",
|
||||
"update_all_one": "Оновити {{count}} плагін",
|
||||
"update_all_one": "Оновити 1 плагін",
|
||||
"update_to": "Оновити {{name}}"
|
||||
},
|
||||
"PluginListLabel": {
|
||||
@@ -159,9 +100,9 @@
|
||||
"desc": "Ви впевнені, що хочете видалити {{name}}?",
|
||||
"title": "Видалити {{name}}"
|
||||
},
|
||||
"plugin_update_few": "Доступне оновлення для {{count}} плагіни!",
|
||||
"plugin_update_few": "Доступне оновлення для {{count}} плагінів!",
|
||||
"plugin_update_many": "Доступне оновлення для {{count}} плагінів!",
|
||||
"plugin_update_one": "Доступне оновлення для {{count}} плагіна!"
|
||||
"plugin_update_one": "Доступне оновлення для 1 плагіна!"
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_few": "{{count}} плагінів приховано з цього списку",
|
||||
@@ -211,11 +152,6 @@
|
||||
"developer_mode": {
|
||||
"label": "Розробницький режим"
|
||||
},
|
||||
"notifications": {
|
||||
"decky_updates_label": "Доступне оновлення Decky",
|
||||
"header": "Сповіщення",
|
||||
"plugin_updates_label": "Доступні оновлення плагінів"
|
||||
},
|
||||
"other": {
|
||||
"header": "Інше"
|
||||
},
|
||||
@@ -226,20 +162,9 @@
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Розробник",
|
||||
"general_title": "Загальне",
|
||||
"plugins_title": "Плагіни",
|
||||
"testing_title": "Тестування"
|
||||
"plugins_title": "Плагіни"
|
||||
},
|
||||
"Store": {
|
||||
"download_progress_info": {
|
||||
"download_remote": "Завантаження будь-яких зовнішніх бінарних файлів",
|
||||
"download_zip": "Завантаження плагіна",
|
||||
"increment_count": "Збільшення лічильника завантажень",
|
||||
"installing_plugin": "Встановлення плагіна",
|
||||
"open_zip": "Відкриття ZIP-файлу",
|
||||
"parse_zip": "Обробка ZIP-файлу",
|
||||
"start": "Ініціалізація",
|
||||
"uninstalling_previous": "Видалення попередньої копії"
|
||||
},
|
||||
"store_contrib": {
|
||||
"desc": "Якщо ви бажаєте додати щось у Decky Plugin Store, завітайте у репозиторій SteamDeckHomebrew/decky-plugin-template на GitHub. Інформація про розробку та поширення доступна у README.",
|
||||
"label": "Зробити внесок"
|
||||
@@ -263,17 +188,9 @@
|
||||
"about": "Інформація",
|
||||
"alph_asce": "За алфавітом (Z до A)",
|
||||
"alph_desc": "За алфавітом (A до Z)",
|
||||
"date_asce": "Спочатку найстаріші",
|
||||
"date_desc": "Спочатку найновіші",
|
||||
"downloads_asce": "Спочатку найменш завантажені",
|
||||
"downloads_desc": "Спочатку найчастіше завантажувані",
|
||||
"title": "Огляд"
|
||||
},
|
||||
"store_testing_cta": "Розгляньте можливість тестування нових плагінів, щоб допомогти команді Decky Loader!",
|
||||
"store_testing_warning": {
|
||||
"desc": "Ви можете використовувати цей канал магазину для тестування найновіших (експериментальних) версій плагінів. Обов’язково залишайте відгук на GitHub, щоб плагін можна було оновити для всіх користувачів.",
|
||||
"label": "Ласкаво просимо до каналу тестування магазину"
|
||||
}
|
||||
"store_testing_cta": "Розгляньте можливість тестування нових плагінів, щоб допомогти команді Decky Loader!"
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
@@ -287,17 +204,6 @@
|
||||
"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": {
|
||||
"decky_updates": "Оновлення Decky",
|
||||
"no_patch_notes_desc": "Немає нотаток до цієї версії",
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import platform, os
|
||||
|
||||
ON_WINDOWS = platform.system() == "Windows"
|
||||
ON_MAC = platform.system() == "Darwin"
|
||||
ON_LINUX = not ON_WINDOWS and not ON_MAC
|
||||
ON_LINUX = not ON_WINDOWS
|
||||
|
||||
if ON_WINDOWS:
|
||||
from .localplatformwin import *
|
||||
from . import localplatformwin as localplatform
|
||||
elif ON_MAC:
|
||||
from .localplatformmac import *
|
||||
from . import localplatformmac as localplatform
|
||||
else:
|
||||
from .localplatformlinux import *
|
||||
from . import localplatformlinux as localplatform
|
||||
|
||||
@@ -116,26 +116,28 @@ def get_username() -> str:
|
||||
return _get_user()
|
||||
|
||||
def setgid(user : UserType = UserType.HOST_USER):
|
||||
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:
|
||||
pass
|
||||
elif user == UserType.HOST_USER:
|
||||
os.setgid(host_user_group_id)
|
||||
user_id = 0
|
||||
|
||||
if user == UserType.HOST_USER:
|
||||
user_id = _get_user_group_id()
|
||||
elif user == UserType.EFFECTIVE_USER:
|
||||
os.setgid(effective_user_group_id)
|
||||
pass # we already are
|
||||
else:
|
||||
raise Exception("Unknown user type")
|
||||
|
||||
os.setgid(user_id)
|
||||
|
||||
def setuid(user : UserType = UserType.HOST_USER):
|
||||
host_user_id, effective_user_id = _get_user_id(), _get_effective_user_id()
|
||||
if host_user_id == effective_user_id:
|
||||
pass
|
||||
elif user == UserType.HOST_USER:
|
||||
os.setuid(host_user_id)
|
||||
user_id = 0
|
||||
|
||||
if user == UserType.HOST_USER:
|
||||
user_id = _get_user_id()
|
||||
elif user == UserType.EFFECTIVE_USER:
|
||||
os.setuid(effective_user_id)
|
||||
pass # we already are
|
||||
else:
|
||||
raise Exception("Unknown user type")
|
||||
|
||||
os.setuid(user_id)
|
||||
|
||||
async def service_active(service_name : str) -> bool:
|
||||
res, _, _ = await run(["systemctl", "is-active", service_name], stdout=DEVNULL, stderr=DEVNULL)
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
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
|
||||
@@ -41,7 +41,6 @@ class PluginWrapper:
|
||||
self.author = json["author"]
|
||||
self.flags = json["flags"]
|
||||
self.api_version = json["api_version"] if "api_version" in json else 0
|
||||
self.disabled = False
|
||||
|
||||
self.passive = not path.isfile(self.file)
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ from ..localplatform.localsocket import LocalSocket
|
||||
from ..localplatform.localplatform import setgid, setuid, get_username, get_home_path, ON_LINUX
|
||||
from ..enums import UserType
|
||||
from .. import helpers
|
||||
from .. import settings # pyright: ignore [reportUnusedImport]
|
||||
|
||||
from typing import List, TypeVar, Any
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from __future__ import annotations
|
||||
from os import path, stat_result
|
||||
from os import stat_result
|
||||
import uuid
|
||||
from urllib.parse import unquote
|
||||
from json.decoder import JSONDecodeError
|
||||
@@ -8,7 +8,7 @@ import re
|
||||
from traceback import format_exc
|
||||
from stat import FILE_ATTRIBUTE_HIDDEN # pyright: ignore [reportAttributeAccessIssue, reportUnknownVariableType]
|
||||
|
||||
from asyncio import StreamReader, StreamWriter, sleep, start_server, gather, open_connection
|
||||
from asyncio import StreamReader, StreamWriter, start_server, gather, open_connection
|
||||
from aiohttp import ClientSession, hdrs
|
||||
from aiohttp.web import Request, StreamResponse, Response, json_response, post
|
||||
from typing import TYPE_CHECKING, Callable, Coroutine, Dict, Any, List, TypedDict
|
||||
@@ -80,9 +80,6 @@ class Utilities:
|
||||
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/_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([
|
||||
post("/methods/{method_name}", self._handle_legacy_server_method_call)
|
||||
@@ -217,7 +214,7 @@ class Utilities:
|
||||
|
||||
async def http_request_legacy(self, method: str, url: str, extra_opts: Any = {}, timeout: int | None = None):
|
||||
async with ClientSession() as web:
|
||||
res = await web.request(method, url, ssl=helpers.get_ssl_context(), timeout=timeout, **extra_opts) # type: ignore
|
||||
res = await web.request(method, url, ssl=helpers.get_ssl_context(), timeout=timeout, **extra_opts)
|
||||
text = await res.text()
|
||||
return {
|
||||
"status": res.status,
|
||||
@@ -393,6 +390,7 @@ class Utilities:
|
||||
"total": len(all),
|
||||
}
|
||||
|
||||
|
||||
# Based on https://stackoverflow.com/a/46422554/13174603
|
||||
def start_rdt_proxy(self, ip: str, port: int):
|
||||
async def pipe(reader: StreamReader, writer: StreamWriter):
|
||||
@@ -476,41 +474,3 @@ class Utilities:
|
||||
|
||||
async def get_tab_id(self, name: str):
|
||||
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)
|
||||
+159
-24
@@ -1,27 +1,162 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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;
|
||||
}
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
.st1 {
|
||||
fill: #fff;
|
||||
}
|
||||
</style>
|
||||
<svg
|
||||
width="81.700577mm"
|
||||
height="24.334814mm"
|
||||
viewBox="0 0 81.700577 24.334814"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||
sodipodi:docname="download.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#ffffff"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="1"
|
||||
inkscape:deskcolor="#505050"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
inkscape:zoom="3.659624"
|
||||
inkscape:cx="115.44902"
|
||||
inkscape:cy="59.295709"
|
||||
inkscape:window-width="1827"
|
||||
inkscape:window-height="1233"
|
||||
inkscape:window-x="69"
|
||||
inkscape:window-y="38"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs2">
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient4494">
|
||||
<stop
|
||||
style="stop-color:#009fff;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop4490" />
|
||||
<stop
|
||||
style="stop-color:#ff1965;stop-opacity:1;"
|
||||
offset="0.79417855"
|
||||
id="stop4498" />
|
||||
<stop
|
||||
style="stop-color:#b9b500;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop4492" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient4494"
|
||||
id="linearGradient4496"
|
||||
x1="49.131042"
|
||||
y1="118.6573"
|
||||
x2="150.29259"
|
||||
y2="138.74957"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
spreadMethod="pad"
|
||||
gradientTransform="matrix(1.0500324,0,0,1,-1.6155884,24.621921)" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient4494"
|
||||
id="linearGradient13802"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1.0500324,0,0,1,-1.6155884,24.621921)"
|
||||
x1="49.131042"
|
||||
y1="118.6573"
|
||||
x2="150.29259"
|
||||
y2="138.74957"
|
||||
spreadMethod="pad" />
|
||||
</defs>
|
||||
<rect class="st0" x="0" y="0" width="176.36" height="38" rx="19" ry="19"/>
|
||||
<g>
|
||||
<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"/>
|
||||
<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"/>
|
||||
<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"/>
|
||||
<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"/>
|
||||
<path class="st1" d="M118.22,26.66V9.98h2.56v16.67h-2.56Z"/>
|
||||
<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"/>
|
||||
<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"/>
|
||||
<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"/>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-64.149712,-136.3326)">
|
||||
<rect
|
||||
style="mix-blend-mode:normal;fill:url(#linearGradient13802);fill-opacity:1;stroke:none;stroke-width:0.271121"
|
||||
id="rect111"
|
||||
width="81.700577"
|
||||
height="24.334814"
|
||||
x="64.149712"
|
||||
y="136.3326"
|
||||
ry="8.1781616" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:3.175px;fill:#000000;stroke:none;stroke-width:0.264583"
|
||||
x="66.364288"
|
||||
y="124.84658"
|
||||
id="text10382"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan10380"
|
||||
style="stroke-width:0.264583"
|
||||
x="66.364288"
|
||||
y="124.84658" /></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15.1694px;font-family:sans-serif;-inkscape-font-specification:sans-serif;white-space:pre;inline-size:82.6483;display:inline;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.264583"
|
||||
x="67.732498"
|
||||
y="126.05277"
|
||||
id="text10440"
|
||||
transform="translate(1.088576,28.135753)"><tspan
|
||||
x="67.732498"
|
||||
y="126.05277"
|
||||
id="tspan13872">Download</tspan></text>
|
||||
<rect
|
||||
style="mix-blend-mode:normal;fill:url(#linearGradient4496);fill-opacity:1;stroke:none;stroke-width:0.271121"
|
||||
id="rect13792"
|
||||
width="81.700577"
|
||||
height="24.334814"
|
||||
x="64.149712"
|
||||
y="136.3326"
|
||||
ry="8.1781616" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:3.175px;fill:#000000;stroke:none;stroke-width:0.264583"
|
||||
x="66.364288"
|
||||
y="124.84658"
|
||||
id="text13796"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan13794"
|
||||
style="stroke-width:0.264583"
|
||||
x="66.364288"
|
||||
y="124.84658" /></text>
|
||||
<g
|
||||
aria-label="Download"
|
||||
transform="translate(1.088576,28.135753)"
|
||||
id="text13800"
|
||||
style="font-size:15.1694px;-inkscape-font-specification:sans-serif;white-space:pre;inline-size:82.6483;display:inline;fill:#ffffff;stroke-width:0.264583">
|
||||
<path
|
||||
d="m 77.880751,120.53111 q 0,2.74566 -1.501771,4.14125 -1.486601,1.38041 -4.156416,1.38041 h -3.01871 v -10.83095 h 3.337268 q 1.638295,0 2.836678,0.60678 1.198382,0.60677 1.850666,1.78999 0.652285,1.16804 0.652285,2.91252 z m -1.441093,0.0455 q 0,-2.16923 -1.077028,-3.17041 -1.061858,-1.01635 -3.01871,-1.01635 H 70.5691 v 8.49487 h 1.471432 q 4.399126,0 4.399126,-4.30811 z"
|
||||
id="path13828" />
|
||||
<path
|
||||
d="m 87.164417,121.9722 q 0,2.01753 -1.03152,3.1249 -1.01635,1.10737 -2.760831,1.10737 -1.077027,0 -1.926513,-0.48542 -0.834317,-0.50059 -1.319738,-1.4411 -0.485421,-0.95567 -0.485421,-2.30575 0,-2.01753 1.01635,-3.10972 1.01635,-1.0922 2.760831,-1.0922 1.107366,0 1.941683,0.50059 0.849486,0.48542 1.319738,1.42592 0.485421,0.92534 0.485421,2.27541 z m -6.143608,0 q 0,1.4411 0.561268,2.29058 0.576437,0.83432 1.820328,0.83432 1.228722,0 1.805159,-0.83432 0.576437,-0.84948 0.576437,-2.29058 0,-1.44109 -0.576437,-2.26024 -0.576437,-0.81914 -1.820328,-0.81914 -1.243891,0 -1.805159,0.81914 -0.561268,0.81915 -0.561268,2.26024 z"
|
||||
id="path13830" />
|
||||
<path
|
||||
d="m 94.218174,121.45644 q -0.197202,-0.62194 -0.348896,-1.21355 -0.136525,-0.60677 -0.212372,-0.9405 h -0.06068 q -0.06068,0.33373 -0.197203,0.9405 -0.136524,0.59161 -0.348896,1.22872 l -1.456262,4.56599 h -1.51694 l -2.229902,-8.1308 h 1.380415 l 1.122536,4.33845 q 0.166863,0.65229 0.318557,1.31974 0.151694,0.66745 0.212372,1.10737 h 0.06068 q 0.06068,-0.25788 0.136525,-0.63712 0.09102,-0.37923 0.197202,-0.78881 0.106186,-0.42474 0.212372,-0.75847 l 1.441093,-4.58116 h 1.456262 l 1.395585,4.58116 q 0.166864,0.51576 0.318558,1.12254 0.166863,0.60678 0.227541,1.04669 h 0.06068 q 0.04551,-0.37924 0.197202,-1.04669 0.166864,-0.66745 0.348897,-1.36525 l 1.137705,-4.33845 h 1.365246 l -2.260241,8.1308 h -1.562448 z"
|
||||
id="path13832" />
|
||||
<path
|
||||
d="m 104.8064,117.77028 q 1.45627,0 2.19957,0.71296 0.7433,0.69779 0.7433,2.27541 v 5.29412 h -1.31974 v -5.2031 q 0,-1.95685 -1.82033,-1.95685 -1.35007,0 -1.86583,0.75847 -0.51576,0.75847 -0.51576,2.18439 v 4.21709 h -1.33491 v -8.1308 h 1.07703 l 0.1972,1.10737 h 0.0759 q 0.3944,-0.63711 1.09219,-0.9405 0.69779,-0.31856 1.47143,-0.31856 z"
|
||||
id="path13834" />
|
||||
<path
|
||||
d="m 111.6023,126.05277 h -1.33491 v -11.52874 h 1.33491 z"
|
||||
id="path13836" />
|
||||
<path
|
||||
d="m 121.25003,121.9722 q 0,2.01753 -1.03152,3.1249 -1.01635,1.10737 -2.76084,1.10737 -1.07702,0 -1.92651,-0.48542 -0.83432,-0.50059 -1.31974,-1.4411 -0.48542,-0.95567 -0.48542,-2.30575 0,-2.01753 1.01635,-3.10972 1.01635,-1.0922 2.76083,-1.0922 1.10737,0 1.94169,0.50059 0.84948,0.48542 1.31973,1.42592 0.48543,0.92534 0.48543,2.27541 z m -6.14361,0 q 0,1.4411 0.56127,2.29058 0.57643,0.83432 1.82032,0.83432 1.22873,0 1.80516,-0.83432 0.57644,-0.84948 0.57644,-2.29058 0,-1.44109 -0.57644,-2.26024 -0.57643,-0.81914 -1.82033,-0.81914 -1.24389,0 -1.80515,0.81914 -0.56127,0.81915 -0.56127,2.26024 z"
|
||||
id="path13838" />
|
||||
<path
|
||||
d="m 126.43796,117.78545 q 1.4866,0 2.19956,0.65228 0.71296,0.65229 0.71296,2.07821 v 5.53683 h -0.97084 l -0.25788,-1.15287 h -0.0607 q -0.53093,0.66745 -1.12253,0.98601 -0.57644,0.31856 -1.60796,0.31856 -1.10737,0 -1.8355,-0.57644 -0.72813,-0.59161 -0.72813,-1.8355 0,-1.21355 0.95567,-1.86583 0.95567,-0.66746 2.94287,-0.72814 l 1.38041,-0.0455 v -0.48542 q 0,-1.01635 -0.43991,-1.41076 -0.43991,-0.3944 -1.24389,-0.3944 -0.63712,0 -1.21355,0.1972 -0.57644,0.18203 -1.07703,0.42474 l -0.40957,-1.00118 q 0.53092,-0.28822 1.25906,-0.48542 0.72813,-0.21237 1.51694,-0.21237 z m 0.3944,4.33845 q -1.51694,0.0607 -2.10855,0.48542 -0.57643,0.42474 -0.57643,1.19838 0,0.68262 0.40957,1.00118 0.42474,0.31856 1.07703,0.31856 1.03152,0 1.71414,-0.56127 0.68262,-0.57644 0.68262,-1.75965 v -0.72813 z"
|
||||
id="path13840" />
|
||||
<path
|
||||
d="m 134.7508,126.20447 q -1.51694,0 -2.42711,-1.04669 -0.91016,-1.06186 -0.91016,-3.15524 0,-2.09337 0.91016,-3.15523 0.92534,-1.07703 2.44228,-1.07703 0.9405,0 1.53211,0.3489 0.60677,0.34889 0.98601,0.84948 h 0.091 q -0.0152,-0.1972 -0.0607,-0.57643 -0.0303,-0.39441 -0.0303,-0.62195 v -3.24625 h 1.3349 v 11.52874 h -1.07702 l -0.19721,-1.09219 h -0.0607 q -0.36407,0.51576 -0.97084,0.87982 -0.60678,0.36407 -1.56245,0.36407 z m 0.21237,-1.10737 q 1.2894,0 1.80516,-0.69779 0.53093,-0.71296 0.53093,-2.13889 v -0.24271 q 0,-1.51694 -0.50059,-2.32092 -0.50059,-0.81914 -1.85067,-0.81914 -1.07703,0 -1.62313,0.86465 -0.53093,0.84949 -0.53093,2.29058 0,1.45626 0.53093,2.26024 0.5461,0.80398 1.6383,0.80398 z"
|
||||
id="path13842" />
|
||||
</g>
|
||||
</g>
|
||||
<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>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 8.9 KiB |
@@ -13,15 +13,15 @@
|
||||
"localize": "i18next"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@decky/api": "^1.1.3",
|
||||
"@decky/api": "^1.1.1",
|
||||
"@rollup/plugin-commonjs": "^26.0.1",
|
||||
"@rollup/plugin-image": "^3.0.3",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@rollup/plugin-replace": "^5.0.7",
|
||||
"@rollup/plugin-typescript": "^11.1.6",
|
||||
"@types/react": "19.1.1",
|
||||
"@types/react-dom": "19.1.1",
|
||||
"@types/react": "18.3.3",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"@types/react-file-icon": "^1.0.4",
|
||||
"@types/react-router": "5.1.20",
|
||||
"husky": "^9.0.11",
|
||||
@@ -30,8 +30,8 @@
|
||||
"inquirer": "^9.2.23",
|
||||
"prettier": "^3.3.2",
|
||||
"prettier-plugin-import-sort": "^0.0.7",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"rollup": "^4.22.4",
|
||||
"rollup-plugin-delete": "^2.0.0",
|
||||
"rollup-plugin-external-globals": "^0.10.0",
|
||||
@@ -47,13 +47,13 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@decky/ui": "^4.11.6",
|
||||
"@decky/ui": "^4.10.4",
|
||||
"compare-versions": "^6.1.1",
|
||||
"filesize": "^10.1.2",
|
||||
"i18next": "^25.6.0",
|
||||
"i18next": "^23.11.5",
|
||||
"i18next-http-backend": "^2.5.2",
|
||||
"react-file-icon": "^1.6.0",
|
||||
"react-i18next": "^16.0.1",
|
||||
"react-file-icon": "^1.5.0",
|
||||
"react-i18next": "^14.1.2",
|
||||
"react-icons": "^5.2.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
"remark-gfm": "^4.0.0"
|
||||
|
||||
Generated
+82
-97
@@ -9,8 +9,8 @@ importers:
|
||||
.:
|
||||
dependencies:
|
||||
'@decky/ui':
|
||||
specifier: ^4.11.6
|
||||
version: 4.11.6
|
||||
specifier: ^4.10.4
|
||||
version: 4.10.4
|
||||
compare-versions:
|
||||
specifier: ^6.1.1
|
||||
version: 6.1.1
|
||||
@@ -18,30 +18,30 @@ importers:
|
||||
specifier: ^10.1.2
|
||||
version: 10.1.2
|
||||
i18next:
|
||||
specifier: ^25.6.0
|
||||
version: 25.6.0(typescript@5.4.5)
|
||||
specifier: ^23.11.5
|
||||
version: 23.11.5
|
||||
i18next-http-backend:
|
||||
specifier: ^2.5.2
|
||||
version: 2.5.2
|
||||
react-file-icon:
|
||||
specifier: ^1.6.0
|
||||
version: 1.6.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
specifier: ^1.5.0
|
||||
version: 1.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
react-i18next:
|
||||
specifier: ^16.0.1
|
||||
version: 16.0.1(i18next@25.6.0(typescript@5.4.5))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.4.5)
|
||||
specifier: ^14.1.2
|
||||
version: 14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
react-icons:
|
||||
specifier: ^5.2.1
|
||||
version: 5.2.1(react@19.1.1)
|
||||
version: 5.2.1(react@18.3.1)
|
||||
react-markdown:
|
||||
specifier: ^9.0.1
|
||||
version: 9.0.1(@types/react@19.1.1)(react@19.1.1)
|
||||
version: 9.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||
remark-gfm:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
devDependencies:
|
||||
'@decky/api':
|
||||
specifier: ^1.1.3
|
||||
version: 1.1.3
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1
|
||||
'@rollup/plugin-commonjs':
|
||||
specifier: ^26.0.1
|
||||
version: 26.0.1(rollup@4.22.4)
|
||||
@@ -61,11 +61,11 @@ importers:
|
||||
specifier: ^11.1.6
|
||||
version: 11.1.6(rollup@4.22.4)(tslib@2.6.3)(typescript@5.4.5)
|
||||
'@types/react':
|
||||
specifier: 19.1.1
|
||||
version: 19.1.1
|
||||
specifier: 18.3.3
|
||||
version: 18.3.3
|
||||
'@types/react-dom':
|
||||
specifier: 19.1.1
|
||||
version: 19.1.1(@types/react@19.1.1)
|
||||
specifier: 18.3.0
|
||||
version: 18.3.0
|
||||
'@types/react-file-icon':
|
||||
specifier: ^1.0.4
|
||||
version: 1.0.4
|
||||
@@ -91,11 +91,11 @@ importers:
|
||||
specifier: ^0.0.7
|
||||
version: 0.0.7(prettier@3.3.2)
|
||||
react:
|
||||
specifier: 19.1.1
|
||||
version: 19.1.1
|
||||
specifier: 18.3.1
|
||||
version: 18.3.1
|
||||
react-dom:
|
||||
specifier: 19.1.1
|
||||
version: 19.1.1(react@19.1.1)
|
||||
specifier: 18.3.1
|
||||
version: 18.3.1(react@18.3.1)
|
||||
rollup:
|
||||
specifier: ^4.22.4
|
||||
version: 4.22.4
|
||||
@@ -203,10 +203,6 @@ packages:
|
||||
resolution: {integrity: sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/runtime@7.28.4':
|
||||
resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/template@7.24.7':
|
||||
resolution: {integrity: sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -219,11 +215,11 @@ packages:
|
||||
resolution: {integrity: sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@decky/api@1.1.3':
|
||||
resolution: {integrity: sha512-XsPCZxfxk5I1UtylIUN3qaWQI31siQbKfbLIskkI5innEatY1m4NQqBv/6hwPaO9mKMbdqYpnh5PSJDeMEOOBA==}
|
||||
'@decky/api@1.1.1':
|
||||
resolution: {integrity: sha512-R5fkBRHBt5QIQY7Q0AlbVIhlIZ/nTzwBOoi8Rt4Go2fjFnoMKPInCJl6cPjXzimGwl2pyqKJgY6VnH6ar0XrHQ==}
|
||||
|
||||
'@decky/ui@4.11.6':
|
||||
resolution: {integrity: sha512-vPCr2/KODeM6DAzIL/XN2e/RY7vhebXoWoh8e0VvB5QJU59Usb1z/cIpNmqe/GEMd1P3om6DFMcpEW5v8Se95Q==}
|
||||
'@decky/ui@4.10.4':
|
||||
resolution: {integrity: sha512-swgC4IVtQzZVw8dtP/iztpNYUl1eR0dxWfiMpswY8YglDsBn4ntspbL91Ic4WgxvkOEMSpsIs+zkVtjHE9zi3A==}
|
||||
|
||||
'@esbuild/aix-ppc64@0.20.2':
|
||||
resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==}
|
||||
@@ -602,10 +598,11 @@ packages:
|
||||
'@types/node@20.14.2':
|
||||
resolution: {integrity: sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==}
|
||||
|
||||
'@types/react-dom@19.1.1':
|
||||
resolution: {integrity: sha512-jFf/woGTVTjUJsl2O7hcopJ1r0upqoq/vIOoCj0yLh3RIXxWcljlpuZ+vEBRXsymD1jhfeJrlyTy/S1UW+4y1w==}
|
||||
peerDependencies:
|
||||
'@types/react': ^19.0.0
|
||||
'@types/prop-types@15.7.12':
|
||||
resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==}
|
||||
|
||||
'@types/react-dom@18.3.0':
|
||||
resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==}
|
||||
|
||||
'@types/react-file-icon@1.0.4':
|
||||
resolution: {integrity: sha512-c1mIklUDaxm9odxf8RTiy/EAxsblZliJ86EKIOAyuafP9eK3iudyn4ATv53DX6ZvgGymc7IttVNm97LTGnTiYA==}
|
||||
@@ -613,8 +610,8 @@ packages:
|
||||
'@types/react-router@5.1.20':
|
||||
resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==}
|
||||
|
||||
'@types/react@19.1.1':
|
||||
resolution: {integrity: sha512-ePapxDL7qrgqSF67s0h9m412d9DbXyC1n59O2st+9rjuuamWsZuD2w55rqY12CbzsZ7uVXb5Nw0gEp9Z8MMutQ==}
|
||||
'@types/react@18.3.3':
|
||||
resolution: {integrity: sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==}
|
||||
|
||||
'@types/resolve@1.20.2':
|
||||
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
|
||||
@@ -1190,16 +1187,8 @@ packages:
|
||||
engines: {node: '>=18.0.0 || >=20.0.0 || >=22.0.0', npm: '>=6', yarn: '>=1'}
|
||||
hasBin: true
|
||||
|
||||
i18next@23.16.8:
|
||||
resolution: {integrity: sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==}
|
||||
|
||||
i18next@25.6.0:
|
||||
resolution: {integrity: sha512-tTn8fLrwBYtnclpL5aPXK/tAYBLWVvoHM1zdfXoRNLcI+RvtMsoZRV98ePlaW3khHYKuNh/Q65W/+NVFUeIwVw==}
|
||||
peerDependencies:
|
||||
typescript: ^5
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
i18next@23.11.5:
|
||||
resolution: {integrity: sha512-41pvpVbW9rhZPk5xjCX2TPJi2861LEig/YRhUkY+1FQ2IQPS0bKUDYnEqY8XPPbB48h1uIwLnP9iiEfuSl20CA==}
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
||||
@@ -1720,32 +1709,29 @@ packages:
|
||||
quick-temp@0.1.8:
|
||||
resolution: {integrity: sha512-YsmIFfD9j2zaFwJkzI6eMG7y0lQP7YeWzgtFgNl38pGWZBSXJooZbOWwkcRot7Vt0Fg9L23pX0tqWU3VvLDsiA==}
|
||||
|
||||
react-dom@19.1.1:
|
||||
resolution: {integrity: sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==}
|
||||
react-dom@18.3.1:
|
||||
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
|
||||
peerDependencies:
|
||||
react: ^19.1.1
|
||||
react: ^18.3.1
|
||||
|
||||
react-file-icon@1.6.0:
|
||||
resolution: {integrity: sha512-Ba4Qa2ya/kvhcCd4LJja77sV7JD7u1ZXcI1DUz+TII3nGmglG6QY+NZeHizThokgct3qI0glwb9eV8NqRGs5lw==}
|
||||
react-file-icon@1.5.0:
|
||||
resolution: {integrity: sha512-6K2/nAI69CS838HOS+4S95MLXwf1neWywek1FgqcTFPTYjnM8XT7aBLz4gkjoqQKY9qPhu3A2tu+lvxhmZYY9w==}
|
||||
peerDependencies:
|
||||
react: ^19.0.0 || ^18.0.0 || ^17.0.0 || ^16.2.0
|
||||
react-dom: ^19.0.0 || ^18.0.0 || ^17.0.0 || ^16.2.0
|
||||
react: ^18.0.0 || ^17.0.0 || ^16.2.0
|
||||
react-dom: ^18.0.0 || ^17.0.0 || ^16.2.0
|
||||
|
||||
react-i18next@16.0.1:
|
||||
resolution: {integrity: sha512-0S//bpYEkCPjzuVmxDf9Z6+Y+ArNvpAUk7eDL4qNCZXjDh6Z9j6MZ+NThU7kMCOsmYmDCun3GYEwkiOjjZo9Ug==}
|
||||
react-i18next@14.1.2:
|
||||
resolution: {integrity: sha512-FSIcJy6oauJbGEXfhUgVeLzvWBhIBIS+/9c6Lj4niwKZyGaGb4V4vUbATXSlsHJDXXB+ociNxqFNiFuV1gmoqg==}
|
||||
peerDependencies:
|
||||
i18next: '>= 25.5.2'
|
||||
i18next: '>= 23.2.3'
|
||||
react: '>= 16.8.0'
|
||||
react-dom: '*'
|
||||
react-native: '*'
|
||||
typescript: ^5
|
||||
peerDependenciesMeta:
|
||||
react-dom:
|
||||
optional: true
|
||||
react-native:
|
||||
optional: true
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
react-icons@5.2.1:
|
||||
resolution: {integrity: sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw==}
|
||||
@@ -1761,8 +1747,8 @@ packages:
|
||||
'@types/react': '>=18'
|
||||
react: '>=18'
|
||||
|
||||
react@19.1.1:
|
||||
resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==}
|
||||
react@18.3.1:
|
||||
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
readable-stream@2.3.8:
|
||||
@@ -1887,8 +1873,8 @@ packages:
|
||||
safer-buffer@2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
|
||||
scheduler@0.26.0:
|
||||
resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
|
||||
scheduler@0.23.2:
|
||||
resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
|
||||
|
||||
semver@6.3.1:
|
||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||
@@ -2280,8 +2266,6 @@ snapshots:
|
||||
dependencies:
|
||||
regenerator-runtime: 0.14.1
|
||||
|
||||
'@babel/runtime@7.28.4': {}
|
||||
|
||||
'@babel/template@7.24.7':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.24.7
|
||||
@@ -2309,9 +2293,9 @@ snapshots:
|
||||
'@babel/helper-validator-identifier': 7.24.7
|
||||
to-fast-properties: 2.0.0
|
||||
|
||||
'@decky/api@1.1.3': {}
|
||||
'@decky/api@1.1.1': {}
|
||||
|
||||
'@decky/ui@4.11.6': {}
|
||||
'@decky/ui@4.10.4': {}
|
||||
|
||||
'@esbuild/aix-ppc64@0.20.2':
|
||||
optional: true
|
||||
@@ -2583,21 +2567,24 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 5.26.5
|
||||
|
||||
'@types/react-dom@19.1.1(@types/react@19.1.1)':
|
||||
'@types/prop-types@15.7.12': {}
|
||||
|
||||
'@types/react-dom@18.3.0':
|
||||
dependencies:
|
||||
'@types/react': 19.1.1
|
||||
'@types/react': 18.3.3
|
||||
|
||||
'@types/react-file-icon@1.0.4':
|
||||
dependencies:
|
||||
'@types/react': 19.1.1
|
||||
'@types/react': 18.3.3
|
||||
|
||||
'@types/react-router@5.1.20':
|
||||
dependencies:
|
||||
'@types/history': 4.7.11
|
||||
'@types/react': 19.1.1
|
||||
'@types/react': 18.3.3
|
||||
|
||||
'@types/react@19.1.1':
|
||||
'@types/react@18.3.3':
|
||||
dependencies:
|
||||
'@types/prop-types': 15.7.12
|
||||
csstype: 3.1.3
|
||||
|
||||
'@types/resolve@1.20.2': {}
|
||||
@@ -3242,7 +3229,7 @@ snapshots:
|
||||
esbuild: 0.20.2
|
||||
fs-extra: 11.2.0
|
||||
gulp-sort: 2.0.0
|
||||
i18next: 23.16.8
|
||||
i18next: 23.11.5
|
||||
js-yaml: 4.1.0
|
||||
lilconfig: 3.1.2
|
||||
rsvp: 4.8.5
|
||||
@@ -3253,16 +3240,10 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
i18next@23.16.8:
|
||||
i18next@23.11.5:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.24.7
|
||||
|
||||
i18next@25.6.0(typescript@5.4.5):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.4
|
||||
optionalDependencies:
|
||||
typescript: 5.4.5
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
@@ -3979,43 +3960,43 @@ snapshots:
|
||||
rimraf: 2.7.1
|
||||
underscore.string: 3.3.6
|
||||
|
||||
react-dom@19.1.1(react@19.1.1):
|
||||
react-dom@18.3.1(react@18.3.1):
|
||||
dependencies:
|
||||
react: 19.1.1
|
||||
scheduler: 0.26.0
|
||||
loose-envify: 1.4.0
|
||||
react: 18.3.1
|
||||
scheduler: 0.23.2
|
||||
|
||||
react-file-icon@1.6.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
|
||||
react-file-icon@1.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
colord: 2.9.3
|
||||
prop-types: 15.8.1
|
||||
react: 19.1.1
|
||||
react-dom: 19.1.1(react@19.1.1)
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
react-i18next@16.0.1(i18next@25.6.0(typescript@5.4.5))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.4.5):
|
||||
react-i18next@14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.4
|
||||
'@babel/runtime': 7.24.7
|
||||
html-parse-stringify: 3.0.1
|
||||
i18next: 25.6.0(typescript@5.4.5)
|
||||
react: 19.1.1
|
||||
i18next: 23.11.5
|
||||
react: 18.3.1
|
||||
optionalDependencies:
|
||||
react-dom: 19.1.1(react@19.1.1)
|
||||
typescript: 5.4.5
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
react-icons@5.2.1(react@19.1.1):
|
||||
react-icons@5.2.1(react@18.3.1):
|
||||
dependencies:
|
||||
react: 19.1.1
|
||||
react: 18.3.1
|
||||
|
||||
react-is@16.13.1: {}
|
||||
|
||||
react-markdown@9.0.1(@types/react@19.1.1)(react@19.1.1):
|
||||
react-markdown@9.0.1(@types/react@18.3.3)(react@18.3.1):
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
'@types/react': 19.1.1
|
||||
'@types/react': 18.3.3
|
||||
devlop: 1.1.0
|
||||
hast-util-to-jsx-runtime: 2.3.0
|
||||
html-url-attributes: 3.0.0
|
||||
mdast-util-to-hast: 13.2.0
|
||||
react: 19.1.1
|
||||
react: 18.3.1
|
||||
remark-parse: 11.0.0
|
||||
remark-rehype: 11.1.0
|
||||
unified: 11.0.4
|
||||
@@ -4024,7 +4005,9 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
react@19.1.1: {}
|
||||
react@18.3.1:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
|
||||
readable-stream@2.3.8:
|
||||
dependencies:
|
||||
@@ -4181,7 +4164,9 @@ snapshots:
|
||||
|
||||
safer-buffer@2.1.2: {}
|
||||
|
||||
scheduler@0.26.0: {}
|
||||
scheduler@0.23.2:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
|
||||
semver@6.3.1: {}
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
minimumReleaseAgeExclude:
|
||||
- "@decky/api"
|
||||
- "@decky/ui"
|
||||
@@ -23,7 +23,6 @@ export default defineConfig([
|
||||
}),
|
||||
externalGlobals({
|
||||
react: 'SP_REACT',
|
||||
'react/jsx-runtime': 'SP_JSX',
|
||||
'react-dom': 'SP_REACTDOM',
|
||||
// hack to shut up react-markdown
|
||||
process: '{cwd: () => {}}',
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
import { DialogButton, Focusable, ModalRoot, PanelSection, ScrollPanelGroup, showModal } from '@decky/ui';
|
||||
import { lazy, useEffect, useMemo, useState } from 'react';
|
||||
import { FaInfo, FaTimes } from 'react-icons/fa';
|
||||
|
||||
import { Announcement, getAnnouncements } from '../store';
|
||||
import { useSetting } from '../utils/hooks/useSetting';
|
||||
import WithSuspense from './WithSuspense';
|
||||
|
||||
const SEVERITIES = {
|
||||
High: {
|
||||
color: '#bb1414',
|
||||
text: '#fff',
|
||||
},
|
||||
Medium: {
|
||||
color: '#bbbb14',
|
||||
text: '#fff',
|
||||
},
|
||||
Low: {
|
||||
color: '#1488bb',
|
||||
text: '#fff',
|
||||
},
|
||||
};
|
||||
|
||||
const welcomeAnnouncement: Announcement = {
|
||||
id: 'welcomeAnnouncement',
|
||||
title: 'Welcome to Decky!',
|
||||
text: 'We hope you enjoy using Decky! If you have any questions or feedback, please let us know.',
|
||||
created: Date.now().toString(),
|
||||
updated: Date.now().toString(),
|
||||
};
|
||||
|
||||
const welcomeAnnouncement2: Announcement = {
|
||||
id: 'welcomeAnnouncement2',
|
||||
title: 'Test With mkdown content and a slightly long title',
|
||||
text: '# Lorem Ipsum\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n\n## Features\n\n- **Bold text** for emphasis\n- *Italic text* for style\n- `Code snippets` for technical content\n\n### Getting Started\n\nUt enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n\n> This is a blockquote with some important information.\n\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.',
|
||||
created: Date.now().toString(),
|
||||
updated: Date.now().toString(),
|
||||
};
|
||||
|
||||
export function AnnouncementsDisplay() {
|
||||
const [announcements, setAnnouncements] = useState<Announcement[]>([welcomeAnnouncement, welcomeAnnouncement2]);
|
||||
const [hiddenAnnouncementIds, setHiddenAnnouncementIds] = useSetting<string[]>('hiddenAnnouncementIds', []);
|
||||
|
||||
function addAnnouncements(newAnnouncements: Announcement[]) {
|
||||
// Removes any duplicates and sorts by created date
|
||||
setAnnouncements((oldAnnouncements) => {
|
||||
const newArr = [...oldAnnouncements, ...newAnnouncements];
|
||||
const setOfIds = new Set(newArr.map((a) => a.id));
|
||||
return (
|
||||
(
|
||||
Array.from(setOfIds)
|
||||
.map((id) => newArr.find((a) => a.id === id))
|
||||
// Typescript doesn't type filter(Boolean) correctly, so I have to assert this
|
||||
.filter(Boolean) as Announcement[]
|
||||
).sort((a, b) => {
|
||||
return new Date(b.created).getTime() - new Date(a.created).getTime();
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchAnnouncement() {
|
||||
const announcements = await getAnnouncements();
|
||||
announcements && addAnnouncements(announcements);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void fetchAnnouncement();
|
||||
}, []);
|
||||
|
||||
const currentlyDisplayingAnnouncements: Announcement[] = useMemo(() => {
|
||||
return announcements.filter((announcement) => !hiddenAnnouncementIds.includes(announcement.id));
|
||||
}, [announcements, hiddenAnnouncementIds]);
|
||||
|
||||
function hideAnnouncement(id: string) {
|
||||
setHiddenAnnouncementIds([...hiddenAnnouncementIds, id]);
|
||||
void fetchAnnouncement();
|
||||
}
|
||||
|
||||
if (currentlyDisplayingAnnouncements.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<PanelSection>
|
||||
<Focusable style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
||||
{currentlyDisplayingAnnouncements.map((announcement) => (
|
||||
<Announcement
|
||||
key={announcement.id}
|
||||
announcement={announcement}
|
||||
onHide={() => hideAnnouncement(announcement.id)}
|
||||
/>
|
||||
))}
|
||||
</Focusable>
|
||||
</PanelSection>
|
||||
);
|
||||
}
|
||||
|
||||
function Announcement({ announcement, onHide }: { announcement: Announcement; onHide: () => void }) {
|
||||
// Severity is not implemented in the API currently
|
||||
const severity = SEVERITIES['Low'];
|
||||
return (
|
||||
<Focusable
|
||||
style={{
|
||||
// Transparency is 20% of the color
|
||||
backgroundColor: `${severity.color}33`,
|
||||
color: severity.text,
|
||||
borderColor: severity.color,
|
||||
borderWidth: '2px',
|
||||
borderStyle: 'solid',
|
||||
padding: '0.7rem',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontWeight: 'bold' }}>{announcement.title}</span>
|
||||
<Focusable style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||
<DialogButton
|
||||
style={{
|
||||
width: '1rem',
|
||||
minWidth: '1rem',
|
||||
height: '1rem',
|
||||
padding: '0',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
onClick={() =>
|
||||
showModal(
|
||||
<AnnouncementModal
|
||||
announcement={announcement}
|
||||
onHide={() => {
|
||||
onHide();
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
}
|
||||
>
|
||||
<FaInfo
|
||||
style={{
|
||||
height: '.75rem',
|
||||
}}
|
||||
/>
|
||||
</DialogButton>
|
||||
<DialogButton
|
||||
style={{
|
||||
width: '1rem',
|
||||
minWidth: '1rem',
|
||||
height: '1rem',
|
||||
padding: '0',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
onClick={() => onHide()}
|
||||
>
|
||||
<FaTimes
|
||||
style={{
|
||||
height: '.75rem',
|
||||
}}
|
||||
/>
|
||||
</DialogButton>
|
||||
</Focusable>
|
||||
</Focusable>
|
||||
);
|
||||
}
|
||||
|
||||
const MarkdownRenderer = lazy(() => import('./Markdown'));
|
||||
|
||||
function AnnouncementModal({
|
||||
announcement,
|
||||
closeModal,
|
||||
onHide,
|
||||
}: {
|
||||
announcement: Announcement;
|
||||
closeModal?: () => void;
|
||||
onHide: () => void;
|
||||
}) {
|
||||
return (
|
||||
<ModalRoot onCancel={closeModal} onEscKeypress={closeModal}>
|
||||
<style>
|
||||
{`
|
||||
.steam-focus {
|
||||
outline-offset: 3px;
|
||||
outline: 2px solid rgba(255, 255, 255, 0.6);
|
||||
animation: pulseOutline 1.2s infinite ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes pulseOutline {
|
||||
0% {
|
||||
outline: 2px solid rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
50% {
|
||||
outline: 2px solid rgba(255, 255, 255, 1);
|
||||
}
|
||||
100% {
|
||||
outline: 2px solid rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<Focusable style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem', height: 'calc(100vh - 200px)' }}>
|
||||
<span style={{ fontWeight: 'bold', fontSize: '1.25rem' }}>{announcement.title}</span>
|
||||
<span style={{ opacity: 0.5 }}>Use your finger to scroll</span>
|
||||
<ScrollPanelGroup
|
||||
// @ts-ignore
|
||||
focusable={false}
|
||||
style={{ flex: 1, height: '100%' }}
|
||||
// onCancelButton doesn't work here
|
||||
onCancelActionDescription="Back"
|
||||
onButtonDown={(evt: any) => {
|
||||
if (!evt?.detail?.button) return;
|
||||
if (evt.detail.button === 2) {
|
||||
closeModal?.();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<WithSuspense>
|
||||
<MarkdownRenderer
|
||||
onDismiss={() => {
|
||||
closeModal?.();
|
||||
}}
|
||||
>
|
||||
{announcement.text}
|
||||
</MarkdownRenderer>
|
||||
</WithSuspense>
|
||||
</ScrollPanelGroup>
|
||||
<Focusable style={{ display: 'flex', gap: '0.5rem' }}>
|
||||
<DialogButton onClick={() => closeModal?.()}>Close</DialogButton>
|
||||
<DialogButton
|
||||
onClick={() => {
|
||||
onHide();
|
||||
closeModal?.();
|
||||
}}
|
||||
>
|
||||
Close and Hide Announcement
|
||||
</DialogButton>
|
||||
</Focusable>
|
||||
</Focusable>
|
||||
</ModalRoot>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { joinClassNames, sleep } from '@decky/ui';
|
||||
import { sleep } from '@decky/ui';
|
||||
import { FunctionComponent, useEffect, useReducer, useState } from 'react';
|
||||
|
||||
import { disablePlugin, uninstallPlugin } from '../plugin';
|
||||
import { uninstallPlugin } from '../plugin';
|
||||
import { VerInfo, doRestart, doShutdown } from '../updater';
|
||||
import { ValveReactErrorInfo, getLikelyErrorSourceFromValveReactError } from '../utils/errors';
|
||||
import { useSetting } from '../utils/hooks/useSetting';
|
||||
@@ -20,26 +20,6 @@ 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 starrCEFForwarding = DeckyBackend.callable('utilities/allow_remote_debugging');
|
||||
|
||||
@@ -84,130 +64,39 @@ const DeckyErrorBoundary: FunctionComponent<DeckyErrorBoundaryProps> = ({ error,
|
||||
<>
|
||||
<style>
|
||||
{`
|
||||
*:has(> .${classes.root}) {
|
||||
margin-top: var(--basicui-header-height);
|
||||
*:has(> .deckyErrorBoundary) {
|
||||
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>
|
||||
<div className={classes.root}>
|
||||
<div className={classes.marginBottom}>An error occurred while rendering this content.</div>
|
||||
<pre className={joinClassNames(classes.marginBottom)} style={{ marginTop: '0px' }}>
|
||||
<div
|
||||
style={{
|
||||
overflow: 'auto',
|
||||
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>
|
||||
{identifier && `Error Reference: ${identifier}`}
|
||||
{versionInfo?.current && `\nDecky Version: ${versionInfo.current}`}
|
||||
</code>
|
||||
</pre>
|
||||
<div className={joinClassNames(classes.likelyOccurred, classes.marginBottom)}>
|
||||
This error likely occurred in {errorSource}.
|
||||
</div>
|
||||
<p>This error likely occured in {errorSource}.</p>
|
||||
{actionLog?.length > 0 && (
|
||||
<pre>
|
||||
<code>
|
||||
@@ -217,88 +106,142 @@ const DeckyErrorBoundary: FunctionComponent<DeckyErrorBoundaryProps> = ({ error,
|
||||
</pre>
|
||||
)}
|
||||
{actionsEnabled && (
|
||||
<div className={classes.panel}>
|
||||
<div className={classes.flexRowWGap} style={{ alignItems: 'center', marginBottom: '8px' }}>
|
||||
<div className={classes.panelHeader}>Actions</div>
|
||||
<div style={{ fontSize: 'small', fontStyle: 'italic' }}>
|
||||
Use the touch screen. Solutions are listed in the recommended order. If you are still experiencing
|
||||
issues, please post in the #loader-support channel at decky.xyz/discord.
|
||||
</div>
|
||||
<>
|
||||
<h3>Actions: </h3>
|
||||
<p>Use the touch screen.</p>
|
||||
<div style={{ display: 'block', marginBottom: '5px' }}>
|
||||
<button style={{ marginRight: '5px', padding: '5px' }} onClick={reset}>
|
||||
Retry
|
||||
</button>
|
||||
<button
|
||||
style={{ marginRight: '5px', padding: '5px' }}
|
||||
onClick={() => {
|
||||
addLogLine('Restarting Steam...');
|
||||
SteamClient.User.StartRestart(false);
|
||||
}}
|
||||
>
|
||||
Restart Steam
|
||||
</button>
|
||||
</div>
|
||||
<div className={classes.rowList}>
|
||||
<div className={joinClassNames(classes.rowItem, classes.buttonDescRow)}>
|
||||
Retry the action or restart
|
||||
<div className={classes.flexRowWGap}>
|
||||
<button onClick={reset}>Retry</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
addLogLine('Restarting Steam...');
|
||||
SteamClient.User.StartRestart(false);
|
||||
}}
|
||||
>
|
||||
Restart Steam
|
||||
</button>
|
||||
<button
|
||||
onClick={async () => {
|
||||
setActionsEnabled(false);
|
||||
addLogLine('Restarting Decky...');
|
||||
doRestart();
|
||||
await sleep(2000);
|
||||
addLogLine('Reloading UI...');
|
||||
}}
|
||||
>
|
||||
Restart Decky
|
||||
</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>
|
||||
{wasCausedByPlugin && (
|
||||
<div className={joinClassNames(classes.rowItem, classes.buttonDescRow)}>
|
||||
Disable or uninstall the suspected plugin
|
||||
<div className={classes.flexRowWGap}>
|
||||
<button
|
||||
onClick={async () => {
|
||||
setActionsEnabled(false);
|
||||
addLogLine(`Disabling ${errorSource}...`);
|
||||
await disablePlugin(errorSource);
|
||||
await sleep(1000);
|
||||
addLogLine('Restarting Decky...');
|
||||
doRestart();
|
||||
await sleep(2000);
|
||||
addLogLine('Restarting Steam...');
|
||||
await sleep(500);
|
||||
SteamClient.User.StartRestart(false);
|
||||
)}
|
||||
{
|
||||
<div style={{ display: 'block', marginBottom: '5px' }}>
|
||||
{updateProgress > -1
|
||||
? 'Update in progress... ' + updateProgress + '%'
|
||||
: updateProgress == -2
|
||||
? 'Update complete. Restarting...'
|
||||
: 'Changing your Decky Loader branch and/or \n checking for updates might help!\n'}
|
||||
{updateProgress == -1 && (
|
||||
<div style={{ height: '30px' }}>
|
||||
<select
|
||||
style={{ height: '100%' }}
|
||||
onChange={async (e) => {
|
||||
const branch = parseInt(e.target.value);
|
||||
setSelectedBranch(branch);
|
||||
setSetVersionToUpdateTo('');
|
||||
}}
|
||||
>
|
||||
Disable {errorSource}
|
||||
</button>
|
||||
<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
|
||||
style={{ height: '100%' }}
|
||||
disabled={updateProgress != -1 || isChecking}
|
||||
onClick={async () => {
|
||||
setActionsEnabled(false);
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Uninstall {errorSource}
|
||||
{' '}
|
||||
{isChecking
|
||||
? 'Checking for updates...'
|
||||
: versionToUpdateTo != ''
|
||||
? 'Update to ' + versionToUpdateTo
|
||||
: 'Check for updates'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={joinClassNames(classes.rowItem, classes.buttonDescRow)}>
|
||||
Disable all plugins
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
{wasCausedByPlugin && (
|
||||
<div style={{ display: 'block', marginBottom: '5px' }}>
|
||||
{'\n'}
|
||||
<button
|
||||
style={{ marginRight: '5px', padding: '5px' }}
|
||||
onClick={async () => {
|
||||
setActionsEnabled(false);
|
||||
addLogLine(`Disabling plugins...`);
|
||||
await DeckyBackend.call('utilities/set_all_plugins_disabled');
|
||||
addLogLine(`Uninstalling ${errorSource}...`);
|
||||
await uninstallPlugin(errorSource);
|
||||
await DeckyPluginLoader.frozenPluginsService.invalidate();
|
||||
await DeckyPluginLoader.hiddenPluginsService.invalidate();
|
||||
await sleep(1000);
|
||||
addLogLine('Restarting Decky...');
|
||||
doRestart();
|
||||
@@ -308,134 +251,27 @@ const DeckyErrorBoundary: FunctionComponent<DeckyErrorBoundaryProps> = ({ error,
|
||||
SteamClient.User.StartRestart(false);
|
||||
}}
|
||||
>
|
||||
Disable All Plugins
|
||||
Uninstall {errorSource} and restart Decky
|
||||
</button>
|
||||
</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 && (
|
||||
<div className={classes.swipePrompt}>
|
||||
<span>Swipe to scroll</span>
|
||||
</div>
|
||||
)}
|
||||
<div className={classes.panel}>
|
||||
<div className={classes.panelHeader}>Trace</div>
|
||||
<pre
|
||||
style={{
|
||||
margin: `8px calc(-1 * ${vars.panelXPadding})`,
|
||||
userSelect: 'auto',
|
||||
overflowX: 'scroll',
|
||||
padding: `0px ${vars.panelXPadding}`,
|
||||
maskImage: `linear-gradient(to right, transparent, black ${vars.panelXPadding}, black calc(100% - ${vars.panelXPadding}), transparent)`,
|
||||
}}
|
||||
>
|
||||
<code>
|
||||
{error.error.stack}
|
||||
{'\n\n'}
|
||||
Component Stack:
|
||||
{error.info.componentStack}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<pre
|
||||
style={{
|
||||
marginTop: '15px',
|
||||
opacity: 0.7,
|
||||
userSelect: 'auto',
|
||||
}}
|
||||
>
|
||||
<code>
|
||||
{error.error.stack}
|
||||
{'\n\n'}
|
||||
Component Stack:
|
||||
{error.info.componentStack}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { FC, ReactNode, createContext, useContext, useEffect, useState } from 'react';
|
||||
|
||||
import { DEFAULT_NOTIFICATION_SETTINGS, NotificationSettings } from '../notification-service';
|
||||
import { DisabledPlugin, Plugin } from '../plugin';
|
||||
import { Plugin } from '../plugin';
|
||||
import { PluginUpdateMapping } from '../store';
|
||||
import { VerInfo } from '../updater';
|
||||
|
||||
interface PublicDeckyState {
|
||||
plugins: Plugin[];
|
||||
disabledPlugins: DisabledPlugin[];
|
||||
installedPlugins: (Plugin | DisabledPlugin)[];
|
||||
pluginOrder: string[];
|
||||
frozenPlugins: string[];
|
||||
hiddenPlugins: string[];
|
||||
@@ -28,8 +26,6 @@ export interface UserInfo {
|
||||
|
||||
export class DeckyState {
|
||||
private _plugins: Plugin[] = [];
|
||||
private _disabledPlugins: DisabledPlugin[] = [];
|
||||
private _installedPlugins: (Plugin | DisabledPlugin)[] = [];
|
||||
private _pluginOrder: string[] = [];
|
||||
private _frozenPlugins: string[] = [];
|
||||
private _hiddenPlugins: string[] = [];
|
||||
@@ -46,8 +42,6 @@ export class DeckyState {
|
||||
publicState(): PublicDeckyState {
|
||||
return {
|
||||
plugins: this._plugins,
|
||||
disabledPlugins: this._disabledPlugins,
|
||||
installedPlugins: this._installedPlugins,
|
||||
pluginOrder: this._pluginOrder,
|
||||
frozenPlugins: this._frozenPlugins,
|
||||
hiddenPlugins: this._hiddenPlugins,
|
||||
@@ -68,13 +62,6 @@ export class DeckyState {
|
||||
|
||||
setPlugins(plugins: Plugin[]) {
|
||||
this._plugins = plugins;
|
||||
this._installedPlugins = [...plugins, ...this._disabledPlugins];
|
||||
this.notifyUpdate();
|
||||
}
|
||||
|
||||
setDisabledPlugins(disabledPlugins: DisabledPlugin[]) {
|
||||
this._disabledPlugins = disabledPlugins;
|
||||
this._installedPlugins = [...this._plugins, ...disabledPlugins];
|
||||
this.notifyUpdate();
|
||||
}
|
||||
|
||||
@@ -138,7 +125,6 @@ interface DeckyStateContext extends PublicDeckyState {
|
||||
setIsLoaderUpdating(hasUpdate: boolean): void;
|
||||
setActivePlugin(name: string): void;
|
||||
setPluginOrder(pluginOrder: string[]): void;
|
||||
setDisabledPlugins(disabled: DisabledPlugin[]): void;
|
||||
closeActivePlugin(): void;
|
||||
}
|
||||
|
||||
@@ -177,7 +163,6 @@ export const DeckyStateContextProvider: FC<Props> = ({ children, deckyState }) =
|
||||
const setActivePlugin = deckyState.setActivePlugin.bind(deckyState);
|
||||
const closeActivePlugin = deckyState.closeActivePlugin.bind(deckyState);
|
||||
const setPluginOrder = deckyState.setPluginOrder.bind(deckyState);
|
||||
const setDisabledPlugins = deckyState.setDisabledPlugins.bind(deckyState);
|
||||
|
||||
return (
|
||||
<DeckyStateContext.Provider
|
||||
@@ -188,7 +173,6 @@ export const DeckyStateContextProvider: FC<Props> = ({ children, deckyState }) =
|
||||
setActivePlugin,
|
||||
closeActivePlugin,
|
||||
setPluginOrder,
|
||||
setDisabledPlugins,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,24 +1,17 @@
|
||||
import { ButtonItem, ErrorBoundary, Focusable, PanelSection, PanelSectionRow } from '@decky/ui';
|
||||
import { FC, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaBan, FaEyeSlash } from 'react-icons/fa';
|
||||
import { FaEyeSlash } from 'react-icons/fa';
|
||||
|
||||
import { AnnouncementsDisplay } from './AnnouncementsDisplay';
|
||||
import { useDeckyState } from './DeckyState';
|
||||
import NotificationBadge from './NotificationBadge';
|
||||
import { useQuickAccessVisible } from './QuickAccessVisibleState';
|
||||
import TitleView from './TitleView';
|
||||
|
||||
const PluginView: FC = () => {
|
||||
const {
|
||||
plugins,
|
||||
disabledPlugins,
|
||||
hiddenPlugins,
|
||||
updates,
|
||||
activePlugin,
|
||||
pluginOrder,
|
||||
setActivePlugin,
|
||||
closeActivePlugin,
|
||||
} = useDeckyState();
|
||||
const { plugins, hiddenPlugins, updates, activePlugin, pluginOrder, setActivePlugin, closeActivePlugin } =
|
||||
useDeckyState();
|
||||
const visible = useQuickAccessVisible();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -29,9 +22,7 @@ const PluginView: FC = () => {
|
||||
.sort((a, b) => pluginOrder.indexOf(a.name) - pluginOrder.indexOf(b.name))
|
||||
.filter((p) => p.content)
|
||||
.filter(({ name }) => !hiddenPlugins.includes(name));
|
||||
}, [plugins, pluginOrder, hiddenPlugins]);
|
||||
|
||||
const numberOfHidden = hiddenPlugins.filter((name) => !!plugins.find((p) => p.name === name)).length;
|
||||
}, [plugins, pluginOrder]);
|
||||
|
||||
if (activePlugin) {
|
||||
return (
|
||||
@@ -51,6 +42,7 @@ const PluginView: FC = () => {
|
||||
paddingTop: '16px',
|
||||
}}
|
||||
>
|
||||
<AnnouncementsDisplay />
|
||||
<PanelSection>
|
||||
{pluginList.map(({ name, icon }) => (
|
||||
<PanelSectionRow key={name}>
|
||||
@@ -63,28 +55,12 @@ const PluginView: FC = () => {
|
||||
</ButtonItem>
|
||||
</PanelSectionRow>
|
||||
))}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
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>
|
||||
{hiddenPlugins.length > 0 && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', fontSize: '0.8rem', marginTop: '10px' }}>
|
||||
<FaEyeSlash />
|
||||
<div>{t('PluginView.hidden', { count: hiddenPlugins.length })}</div>
|
||||
</div>
|
||||
)}
|
||||
</PanelSection>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { FC, PropsWithChildren, createContext, useContext, useState } from 'react';
|
||||
import { FC, ReactNode, createContext, useContext, useState } from 'react';
|
||||
|
||||
const QuickAccessVisibleState = createContext<boolean>(false);
|
||||
|
||||
export const useQuickAccessVisible = () => useContext(QuickAccessVisibleState);
|
||||
|
||||
export const QuickAccessVisibleStateProvider: FC<PropsWithChildren<{ tab: any }>> = ({ children, tab }) => {
|
||||
export const QuickAccessVisibleStateProvider: FC<{ tab: any; children: ReactNode }> = ({ children, tab }) => {
|
||||
const initial = tab.initialVisibility;
|
||||
const [visible, setVisible] = useState<boolean>(initial);
|
||||
// HACK but i can't think of a better way to do this
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useDeckyState } from './DeckyState';
|
||||
|
||||
const titleStyles: CSSProperties = {
|
||||
display: 'flex',
|
||||
paddingTop: '3px',
|
||||
paddingRight: '16px',
|
||||
position: 'sticky',
|
||||
top: '0px',
|
||||
|
||||
@@ -10,7 +10,7 @@ interface WithSuspenseProps {
|
||||
const WithSuspense: FunctionComponent<WithSuspenseProps> = (props) => {
|
||||
const propsCopy = { ...props };
|
||||
delete propsCopy.children;
|
||||
(props.children as ReactElement<any>)?.props && Object.assign((props.children as ReactElement<any>).props, propsCopy); // There is probably a better way to do this but valve does it this way so ¯\_(ツ)_/¯
|
||||
(props.children as ReactElement)?.props && Object.assign((props.children as ReactElement).props, propsCopy); // There is probably a better way to do this but valve does it this way so ¯\_(ツ)_/¯
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
|
||||
@@ -3,11 +3,10 @@ import { FC, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaCheck, FaDownload } from 'react-icons/fa';
|
||||
|
||||
import { DisabledPlugin, InstallType, InstallTypeTranslationMapping } from '../../plugin';
|
||||
import { InstallType, InstallTypeTranslationMapping } from '../../plugin';
|
||||
|
||||
interface MultiplePluginsInstallModalProps {
|
||||
requests: { name: string; version: string; hash: string; install_type: InstallType }[];
|
||||
disabledPlugins: DisabledPlugin[];
|
||||
onOK(): void | Promise<void>;
|
||||
onCancel(): void | Promise<void>;
|
||||
closeModal?(): void;
|
||||
@@ -18,7 +17,6 @@ type TitleTranslationMapping = 'mixed' | (typeof InstallTypeTranslationMapping)[
|
||||
|
||||
const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({
|
||||
requests,
|
||||
disabledPlugins,
|
||||
onOK,
|
||||
onCancel,
|
||||
closeModal,
|
||||
@@ -118,11 +116,10 @@ const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({
|
||||
version,
|
||||
});
|
||||
|
||||
const disabled = disabledPlugins.some((p) => p.name === name);
|
||||
return (
|
||||
<li key={i} style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<span>
|
||||
{disabled ? `${description} - ${t('PluginInstallModal.disabled')}` : description}{' '}
|
||||
{description}{' '}
|
||||
{(pluginsCompleted.includes(name) && <FaCheck />) || (name === pluginInProgress && <FaDownload />)}
|
||||
</span>
|
||||
{hash === 'False' && (
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
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,7 +9,6 @@ interface PluginInstallModalProps {
|
||||
version: string;
|
||||
hash: string;
|
||||
installType: InstallType;
|
||||
disabled?: boolean;
|
||||
onOK(): void;
|
||||
onCancel(): void;
|
||||
closeModal?(): void;
|
||||
@@ -20,7 +19,6 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
|
||||
version,
|
||||
hash,
|
||||
installType,
|
||||
disabled,
|
||||
onOK,
|
||||
onCancel,
|
||||
closeModal,
|
||||
@@ -47,10 +45,6 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
|
||||
}, []);
|
||||
|
||||
const installTypeTranslationKey = InstallTypeTranslationMapping[installType];
|
||||
const description = t(`PluginInstallModal.${installTypeTranslationKey}.desc`, {
|
||||
artifact: artifact,
|
||||
version: version,
|
||||
});
|
||||
|
||||
return (
|
||||
<ConfirmModal
|
||||
@@ -124,7 +118,10 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
|
||||
// t('PluginInstallModal.update.desc')
|
||||
// t('PluginInstallModal.downgrade.desc')
|
||||
// t('PluginInstallModal.overwrite.desc')
|
||||
disabled ? `${description} ${t('PluginInstallModal.disabled')}` : description
|
||||
t(`PluginInstallModal.${installTypeTranslationKey}.desc`, {
|
||||
artifact: artifact,
|
||||
version: version,
|
||||
})
|
||||
}
|
||||
</div>
|
||||
{hash == 'False' && <span style={{ color: 'red' }}>{t('PluginInstallModal.no_hash')}</span>}
|
||||
|
||||
@@ -2,10 +2,8 @@ import { ConfirmModal, Spinner } from '@decky/ui';
|
||||
import { FC, useState } from 'react';
|
||||
|
||||
import { uninstallPlugin } from '../../plugin';
|
||||
import { DeckyState } from '../DeckyState';
|
||||
|
||||
interface PluginUninstallModalProps {
|
||||
deckyState: DeckyState;
|
||||
name: string;
|
||||
title: string;
|
||||
buttonText: string;
|
||||
@@ -13,14 +11,7 @@ interface PluginUninstallModalProps {
|
||||
closeModal?(): void;
|
||||
}
|
||||
|
||||
const PluginUninstallModal: FC<PluginUninstallModalProps> = ({
|
||||
name,
|
||||
title,
|
||||
buttonText,
|
||||
description,
|
||||
deckyState,
|
||||
closeModal,
|
||||
}) => {
|
||||
const PluginUninstallModal: FC<PluginUninstallModalProps> = ({ name, title, buttonText, description, closeModal }) => {
|
||||
const [uninstalling, setUninstalling] = useState<boolean>(false);
|
||||
return (
|
||||
<ConfirmModal
|
||||
@@ -28,7 +19,6 @@ const PluginUninstallModal: FC<PluginUninstallModalProps> = ({
|
||||
onOK={async () => {
|
||||
setUninstalling(true);
|
||||
await uninstallPlugin(name);
|
||||
deckyState.setDisabledPlugins(deckyState.publicState().disabledPlugins.filter((d) => d.name !== name));
|
||||
// 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
|
||||
await DeckyPluginLoader.frozenPluginsService.invalidate();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FC, JSX, useEffect, useState } from 'react';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IconContext } from 'react-icons';
|
||||
import { FaExclamationTriangle, FaQuestionCircle, FaUserSlash } from 'react-icons/fa';
|
||||
|
||||
@@ -1,14 +1,4 @@
|
||||
import {
|
||||
Carousel,
|
||||
DialogButton,
|
||||
Field,
|
||||
Focusable,
|
||||
ProgressBarWithInfo,
|
||||
Spinner,
|
||||
findSP,
|
||||
gamepadDialogClasses,
|
||||
showModal,
|
||||
} from '@decky/ui';
|
||||
import { Carousel, DialogButton, Field, Focusable, ProgressBarWithInfo, Spinner, findSP, showModal } from '@decky/ui';
|
||||
import { Suspense, lazy, useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaExclamation } from 'react-icons/fa';
|
||||
@@ -163,22 +153,13 @@ export default function UpdaterSettings() {
|
||||
: t('Updater.updates.install_button')}
|
||||
</DialogButton>
|
||||
) : (
|
||||
<div id="decky-hide-left">
|
||||
<style>
|
||||
{`
|
||||
#decky-hide-left .${gamepadDialogClasses.FieldLeftColumn} {
|
||||
display: none;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<ProgressBarWithInfo
|
||||
layout="inline"
|
||||
bottomSeparator="none"
|
||||
nProgress={updateProgress}
|
||||
indeterminate={reloading}
|
||||
sOperationText={reloading ? t('Updater.updates.reloading') : t('Updater.updates.updating')}
|
||||
/>
|
||||
</div>
|
||||
<ProgressBarWithInfo
|
||||
layout="inline"
|
||||
bottomSeparator="none"
|
||||
nProgress={updateProgress}
|
||||
indeterminate={reloading}
|
||||
sOperationText={reloading ? t('Updater.updates.reloading') : t('Updater.updates.updating')}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
{versionInfo?.remote && versionInfo?.remote?.tag_name != versionInfo?.current && (
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { FC } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaBan, FaEyeSlash, FaLock } from 'react-icons/fa';
|
||||
import { FaEyeSlash, FaLock } from 'react-icons/fa';
|
||||
|
||||
interface PluginListLabelProps {
|
||||
frozen: boolean;
|
||||
hidden: boolean;
|
||||
disabled: boolean;
|
||||
name: string;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
const PluginListLabel: FC<PluginListLabelProps> = ({ name, frozen, hidden, version, disabled }) => {
|
||||
const PluginListLabel: FC<PluginListLabelProps> = ({ name, frozen, hidden, version }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||||
@@ -44,20 +43,6 @@ const PluginListLabel: FC<PluginListLabelProps> = ({ name, frozen, hidden, versi
|
||||
{t('PluginListLabel.hidden')}
|
||||
</div>
|
||||
)}
|
||||
{disabled && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: '0.8rem',
|
||||
color: '#dcdedf',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
}}
|
||||
>
|
||||
<FaBan />
|
||||
{t('PluginListLabel.disabled')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,11 +2,9 @@ import {
|
||||
DialogBody,
|
||||
DialogButton,
|
||||
DialogControlsSection,
|
||||
Focusable,
|
||||
GamepadEvent,
|
||||
Menu,
|
||||
MenuItem,
|
||||
NavEntryPositionPreferences,
|
||||
ReorderableEntry,
|
||||
ReorderableList,
|
||||
showContextMenu,
|
||||
@@ -15,7 +13,7 @@ import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaDownload, FaEllipsisH, FaRecycle } from 'react-icons/fa';
|
||||
|
||||
import { InstallType, enablePlugin } from '../../../../plugin';
|
||||
import { InstallType } from '../../../../plugin';
|
||||
import {
|
||||
StorePluginVersion,
|
||||
getPluginList,
|
||||
@@ -37,7 +35,6 @@ async function reinstallPlugin(pluginName: string, currentVersion?: string) {
|
||||
|
||||
type PluginTableData = PluginData & {
|
||||
name: string;
|
||||
disabled: boolean;
|
||||
frozen: boolean;
|
||||
onFreeze(): void;
|
||||
onUnfreeze(): void;
|
||||
@@ -57,25 +54,22 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> }
|
||||
return null;
|
||||
}
|
||||
|
||||
const { name, update, version, onHide, onShow, hidden, onFreeze, onUnfreeze, frozen, isDeveloper, disabled } =
|
||||
props.entry.data;
|
||||
const { name, update, version, onHide, onShow, hidden, onFreeze, onUnfreeze, frozen, isDeveloper } = props.entry.data;
|
||||
|
||||
const showCtxMenu = (e: MouseEvent | GamepadEvent) => {
|
||||
showContextMenu(
|
||||
<Menu label={t('PluginListIndex.plugin_actions')}>
|
||||
{!disabled && (
|
||||
<MenuItem
|
||||
onSelected={async () => {
|
||||
try {
|
||||
await reloadPluginBackend(name);
|
||||
} catch (err) {
|
||||
console.error('Error Reloading Plugin Backend', err);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('PluginListIndex.reload')}
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem
|
||||
onSelected={async () => {
|
||||
try {
|
||||
await reloadPluginBackend(name);
|
||||
} catch (err) {
|
||||
console.error('Error Reloading Plugin Backend', err);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('PluginListIndex.reload')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onSelected={() =>
|
||||
DeckyPluginLoader.uninstallPlugin(
|
||||
@@ -88,28 +82,11 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> }
|
||||
>
|
||||
{t('PluginListIndex.uninstall')}
|
||||
</MenuItem>
|
||||
{disabled ? (
|
||||
<MenuItem onSelected={() => enablePlugin(name)}>{t('PluginListIndex.enable')}</MenuItem>
|
||||
{hidden ? (
|
||||
<MenuItem onSelected={onShow}>{t('PluginListIndex.show')}</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>
|
||||
<MenuItem onSelected={onHide}>{t('PluginListIndex.hide')}</MenuItem>
|
||||
)}
|
||||
{!disabled &&
|
||||
(hidden ? (
|
||||
<MenuItem onSelected={onShow}>{t('PluginListIndex.show')}</MenuItem>
|
||||
) : (
|
||||
<MenuItem onSelected={onHide}>{t('PluginListIndex.hide')}</MenuItem>
|
||||
))}
|
||||
{frozen ? (
|
||||
<MenuItem onSelected={onUnfreeze}>{t('PluginListIndex.unfreeze')}</MenuItem>
|
||||
) : (
|
||||
@@ -121,7 +98,7 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> }
|
||||
};
|
||||
|
||||
return (
|
||||
<Focusable navEntryPreferPosition={NavEntryPositionPreferences.MAINTAIN_X} style={{ display: 'flex' }}>
|
||||
<>
|
||||
{update ? (
|
||||
<DialogButton
|
||||
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
|
||||
@@ -160,7 +137,7 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> }
|
||||
>
|
||||
<FaEllipsisH />
|
||||
</DialogButton>
|
||||
</Focusable>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -170,18 +147,16 @@ type PluginData = {
|
||||
};
|
||||
|
||||
export default function PluginList({ isDeveloper }: { isDeveloper: boolean }) {
|
||||
const { installedPlugins, disabledPlugins, updates, pluginOrder, setPluginOrder, frozenPlugins, hiddenPlugins } =
|
||||
useDeckyState();
|
||||
|
||||
const { plugins, updates, pluginOrder, setPluginOrder, frozenPlugins, hiddenPlugins } = useDeckyState();
|
||||
const [_, setPluginOrderSetting] = useSetting<string[]>(
|
||||
'pluginOrder',
|
||||
installedPlugins.map((plugin) => plugin.name),
|
||||
plugins.map((plugin) => plugin.name),
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
DeckyPluginLoader.checkPluginUpdates();
|
||||
}, [installedPlugins, frozenPlugins]);
|
||||
}, []);
|
||||
|
||||
const [pluginEntries, setPluginEntries] = useState<ReorderableEntry<PluginTableData>[]>([]);
|
||||
const hiddenPluginsService = DeckyPluginLoader.hiddenPluginsService;
|
||||
@@ -189,24 +164,15 @@ export default function PluginList({ isDeveloper }: { isDeveloper: boolean }) {
|
||||
|
||||
useEffect(() => {
|
||||
setPluginEntries(
|
||||
installedPlugins.map(({ name, version }) => {
|
||||
plugins.map(({ name, version }) => {
|
||||
const frozen = frozenPlugins.includes(name);
|
||||
const hidden = hiddenPlugins.includes(name);
|
||||
|
||||
return {
|
||||
label: (
|
||||
<PluginListLabel
|
||||
name={name}
|
||||
frozen={frozen}
|
||||
hidden={hidden}
|
||||
version={version}
|
||||
disabled={disabledPlugins.find((p) => p.name == name) !== undefined}
|
||||
/>
|
||||
),
|
||||
label: <PluginListLabel name={name} frozen={frozen} hidden={hidden} version={version} />,
|
||||
position: pluginOrder.indexOf(name),
|
||||
data: {
|
||||
name,
|
||||
disabled: disabledPlugins.some((disabledPlugin) => disabledPlugin.name === name),
|
||||
frozen,
|
||||
hidden,
|
||||
isDeveloper,
|
||||
@@ -220,9 +186,9 @@ export default function PluginList({ isDeveloper }: { isDeveloper: boolean }) {
|
||||
};
|
||||
}),
|
||||
);
|
||||
}, [installedPlugins, updates, hiddenPlugins, disabledPlugins]);
|
||||
}, [plugins, updates, hiddenPlugins]);
|
||||
|
||||
if (installedPlugins.length === 0) {
|
||||
if (plugins.length === 0) {
|
||||
return (
|
||||
<div>
|
||||
<p>{t('PluginListIndex.no_plugin')}</p>
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
DialogControlsSection,
|
||||
Field,
|
||||
Focusable,
|
||||
NavEntryPositionPreferences,
|
||||
Navigation,
|
||||
ProgressBar,
|
||||
SteamSpinner,
|
||||
@@ -66,9 +65,9 @@ export default function TestingVersionList() {
|
||||
|
||||
if (testingVersions.length === 0) {
|
||||
return (
|
||||
<DialogBody>
|
||||
<div>
|
||||
<p>No open PRs found</p>
|
||||
</DialogBody>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -80,21 +79,15 @@ export default function TestingVersionList() {
|
||||
<ul style={{ listStyleType: 'none', padding: '0' }}>
|
||||
{testingVersions.map((version) => {
|
||||
return (
|
||||
<li key={`${version.id}_${version.name}`}>
|
||||
<li>
|
||||
<Field
|
||||
label={
|
||||
<>
|
||||
{version.name}{' '}
|
||||
<span style={{ opacity: '50%', whiteSpace: 'nowrap', marginLeft: 'auto', alignSelf: 'center' }}>
|
||||
{'#' + version.id}
|
||||
</span>
|
||||
{version.name} <span style={{ opacity: '50%' }}>{'#' + version.id}</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Focusable
|
||||
style={{ height: '40px', marginLeft: 'auto', display: 'flex' }}
|
||||
navEntryPreferPosition={NavEntryPositionPreferences.MAINTAIN_X}
|
||||
>
|
||||
<Focusable style={{ height: '40px', marginLeft: 'auto', display: 'flex' }}>
|
||||
<DialogButton
|
||||
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
|
||||
onClick={async () => {
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
import {
|
||||
ButtonItem,
|
||||
Dropdown,
|
||||
Focusable,
|
||||
NavEntryPositionPreferences,
|
||||
PanelSectionRow,
|
||||
SingleDropdownOption,
|
||||
SuspensefulImage,
|
||||
} from '@decky/ui';
|
||||
import { ButtonItem, Dropdown, Focusable, PanelSectionRow, SingleDropdownOption, SuspensefulImage } from '@decky/ui';
|
||||
import { CSSProperties, FC, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaArrowDown, FaArrowUp, FaCheck, FaDownload, FaRecycle } from 'react-icons/fa';
|
||||
|
||||
import { DisabledPlugin, InstallType, Plugin } from '../../plugin';
|
||||
import { InstallType, Plugin } from '../../plugin';
|
||||
import { StorePlugin, requestPluginInstall } from '../../store';
|
||||
import ExternalLink from '../ExternalLink';
|
||||
|
||||
interface PluginCardProps {
|
||||
storePlugin: StorePlugin;
|
||||
installedPlugin: Plugin | DisabledPlugin | undefined;
|
||||
installedPlugin: Plugin | undefined;
|
||||
}
|
||||
|
||||
const PluginCard: FC<PluginCardProps> = ({ storePlugin, installedPlugin }) => {
|
||||
@@ -147,10 +139,7 @@ const PluginCard: FC<PluginCardProps> = ({ storePlugin, installedPlugin }) => {
|
||||
</div>
|
||||
<div className="deckyStoreCardButtonRow">
|
||||
<PanelSectionRow>
|
||||
<Focusable
|
||||
style={{ display: 'flex', gap: '5px', padding: 0 }}
|
||||
navEntryPreferPosition={NavEntryPositionPreferences.MAINTAIN_X}
|
||||
>
|
||||
<Focusable style={{ display: 'flex', gap: '5px', padding: 0 }}>
|
||||
<div
|
||||
className="deckyStoreCardInstallContainer"
|
||||
style={
|
||||
|
||||
@@ -105,7 +105,7 @@ const BrowseTab: FC<{ setPluginCount: Dispatch<SetStateAction<number | null>> }>
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const { installedPlugins } = useDeckyState();
|
||||
const { plugins: installedPlugins } = useDeckyState();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -240,7 +240,6 @@ const BrowseTab: FC<{ setPluginCount: Dispatch<SetStateAction<number | null>> }>
|
||||
})
|
||||
.map((plugin: StorePlugin) => (
|
||||
<PluginCard
|
||||
key={`${plugin.id}_${plugin.name}`}
|
||||
storePlugin={plugin}
|
||||
installedPlugin={installedPlugins.find((installedPlugin) => installedPlugin.name === plugin.name)}
|
||||
/>
|
||||
|
||||
@@ -24,7 +24,7 @@ class ErrorBoundaryHook extends Logger {
|
||||
window.__ERRORBOUNDARY_HOOK_INSTANCE = this;
|
||||
|
||||
// valve writes only the sanest of code
|
||||
const exp = /^\(\)=>\(.\|\|(?:.\(|\(.=)new .\),.\)$/;
|
||||
const exp = /^\(\)=>\(.\|\|.\(new .\),.\)$/;
|
||||
const initErrorReportingStore = findModuleExport(
|
||||
(e) => typeof e == 'function' && e?.toString && exp.test(e.toString()),
|
||||
);
|
||||
|
||||
+5
-15
@@ -1,4 +1,8 @@
|
||||
// Sets up DFL, then loads start.ts which starts up the loader
|
||||
interface Window {
|
||||
// Shut up TS
|
||||
SP_REACTDOM: any;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
console.debug('[Decky:Boot] Frontend init');
|
||||
@@ -17,21 +21,7 @@
|
||||
// deliberate partial import
|
||||
const DFLWebpack = await import('@decky/ui/dist/webpack');
|
||||
window.SP_REACT = DFLWebpack.findModule((m) => m.Component && m.PureComponent && m.useLayoutEffect);
|
||||
window.SP_REACTDOM =
|
||||
DFLWebpack.findModule((m) => m.createPortal && m.createRoot) ||
|
||||
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...');
|
||||
const jsxModule = DFLWebpack.findModule((m) => (m.jsx && m.jsxs) || (m.jsx && Object.keys(m).length == 1));
|
||||
if (jsxModule.jsxs) {
|
||||
window.SP_JSX = jsxModule;
|
||||
} else {
|
||||
window.SP_JSX = {
|
||||
jsx: jsxModule.jsx,
|
||||
jsxs: jsxModule.jsx,
|
||||
Fragment: window.SP_REACT.Fragment,
|
||||
};
|
||||
}
|
||||
window.SP_REACTDOM = DFLWebpack.findModule((m) => m.createPortal && m.createRoot);
|
||||
}
|
||||
console.debug('[Decky:Boot] Setting up @decky/ui...');
|
||||
window.DFL = await import('@decky/ui');
|
||||
|
||||
@@ -19,7 +19,6 @@ import { DeckyState, DeckyStateContextProvider, UserInfo, useDeckyState } from '
|
||||
import { File, FileSelectionType } from './components/modals/filepicker';
|
||||
import { deinitFilepickerPatches, initFilepickerPatches } from './components/modals/filepicker/patches';
|
||||
import MultiplePluginsInstallModal from './components/modals/MultiplePluginsInstallModal';
|
||||
import PluginDisableModal from './components/modals/PluginDisableModal';
|
||||
import PluginInstallModal from './components/modals/PluginInstallModal';
|
||||
import PluginUninstallModal from './components/modals/PluginUninstallModal';
|
||||
import NotificationBadge from './components/NotificationBadge';
|
||||
@@ -31,7 +30,7 @@ import { FrozenPluginService } from './frozen-plugins-service';
|
||||
import { HiddenPluginsService } from './hidden-plugins-service';
|
||||
import Logger from './logger';
|
||||
import { NotificationService } from './notification-service';
|
||||
import { DisabledPlugin, InstallType, Plugin, PluginLoadType } from './plugin';
|
||||
import { InstallType, Plugin, PluginLoadType } from './plugin';
|
||||
import RouterHook from './router-hook';
|
||||
import { deinitSteamFixes, initSteamFixes } from './steamfixes';
|
||||
import { checkForPluginUpdates } from './store';
|
||||
@@ -92,7 +91,6 @@ class PluginLoader extends Logger {
|
||||
DeckyBackend.addEventListener('loader/notify_updates', this.notifyUpdates.bind(this));
|
||||
DeckyBackend.addEventListener('loader/import_plugin', this.importPlugin.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_multiple_plugins_install_prompt',
|
||||
@@ -122,6 +120,28 @@ class PluginLoader extends Logger {
|
||||
<DeckyStateContextProvider deckyState={this.deckyState}>
|
||||
<FaPlug />
|
||||
<TabBadge />
|
||||
<style>
|
||||
{`
|
||||
/* fixes random overscrolling in QAM */
|
||||
.${quickAccessMenuClasses?.TabContentColumn} {
|
||||
flex-grow: 1 !important;
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
justify-content: center !important;
|
||||
}
|
||||
.${quickAccessMenuClasses?.Tab} {
|
||||
flex-grow: 1 !important;
|
||||
height: unset !important;
|
||||
--decky-qam-tab-max-height: 64px; /* make things a little easier for themers */
|
||||
max-height: var(--decky-qam-tab-max-height) !important;
|
||||
}
|
||||
/* they broke the footer a while ago and forgot to update the styles LOL */
|
||||
.${quickAccessMenuClasses?.Tabs}.${quickAccessMenuClasses.TabsWithFooter} {
|
||||
margin-bottom: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</DeckyStateContextProvider>
|
||||
),
|
||||
});
|
||||
@@ -177,7 +197,7 @@ class PluginLoader extends Logger {
|
||||
|
||||
private getPluginsFromBackend = DeckyBackend.callable<
|
||||
[],
|
||||
{ name: string; version: string; load_type: PluginLoadType; disabled: boolean }[]
|
||||
{ name: string; version: string; load_type: PluginLoadType }[]
|
||||
>('loader/get_plugins');
|
||||
|
||||
private restartWebhelper = DeckyBackend.callable<[], void>('utilities/restart_webhelper');
|
||||
@@ -200,16 +220,10 @@ class PluginLoader extends Logger {
|
||||
this.runCrashChecker();
|
||||
const plugins = await this.getPluginsFromBackend();
|
||||
const pluginLoadPromises = [];
|
||||
const disabledPlugins: DisabledPlugin[] = [];
|
||||
const loadStart = performance.now();
|
||||
for (const plugin of plugins) {
|
||||
if (plugin.disabled) {
|
||||
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));
|
||||
}
|
||||
if (!this.hasPlugin(plugin.name))
|
||||
pluginLoadPromises.push(this.importPlugin(plugin.name, plugin.version, plugin.load_type, false));
|
||||
}
|
||||
await Promise.all(pluginLoadPromises);
|
||||
const loadEnd = performance.now();
|
||||
@@ -260,9 +274,7 @@ class PluginLoader extends Logger {
|
||||
public async checkPluginUpdates() {
|
||||
const frozenPlugins = this.deckyState.publicState().frozenPlugins;
|
||||
|
||||
const updates = await checkForPluginUpdates(
|
||||
this.deckyState.publicState().installedPlugins.filter((p) => !frozenPlugins.includes(p.name)),
|
||||
);
|
||||
const updates = await checkForPluginUpdates(this.plugins.filter((p) => !frozenPlugins.includes(p.name)));
|
||||
this.deckyState.setUpdates(updates);
|
||||
return updates;
|
||||
}
|
||||
@@ -300,7 +312,6 @@ class PluginLoader extends Logger {
|
||||
version={version}
|
||||
hash={hash}
|
||||
installType={install_type}
|
||||
disabled={this.deckyState.publicState().disabledPlugins.some((p) => p.name === artifact)}
|
||||
onOK={() => DeckyBackend.call<[string]>('utilities/confirm_plugin_install', request_id)}
|
||||
onCancel={() => DeckyBackend.call<[string]>('utilities/cancel_plugin_install', request_id)}
|
||||
/>,
|
||||
@@ -314,7 +325,6 @@ class PluginLoader extends Logger {
|
||||
showModal(
|
||||
<MultiplePluginsInstallModal
|
||||
requests={requests}
|
||||
disabledPlugins={this.deckyState.publicState().disabledPlugins}
|
||||
onOK={() => DeckyBackend.call<[string]>('utilities/confirm_plugin_install', request_id)}
|
||||
onCancel={() => DeckyBackend.call<[string]>('utilities/cancel_plugin_install', request_id)}
|
||||
/>,
|
||||
@@ -322,19 +332,7 @@ class PluginLoader extends Logger {
|
||||
}
|
||||
|
||||
public uninstallPlugin(name: string, title: string, buttonText: string, description: string) {
|
||||
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} />);
|
||||
showModal(<PluginUninstallModal name={name} title={title} buttonText={buttonText} description={description} />);
|
||||
}
|
||||
|
||||
public hasPlugin(name: string) {
|
||||
@@ -375,19 +373,6 @@ class PluginLoader extends Logger {
|
||||
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) {
|
||||
const plugin = this.plugins.find((plugin) => plugin.name === name);
|
||||
plugin?.onDismount?.();
|
||||
@@ -400,7 +385,6 @@ class PluginLoader extends Logger {
|
||||
version?: string | undefined,
|
||||
loadType: PluginLoadType = PluginLoadType.ESMODULE_V1,
|
||||
useQueue: boolean = true,
|
||||
timeoutMS?: number,
|
||||
) {
|
||||
if (useQueue && this.reloadLock) {
|
||||
this.log('Reload currently in progress, adding to queue', name);
|
||||
@@ -414,11 +398,9 @@ class PluginLoader extends Logger {
|
||||
|
||||
this.unloadPlugin(name, true);
|
||||
const startTime = performance.now();
|
||||
|
||||
await this.importReactPlugin(name, version, loadType, timeoutMS);
|
||||
await this.importReactPlugin(name, version, loadType);
|
||||
const endTime = performance.now();
|
||||
|
||||
this.deckyState.setDisabledPlugins(this.deckyState.publicState().disabledPlugins.filter((d) => d.name !== name));
|
||||
this.deckyState.setPlugins(this.plugins);
|
||||
this.log(`Loaded ${name} in ${endTime - startTime}ms`);
|
||||
} catch (e) {
|
||||
@@ -428,7 +410,7 @@ class PluginLoader extends Logger {
|
||||
this.reloadLock = false;
|
||||
const nextPlugin = this.pluginReloadQueue.shift();
|
||||
if (nextPlugin) {
|
||||
this.importPlugin(nextPlugin.name, nextPlugin.version, nextPlugin.loadType, true, timeoutMS);
|
||||
this.importPlugin(nextPlugin.name, nextPlugin.version, loadType);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -438,28 +420,12 @@ class PluginLoader extends Logger {
|
||||
name: string,
|
||||
version?: string,
|
||||
loadType: PluginLoadType = PluginLoadType.ESMODULE_V1,
|
||||
timeoutMS?: number,
|
||||
) {
|
||||
let spExists = this.checkForSP();
|
||||
const timeoutException = new Error(
|
||||
`${name} failed to load within ${timeoutMS ? `${timeoutMS / 1000} second` : ''} time limit`,
|
||||
);
|
||||
let timeout: number | undefined;
|
||||
|
||||
try {
|
||||
switch (loadType) {
|
||||
case PluginLoadType.ESMODULE_V1:
|
||||
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;
|
||||
const plugin_exports = await import(`http://127.0.0.1:1337/plugins/${name}/dist/index.js?t=${Date.now()}`);
|
||||
let plugin = plugin_exports.default();
|
||||
|
||||
this.plugins.push({
|
||||
@@ -471,26 +437,12 @@ class PluginLoader extends Logger {
|
||||
break;
|
||||
|
||||
case PluginLoadType.LEGACY_EVAL_IIFE:
|
||||
const fetchJS = async () => {
|
||||
const controller = new AbortController();
|
||||
const { signal } = controller;
|
||||
|
||||
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();
|
||||
let res = await fetch(`http://127.0.0.1:1337/plugins/${name}/frontend_bundle`, {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'X-Decky-Auth': deckyAuthToken,
|
||||
},
|
||||
});
|
||||
if (res.ok) {
|
||||
let plugin_export: (serverAPI: any) => Plugin = await eval(
|
||||
(await res.text()) + `\n//# sourceURL=decky://decky/legacy_plugin/${encodeURIComponent(name)}/index.js`,
|
||||
@@ -509,8 +461,6 @@ class PluginLoader extends Logger {
|
||||
throw new Error(`${name} has no defined loadType.`);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e === timeoutException) throw timeoutException;
|
||||
|
||||
this.error('Error loading plugin ' + name, e);
|
||||
const TheError: FC<{}> = () => (
|
||||
<PanelSection>
|
||||
@@ -553,8 +503,6 @@ class PluginLoader extends Logger {
|
||||
body: '' + e,
|
||||
icon: <FaExclamationCircle />,
|
||||
});
|
||||
} finally {
|
||||
if (timeout !== undefined) clearTimeout(timeout);
|
||||
}
|
||||
|
||||
if (spExists && !this.checkForSP()) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { JSX } from 'react';
|
||||
export enum PluginLoadType {
|
||||
LEGACY_EVAL_IIFE = 0, // legacy, uses legacy serverAPI
|
||||
ESMODULE_V1 = 1, // esmodule loading with modern @decky/backend apis
|
||||
@@ -15,8 +14,6 @@ export interface Plugin {
|
||||
titleView?: JSX.Element;
|
||||
}
|
||||
|
||||
export type DisabledPlugin = Pick<Plugin, 'name' | 'version'>;
|
||||
|
||||
export enum InstallType {
|
||||
INSTALL,
|
||||
REINSTALL,
|
||||
@@ -58,5 +55,3 @@ type installPluginsArgs = [
|
||||
export let installPlugins = DeckyBackend.callable<installPluginsArgs>('utilities/install_plugins');
|
||||
|
||||
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');
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
getReactRoot,
|
||||
sleep,
|
||||
} from '@decky/ui';
|
||||
import { FC, JSX, ReactElement, ReactNode, cloneElement, createElement } from 'react';
|
||||
import { FC, ReactElement, ReactNode, cloneElement, createElement } from 'react';
|
||||
import type { Route } from 'react-router';
|
||||
|
||||
import {
|
||||
@@ -37,7 +37,7 @@ const isPatched = Symbol('is patched');
|
||||
class RouterHook extends Logger {
|
||||
private routerState: DeckyRouterState = new DeckyRouterState();
|
||||
private globalComponentsState: DeckyGlobalComponentsState = new DeckyGlobalComponentsState();
|
||||
private renderedComponents: ReactElement<any>[] = [];
|
||||
private renderedComponents: ReactElement[] = [];
|
||||
private Route: any;
|
||||
private DeckyGamepadRouterWrapper = this.gamepadRouterWrapper.bind(this);
|
||||
private DeckyDesktopRouterWrapper = this.desktopRouterWrapper.bind(this);
|
||||
@@ -233,7 +233,7 @@ class RouterHook extends Logger {
|
||||
return <>{this.renderedComponents}</>;
|
||||
}
|
||||
|
||||
private gamepadRouterWrapper({ children }: { children: ReactElement<any> }) {
|
||||
private gamepadRouterWrapper({ children }: { children: ReactElement }) {
|
||||
// Used to store the new replicated routes we create to allow routes to be unpatched.
|
||||
|
||||
const { routes, routePatches } = useDeckyRouterState();
|
||||
@@ -251,7 +251,7 @@ class RouterHook extends Logger {
|
||||
return children;
|
||||
}
|
||||
|
||||
private desktopRouterWrapper({ children }: { children: ReactElement<any> }) {
|
||||
private desktopRouterWrapper({ children }: { children: ReactElement }) {
|
||||
// Used to store the new replicated routes we create to allow routes to be unpatched.
|
||||
this.debug('desktop router wrapper render', children);
|
||||
const { routes, routePatches } = useDeckyRouterState();
|
||||
@@ -287,7 +287,7 @@ class RouterHook extends Logger {
|
||||
if (routes) {
|
||||
if (!routeList[routerIndex - 1]?.length || routeList[routerIndex - 1]?.length !== routes.size) {
|
||||
if (routeList[routerIndex - 1]?.length && routeList[routerIndex - 1].length !== routes.size) routerIndex--;
|
||||
const newRouterArray: (ReactElement<any> | JSX.Element)[] = [];
|
||||
const newRouterArray: (ReactElement | JSX.Element)[] = [];
|
||||
routes.forEach(({ component, props }, path) => {
|
||||
newRouterArray.push(
|
||||
<Route path={path} {...props}>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { compare, validate } from 'compare-versions';
|
||||
import { compare } from 'compare-versions';
|
||||
|
||||
import { DisabledPlugin, InstallType, Plugin, installPlugin, installPlugins } from './plugin';
|
||||
import { InstallType, Plugin, installPlugin, installPlugins } from './plugin';
|
||||
import { getSetting, setSetting } from './utils/settings';
|
||||
|
||||
export enum Store {
|
||||
@@ -42,6 +42,14 @@ export interface PluginInstallRequest {
|
||||
installType: InstallType;
|
||||
}
|
||||
|
||||
export interface Announcement {
|
||||
id: string;
|
||||
title: string;
|
||||
text: string;
|
||||
created: string;
|
||||
updated: string;
|
||||
}
|
||||
|
||||
// name: version
|
||||
export type PluginUpdateMapping = Map<string, StorePluginVersion>;
|
||||
|
||||
@@ -49,6 +57,47 @@ export async function getStore(): Promise<Store> {
|
||||
return await getSetting<Store>('store', Store.Default);
|
||||
}
|
||||
|
||||
export async function getAnnouncements(): Promise<Announcement[]> {
|
||||
let version = await window.DeckyPluginLoader.updateVersion();
|
||||
let store = await getSetting<Store | null>('store', null);
|
||||
let customURL = await getSetting<string>(
|
||||
'announcements-url',
|
||||
'https://plugins.deckbrew.xyz/v1/announcements/-/current',
|
||||
);
|
||||
|
||||
if (store === null) {
|
||||
console.log('Could not get store, using Default.');
|
||||
await setSetting('store', Store.Default);
|
||||
store = Store.Default;
|
||||
}
|
||||
|
||||
let resolvedURL;
|
||||
switch (store) {
|
||||
case Store.Default:
|
||||
resolvedURL = 'https://plugins.deckbrew.xyz/v1/announcements/-/current';
|
||||
break;
|
||||
case Store.Testing:
|
||||
resolvedURL = 'https://testing.deckbrew.xyz/v1/announcements/-/current';
|
||||
break;
|
||||
case Store.Custom:
|
||||
resolvedURL = customURL;
|
||||
break;
|
||||
default:
|
||||
console.error('Somehow you ended up without a standard URL, using the default URL.');
|
||||
resolvedURL = 'https://plugins.deckbrew.xyz/v1/announcements/-/current';
|
||||
break;
|
||||
}
|
||||
const res = await fetch(resolvedURL, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Decky-Version': version.current,
|
||||
},
|
||||
});
|
||||
if (res.status !== 200) return [];
|
||||
const json = await res.json();
|
||||
return json ?? [];
|
||||
}
|
||||
|
||||
export async function getPluginList(
|
||||
sort_by: SortOptions | null = null,
|
||||
sort_direction: SortDirections | null = null,
|
||||
@@ -113,21 +162,18 @@ export async function requestMultiplePluginInstalls(requests: PluginInstallReque
|
||||
);
|
||||
}
|
||||
|
||||
export async function checkForPluginUpdates(plugins: (Plugin | DisabledPlugin)[]): Promise<PluginUpdateMapping> {
|
||||
export async function checkForPluginUpdates(plugins: Plugin[]): Promise<PluginUpdateMapping> {
|
||||
const serverData = await getPluginList();
|
||||
const updateMap = new Map<string, StorePluginVersion>();
|
||||
for (let plugin of plugins) {
|
||||
const remotePlugin = serverData?.find((x) => x.name == plugin.name);
|
||||
//FIXME: Ugly hack since plugin.version might be null during evaluation,
|
||||
//so this will set the older version possible
|
||||
const curVer = plugin.version ? plugin.version : '0.0.0';
|
||||
|
||||
const curVer = plugin.version ? plugin.version : '0.0';
|
||||
if (
|
||||
remotePlugin &&
|
||||
remotePlugin.versions?.length > 0 &&
|
||||
plugin.version != remotePlugin?.versions?.[0]?.name &&
|
||||
validate(remotePlugin.versions?.[0]?.name) &&
|
||||
validate(curVer) &&
|
||||
compare(remotePlugin?.versions?.[0]?.name, curVer, '>')
|
||||
) {
|
||||
updateMap.set(plugin.name, remotePlugin.versions[0]);
|
||||
@@ -29,8 +29,7 @@ interface Tab {
|
||||
class TabsHook extends Logger {
|
||||
// private keys = 7;
|
||||
tabs: Tab[] = [];
|
||||
private qamBrowserViewPatch?: Patch;
|
||||
private qamEmbeddedPatch?: Patch;
|
||||
private qamPatch?: Patch;
|
||||
|
||||
constructor() {
|
||||
super('TabsHook');
|
||||
@@ -41,13 +40,11 @@ class TabsHook extends Logger {
|
||||
}
|
||||
|
||||
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 qamBrowserViewRenderer = Object.values(qamModule).find((e: any) =>
|
||||
const qamRenderer = Object.values(qamModule).find((e: any) =>
|
||||
e?.type?.toString?.()?.includes('QuickAccessMenuBrowserView'),
|
||||
);
|
||||
const qamEmbeddedRenderer = Object.values(qamModule).find((e: any) =>
|
||||
e?.type?.toString?.()?.includes('QuickAccessMenuEmbedded'),
|
||||
);
|
||||
|
||||
const patchHandler = createReactTreePatcher(
|
||||
[(tree) => findInReactTree(tree, (node) => node?.props?.onFocusNavDeactivated)],
|
||||
@@ -59,21 +56,12 @@ class TabsHook extends Logger {
|
||||
'TabsHook',
|
||||
);
|
||||
|
||||
this.qamBrowserViewPatch = afterPatch(qamBrowserViewRenderer, 'type', patchHandler);
|
||||
if (qamEmbeddedRenderer) this.qamEmbeddedPatch = afterPatch(qamEmbeddedRenderer, 'type', patchHandler);
|
||||
this.qamPatch = afterPatch(qamRenderer, 'type', patchHandler);
|
||||
|
||||
// Patch already rendered qam
|
||||
const root = getReactRoot(document.getElementById('root') as any);
|
||||
const qamNode =
|
||||
root &&
|
||||
findInReactTree(
|
||||
root,
|
||||
(n: any) =>
|
||||
n.elementType == qamBrowserViewRenderer ||
|
||||
(qamEmbeddedRenderer != null && n.elementType == qamEmbeddedRenderer),
|
||||
); // need elementType, because type is actually mobx wrapper
|
||||
const qamNode = root && findInReactTree(root, (n: any) => n.elementType == qamRenderer); // need elementType, because type is actually mobx wrapper
|
||||
if (qamNode) {
|
||||
console.log('patching existing qam');
|
||||
// Only affects this fiber node so we don't need to unpatch here
|
||||
qamNode.type = qamNode.elementType.type;
|
||||
if (qamNode?.alternate) {
|
||||
@@ -83,8 +71,7 @@ class TabsHook extends Logger {
|
||||
}
|
||||
|
||||
deinit() {
|
||||
this.qamBrowserViewPatch?.unpatch();
|
||||
this.qamEmbeddedPatch?.unpatch();
|
||||
this.qamPatch?.unpatch();
|
||||
}
|
||||
|
||||
add(tab: Tab) {
|
||||
|
||||
@@ -81,7 +81,6 @@ class Toaster extends Logger {
|
||||
const info = {
|
||||
showToast: toast.showToast,
|
||||
sound: toast.sound,
|
||||
playSound: toast.playSound,
|
||||
eFeature: 0,
|
||||
toastDurationMS: toastData.nToastDurationMS,
|
||||
bCritical: toast.critical,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ErrorInfo } from 'react';
|
||||
|
||||
const pluginErrorRegex = /http:\/\/(?:localhost|127\.0\.0\.1):1337\/plugins\/([^\/]*)\//;
|
||||
const pluginErrorRegex = /http:\/\/localhost:1337\/plugins\/([^\/]*)\//;
|
||||
const pluginSourceMapErrorRegex = /decky:\/\/decky\/plugin\/([^\/]*)\//;
|
||||
const legacyPluginErrorRegex = /decky:\/\/decky\/legacy_plugin\/([^\/]*)\/index.js/;
|
||||
|
||||
@@ -44,7 +44,7 @@ export function getLikelyErrorSource(error?: string): ErrorSource {
|
||||
return [decodeURIComponent(legacyPluginMatch[1]), true, false];
|
||||
}
|
||||
|
||||
if (error?.includes('http://localhost:1337/') || error?.includes('http://127.0.0.1:1337/')) {
|
||||
if (error?.includes('http://localhost:1337/')) {
|
||||
return ['the Decky frontend', false, false];
|
||||
}
|
||||
return ['Steam', false, true];
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"target": "ES2021",
|
||||
"jsx": "react-jsx",
|
||||
"jsx": "react",
|
||||
"jsxFactory": "window.SP_REACT.createElement",
|
||||
"jsxFragmentFactory": "window.SP_REACT.Fragment",
|
||||
"declaration": false,
|
||||
"moduleResolution": "node",
|
||||
"noUnusedLocals": true,
|
||||
@@ -13,8 +15,7 @@
|
||||
"noImplicitAny": true,
|
||||
"strict": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src", "index.d.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# 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
|
||||
# Define the directory to scan
|
||||
directory_to_scan="$HOME/homebrew/plugins"
|
||||
directory_to_scan="~/homebrew/plugins"
|
||||
|
||||
# Loop through each subdirectory (one level deep)
|
||||
for dir in "$directory_to_scan"/*/; do
|
||||
|
||||
Reference in New Issue
Block a user