mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-07-11 22:22:00 +00: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:
+29
-3
@@ -1,6 +1,6 @@
|
|||||||
from injector import get_tab
|
from injector import get_tab
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from os import path, rename
|
from os import path, rename, listdir
|
||||||
from shutil import rmtree
|
from shutil import rmtree
|
||||||
from aiohttp import ClientSession, web
|
from aiohttp import ClientSession, web
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
@@ -11,6 +11,8 @@ from time import time
|
|||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
from subprocess import Popen
|
from subprocess import Popen
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
class PluginInstallContext:
|
class PluginInstallContext:
|
||||||
def __init__(self, artifact, name, version, hash) -> None:
|
def __init__(self, artifact, name, version, hash) -> None:
|
||||||
self.artifact = artifact
|
self.artifact = artifact
|
||||||
@@ -25,7 +27,8 @@ class PluginBrowser:
|
|||||||
self.install_requests = {}
|
self.install_requests = {}
|
||||||
|
|
||||||
server_instance.add_routes([
|
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):
|
def _unzip_to_plugin_dir(self, zip, name, hash):
|
||||||
@@ -39,8 +42,31 @@ class PluginBrowser:
|
|||||||
Popen(["chmod", "-R", "555", self.plugin_path])
|
Popen(["chmod", "-R", "555", self.plugin_path])
|
||||||
return True
|
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):
|
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})")
|
self.log.info(f"Installing {name} (Version: {version})")
|
||||||
async with ClientSession() as client:
|
async with ClientSession() as client:
|
||||||
self.log.debug(f"Fetching {artifact}")
|
self.log.debug(f"Fetching {artifact}")
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { SidebarNavigation } from 'decky-frontend-lib';
|
import { SidebarNavigation } from 'decky-frontend-lib';
|
||||||
|
|
||||||
import GeneralSettings from './pages/GeneralSettings';
|
import GeneralSettings from './pages/GeneralSettings';
|
||||||
|
import PluginList from './pages/PluginList';
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
return (
|
return (
|
||||||
@@ -13,6 +14,11 @@ export default function SettingsPage() {
|
|||||||
content: <GeneralSettings />,
|
content: <GeneralSettings />,
|
||||||
route: '/decky/settings/general',
|
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 />);
|
this.routerHook.addRoute('/decky/settings', () => <SettingsPage />);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getPlugins() {
|
||||||
|
return this.plugins;
|
||||||
|
}
|
||||||
|
|
||||||
public addPluginInstallPrompt(artifact: string, version: string, request_id: string, hash: string) {
|
public addPluginInstallPrompt(artifact: string, version: string, request_id: string, hash: string) {
|
||||||
showModal(
|
showModal(
|
||||||
<ModalRoot
|
<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() {
|
public dismountAll() {
|
||||||
for (const plugin of this.plugins) {
|
for (const plugin of this.plugins) {
|
||||||
this.log(`Dismounting ${plugin.name}`);
|
this.log(`Dismounting ${plugin.name}`);
|
||||||
@@ -78,6 +104,13 @@ class PluginLoader extends Logger {
|
|||||||
this.routerHook.removeRoute('/decky/settings');
|
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) {
|
public async importPlugin(name: string) {
|
||||||
if (this.reloadLock) {
|
if (this.reloadLock) {
|
||||||
this.log('Reload currently in progress, adding to queue', name);
|
this.log('Reload currently in progress, adding to queue', name);
|
||||||
@@ -89,13 +122,7 @@ class PluginLoader extends Logger {
|
|||||||
this.reloadLock = true;
|
this.reloadLock = true;
|
||||||
this.log(`Trying to load ${name}`);
|
this.log(`Trying to load ${name}`);
|
||||||
|
|
||||||
const oldPlugin = this.plugins.find(
|
this.unloadPlugin(name);
|
||||||
(plugin) => plugin.name === name || plugin.name === name.replace('$LEGACY_', ''),
|
|
||||||
);
|
|
||||||
if (oldPlugin) {
|
|
||||||
oldPlugin.onDismount?.();
|
|
||||||
this.plugins = this.plugins.filter((plugin) => plugin !== oldPlugin);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name.startsWith('$LEGACY_')) {
|
if (name.startsWith('$LEGACY_')) {
|
||||||
await this.importLegacyPlugin(name.replace('$LEGACY_', ''));
|
await this.importLegacyPlugin(name.replace('$LEGACY_', ''));
|
||||||
|
|||||||
Reference in New Issue
Block a user