mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-13 12:15:09 +03:00
Uninstall functionality (#97)
* feat: POC uninstallation feature * Fixes, placeholder * bugfix: wrong function call * add oncancel and change function called * clean up plugin uninstall code * bugfix, uninstall in store * Limit scope of feature branch * feat: PluginLoader.unloadPlugin * problematic logs
This commit is contained in:
+30
-4
@@ -1,6 +1,6 @@
|
||||
from injector import get_tab
|
||||
from logging import getLogger
|
||||
from os import path, rename
|
||||
from os import path, rename, listdir
|
||||
from shutil import rmtree
|
||||
from aiohttp import ClientSession, web
|
||||
from io import BytesIO
|
||||
@@ -11,6 +11,8 @@ from time import time
|
||||
from hashlib import sha256
|
||||
from subprocess import Popen
|
||||
|
||||
import json
|
||||
|
||||
class PluginInstallContext:
|
||||
def __init__(self, artifact, name, version, hash) -> None:
|
||||
self.artifact = artifact
|
||||
@@ -25,7 +27,8 @@ class PluginBrowser:
|
||||
self.install_requests = {}
|
||||
|
||||
server_instance.add_routes([
|
||||
web.post("/browser/install_plugin", self.install_plugin)
|
||||
web.post("/browser/install_plugin", self.install_plugin),
|
||||
web.post("/browser/uninstall_plugin", self.uninstall_plugin)
|
||||
])
|
||||
|
||||
def _unzip_to_plugin_dir(self, zip, name, hash):
|
||||
@@ -39,8 +42,31 @@ class PluginBrowser:
|
||||
Popen(["chmod", "-R", "555", self.plugin_path])
|
||||
return True
|
||||
|
||||
def find_plugin_folder(self, name):
|
||||
for folder in listdir(self.plugin_path):
|
||||
with open(path.join(self.plugin_path, folder, 'plugin.json'), 'r') as f:
|
||||
plugin = json.load(f)
|
||||
|
||||
if plugin['name'] == name:
|
||||
return path.join(self.plugin_path, folder)
|
||||
|
||||
async def uninstall_plugin(self, name):
|
||||
tab = await get_tab("SP")
|
||||
await tab.open_websocket()
|
||||
|
||||
try:
|
||||
if type(name) != str:
|
||||
data = await name.post()
|
||||
name = data.get("name")
|
||||
await tab.evaluate_js(f"DeckyPluginLoader.unloadPlugin('{name}')")
|
||||
rmtree(self.find_plugin_folder(name))
|
||||
except FileNotFoundError:
|
||||
self.log.warning(f"Plugin {name} not installed, skipping uninstallation")
|
||||
|
||||
return web.Response(text="Requested plugin uninstall")
|
||||
|
||||
async def _install(self, artifact, name, version, hash):
|
||||
rmtree(path.join(self.plugin_path, name), ignore_errors=True)
|
||||
self.uninstall_plugin(name)
|
||||
self.log.info(f"Installing {name} (Version: {version})")
|
||||
async with ClientSession() as client:
|
||||
self.log.debug(f"Fetching {artifact}")
|
||||
@@ -83,4 +109,4 @@ class PluginBrowser:
|
||||
await self._install(request.artifact, request.name, request.version, request.hash)
|
||||
|
||||
def cancel_plugin_install(self, request_id):
|
||||
self.install_requests.pop(request_id)
|
||||
self.install_requests.pop(request_id)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { SidebarNavigation } from 'decky-frontend-lib';
|
||||
|
||||
import GeneralSettings from './pages/GeneralSettings';
|
||||
import PluginList from './pages/PluginList';
|
||||
|
||||
export default function SettingsPage() {
|
||||
return (
|
||||
@@ -13,6 +14,11 @@ export default function SettingsPage() {
|
||||
content: <GeneralSettings />,
|
||||
route: '/decky/settings/general',
|
||||
},
|
||||
{
|
||||
title: 'Plugins',
|
||||
content: <PluginList />,
|
||||
route: '/decky/settings/plugins',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { DialogButton, staticClasses } from 'decky-frontend-lib';
|
||||
import { FaTrash } from 'react-icons/fa';
|
||||
|
||||
export default function PluginList() {
|
||||
const plugins = window.DeckyPluginLoader?.getPlugins();
|
||||
|
||||
if (plugins.length === 0) {
|
||||
return (
|
||||
<div>
|
||||
<p>No plugins installed</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ul style={{ listStyleType: 'none' }}>
|
||||
{window.DeckyPluginLoader?.getPlugins().map(({ name }) => (
|
||||
<li style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
<span>{name}</span>
|
||||
<div className={staticClasses.Title} style={{ marginLeft: 'auto', boxShadow: 'none' }}>
|
||||
<DialogButton
|
||||
style={{ height: '40px', width: '40px', padding: '10px 12px' }}
|
||||
onClick={() => window.DeckyPluginLoader.uninstall_plugin(name)}
|
||||
>
|
||||
<FaTrash />
|
||||
</DialogButton>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
@@ -47,6 +47,10 @@ class PluginLoader extends Logger {
|
||||
this.routerHook.addRoute('/decky/settings', () => <SettingsPage />);
|
||||
}
|
||||
|
||||
public getPlugins() {
|
||||
return this.plugins;
|
||||
}
|
||||
|
||||
public addPluginInstallPrompt(artifact: string, version: string, request_id: string, hash: string) {
|
||||
showModal(
|
||||
<ModalRoot
|
||||
@@ -66,6 +70,28 @@ class PluginLoader extends Logger {
|
||||
);
|
||||
}
|
||||
|
||||
public uninstall_plugin(name: string) {
|
||||
showModal(
|
||||
<ModalRoot
|
||||
onOK={async () => {
|
||||
const formData = new FormData();
|
||||
formData.append('name', name);
|
||||
await fetch('http://localhost:1337/browser/uninstall_plugin', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
}}
|
||||
onCancel={() => {
|
||||
// do nothing
|
||||
}}
|
||||
>
|
||||
<div className={staticClasses.Title} style={{ flexDirection: 'column' }}>
|
||||
Uninstall {name}?
|
||||
</div>
|
||||
</ModalRoot>,
|
||||
);
|
||||
}
|
||||
|
||||
public dismountAll() {
|
||||
for (const plugin of this.plugins) {
|
||||
this.log(`Dismounting ${plugin.name}`);
|
||||
@@ -78,6 +104,13 @@ class PluginLoader extends Logger {
|
||||
this.routerHook.removeRoute('/decky/settings');
|
||||
}
|
||||
|
||||
public unloadPlugin(name: string) {
|
||||
const plugin = this.plugins.find((plugin) => plugin.name === name || plugin.name === name.replace('$LEGACY_', ''));
|
||||
plugin?.onDismount?.();
|
||||
this.plugins = this.plugins.filter((p) => p !== plugin);
|
||||
this.deckyState.setPlugins(this.plugins);
|
||||
}
|
||||
|
||||
public async importPlugin(name: string) {
|
||||
if (this.reloadLock) {
|
||||
this.log('Reload currently in progress, adding to queue', name);
|
||||
@@ -89,13 +122,7 @@ class PluginLoader extends Logger {
|
||||
this.reloadLock = true;
|
||||
this.log(`Trying to load ${name}`);
|
||||
|
||||
const oldPlugin = this.plugins.find(
|
||||
(plugin) => plugin.name === name || plugin.name === name.replace('$LEGACY_', ''),
|
||||
);
|
||||
if (oldPlugin) {
|
||||
oldPlugin.onDismount?.();
|
||||
this.plugins = this.plugins.filter((plugin) => plugin !== oldPlugin);
|
||||
}
|
||||
this.unloadPlugin(name);
|
||||
|
||||
if (name.startsWith('$LEGACY_')) {
|
||||
await this.importLegacyPlugin(name.replace('$LEGACY_', ''));
|
||||
|
||||
Reference in New Issue
Block a user