Compare commits

..

23 Commits

Author SHA1 Message Date
Beebles 86d01db2b9 fix hide behaviour for potential testers 2025-09-26 16:26:40 -06:00
Beebles 50cb08cce9 add in scroll hint 2025-09-26 16:21:08 -06:00
Beebles ef27046143 test setting scrollpanelgroup to false 2025-09-26 16:14:13 -06:00
Beebles 8bb4ff7118 fix scrollpanel group 2025-09-26 16:08:09 -06:00
Beebles e2f36091e2 change to scrollpanelgroup 2025-09-26 16:02:21 -06:00
Beebles 6eab1c1e16 add scroll overflow 2025-09-26 15:54:10 -06:00
Beebles 002f0db04a add .DS_Store to gitignore 2025-09-26 15:47:37 -06:00
Beebles b85912691f further work on modal 2025-09-26 15:46:18 -06:00
Beebles 7fff611d55 fix styling issues 2025-09-26 15:34:37 -06:00
Beebles 9b38abd13f being working on fullscreen modal 2025-09-26 15:23:46 -06:00
Beebles 120a43e55d add in 2nd debug announcement 2025-09-26 14:03:39 -06:00
Beebles a5ce24405b fix array length 0 check 2025-09-26 14:03:01 -06:00
Beebles 3b00e4a792 modify AnnouncementsDisplay to display all current announcements, not one at a time 2025-08-17 11:13:52 -06:00
Beebles 1709a957f7 ensure nulls arent passed to sort 2025-08-01 06:54:40 -06:00
Beebles b8adf165e5 move welcome announcement to default 2025-08-01 06:54:40 -06:00
Beebles 428de00b29 remove duplicates when adding announcements to array 2025-08-01 06:54:40 -06:00
Beebles 5a02f5fbe7 change announcements to be stack 2025-08-01 06:54:40 -06:00
Beebles 83ae98a709 change to use array of hidden announcements 2025-08-01 06:54:40 -06:00
Beebles 1a231bf03e rename motd to announcements and implement new API 2025-08-01 06:54:40 -06:00
Beebles edf6b54db4 move motd into div with padding 2025-08-01 06:45:40 -06:00
Beebles ccdfd53648 only set motd if value returned 2025-08-01 06:45:40 -06:00
Beebles 267b11c9bf fix(motd): run prettier 2025-08-01 06:45:40 -06:00
Beebles 5a212e95fc feat(motd): add motd component (untested) 2025-08-01 06:45:40 -06:00
18 changed files with 330 additions and 186 deletions
+3
View File
@@ -165,3 +165,6 @@ act/.directory
act/artifacts/*
bin/act
/settings/
# macOS
.DS_Store
+2 -2
View File
@@ -1,8 +1,8 @@
{
"deckip" : "192.168.1.69",
"deckip" : "0.0.0.0",
"deckport" : "22",
"deckuser" : "deck",
"deckpass" : "deck",
"deckpass" : "ssap",
"deckkey" : "-i ${env:HOME}/.ssh/id_rsa",
"deckdir" : "/home/deck"
}
+1 -6
View File
@@ -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")
-1
View File
@@ -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)
+2 -22
View File
@@ -80,8 +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.web_app.add_routes([
post("/methods/{method_name}", self._handle_legacy_server_method_call)
@@ -216,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,
@@ -392,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):
@@ -475,22 +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):
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)
plugin = self.context.plugin_loader.plugins[name]
plugin.start()
await self.context.plugin_loader.dispatch_plugin(plugin.name, plugin.version, plugin.load_type)
+1 -1
View File
@@ -47,7 +47,7 @@
}
},
"dependencies": {
"@decky/ui": "^4.10.5",
"@decky/ui": "^4.10.4",
"compare-versions": "^6.1.1",
"filesize": "^10.1.2",
"i18next": "^23.11.5",
+5 -5
View File
@@ -9,8 +9,8 @@ importers:
.:
dependencies:
'@decky/ui':
specifier: ^4.10.5
version: 4.10.5
specifier: ^4.10.4
version: 4.10.4
compare-versions:
specifier: ^6.1.1
version: 6.1.1
@@ -218,8 +218,8 @@ packages:
'@decky/api@1.1.1':
resolution: {integrity: sha512-R5fkBRHBt5QIQY7Q0AlbVIhlIZ/nTzwBOoi8Rt4Go2fjFnoMKPInCJl6cPjXzimGwl2pyqKJgY6VnH6ar0XrHQ==}
'@decky/ui@4.10.5':
resolution: {integrity: sha512-IsQiNtIbNJ5NQQJB9Dir0ir2pgE4QpcsrRB7eic2ClbPwYDuBKc6GLFs3ZQt43Dd4SwCJkzg/CeC+wgkBOMd1w==}
'@decky/ui@4.10.4':
resolution: {integrity: sha512-swgC4IVtQzZVw8dtP/iztpNYUl1eR0dxWfiMpswY8YglDsBn4ntspbL91Ic4WgxvkOEMSpsIs+zkVtjHE9zi3A==}
'@esbuild/aix-ppc64@0.20.2':
resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==}
@@ -2295,7 +2295,7 @@ snapshots:
'@decky/api@1.1.1': {}
'@decky/ui@4.10.5': {}
'@decky/ui@4.10.4': {}
'@esbuild/aix-ppc64@0.20.2':
optional: true
@@ -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 -14
View File
@@ -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[];
disabled: 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,
disabled: 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();
}
+2
View File
@@ -3,6 +3,7 @@ import { FC, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { FaEyeSlash } from 'react-icons/fa';
import { AnnouncementsDisplay } from './AnnouncementsDisplay';
import { useDeckyState } from './DeckyState';
import NotificationBadge from './NotificationBadge';
import { useQuickAccessVisible } from './QuickAccessVisibleState';
@@ -41,6 +42,7 @@ const PluginView: FC = () => {
paddingTop: '16px',
}}
>
<AnnouncementsDisplay />
<PanelSection>
{pluginList.map(({ name, icon }) => (
<PanelSectionRow key={name}>
@@ -1,46 +0,0 @@
import { ConfirmModal, Spinner } from '@decky/ui';
import { FC, useState } from 'react';
import { disablePlugin } from '../../plugin';
interface PluginUninstallModalProps {
name: string;
title: string;
buttonText: string;
description: string;
closeModal?(): void;
}
const PluginDisableModal: FC<PluginUninstallModalProps> = ({ name, title, buttonText, description, closeModal }) => {
const [disabling, setDisabling] = useState<boolean>(false);
return (
<ConfirmModal
closeModal={closeModal}
onOK={async () => {
setDisabling(true);
await disablePlugin(name);
//not sure about this yet
// 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();
await DeckyPluginLoader.hiddenPluginsService.invalidate();
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;
@@ -1,16 +1,15 @@
import { FC } from 'react';
import { useTranslation } from 'react-i18next';
import { FaEyeSlash, FaLock, FaMoon } 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',
}}
>
<FaMoon />
{t('PluginListLabel.disabled')}
</div>
)}
</div>
);
};
@@ -13,7 +13,7 @@ import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaDownload, FaEllipsisH, FaRecycle } from 'react-icons/fa';
import { enablePlugin, InstallType } from '../../../../plugin';
import { InstallType } from '../../../../plugin';
import {
StorePluginVersion,
getPluginList,
@@ -35,7 +35,6 @@ async function reinstallPlugin(pluginName: string, currentVersion?: string) {
type PluginTableData = PluginData & {
name: string;
disabled: boolean;
frozen: boolean;
onFreeze(): void;
onUnfreeze(): void;
@@ -55,7 +54,7 @@ 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(
@@ -83,25 +82,6 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> }
>
{t('PluginListIndex.uninstall')}
</MenuItem>
{disabled ?
<MenuItem
onSelected={() => enablePlugin(name)}
>
{t('PluginListIndex.plugin_enable')}
</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.plugin_disable')}
</MenuItem>
}
{hidden ? (
<MenuItem onSelected={onShow}>{t('PluginListIndex.show')}</MenuItem>
) : (
@@ -167,11 +147,10 @@ type PluginData = {
};
export default function PluginList({ isDeveloper }: { isDeveloper: boolean }) {
const { installedPlugins, disabled, 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();
@@ -185,17 +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={disabled.find(p => p.name == name) !== undefined} />,
label: <PluginListLabel name={name} frozen={frozen} hidden={hidden} version={version} />,
position: pluginOrder.indexOf(name),
data: {
name,
disabled: disabled.some(disabledPlugin => disabledPlugin.name === name),
frozen,
hidden,
isDeveloper,
@@ -209,9 +186,9 @@ export default function PluginList({ isDeveloper }: { isDeveloper: boolean }) {
};
}),
);
}, [installedPlugins, updates, hiddenPlugins]);
}, [plugins, updates, hiddenPlugins]);
if (installedPlugins.length === 0) {
if (plugins.length === 0) {
return (
<div>
<p>{t('PluginListIndex.no_plugin')}</p>
+2 -2
View File
@@ -3,13 +3,13 @@ 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 }) => {
+1 -1
View File
@@ -105,7 +105,7 @@ const BrowseTab: FC<{ setPluginCount: Dispatch<SetStateAction<number | null>> }>
})();
}, []);
const { installedPlugins } = useDeckyState();
const { plugins: installedPlugins } = useDeckyState();
return (
<>
+8 -34
View File
@@ -30,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';
@@ -39,7 +39,6 @@ import Toaster from './toaster';
import { getVersionInfo } from './updater';
import { getSetting, setSetting } from './utils/settings';
import TranslationHelper, { TranslationClass } from './utils/TranslationHelper';
import PluginDisablelModal from './components/modals/PluginDisablelModal';
const StorePage = lazy(() => import('./components/store/Store'));
const SettingsPage = lazy(() => import('./components/settings'));
@@ -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',
@@ -199,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');
@@ -208,10 +206,10 @@ class PluginLoader extends Logger {
let registration: any;
const uiMode = await new Promise(
(r) =>
(registration = SteamClient.UI.RegisterForUIModeChanged((mode: EUIMode) => {
r(mode);
registration.unregister();
})),
(registration = SteamClient.UI.RegisterForUIModeChanged((mode: EUIMode) => {
r(mode);
registration.unregister();
})),
);
if (uiMode == EUIMode.GamePad) {
// wait for SP window to exist before loading plugins
@@ -222,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();
@@ -343,10 +335,6 @@ class PluginLoader extends Logger {
showModal(<PluginUninstallModal name={name} title={title} buttonText={buttonText} description={description} />);
}
public disablePlugin(name: string, title: string, buttonText: string, description: string) {
showModal(<PluginDisablelModal name={name} title={title} buttonText={buttonText} description={description} />);
}
public hasPlugin(name: string) {
return Boolean(this.plugins.find((plugin) => plugin.name == name));
}
@@ -385,17 +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().disabled,
{ 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?.();
@@ -415,15 +392,12 @@ class PluginLoader extends Logger {
return;
}
this.deckyState.setDisabledPlugins(this.deckyState.publicState().disabled.filter(d => d.name !== name))
try {
if (useQueue) this.reloadLock = true;
this.log(`Trying to load ${name}`);
this.unloadPlugin(name, true);
const startTime = performance.now();
await this.importReactPlugin(name, version, loadType);
const endTime = performance.now();
-4
View File
@@ -14,8 +14,6 @@ export interface Plugin {
titleView?: JSX.Element;
}
export type DisabledPlugin = Pick<Plugin, 'name' | 'version'>;
export enum InstallType {
INSTALL,
REINSTALL,
@@ -57,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');
@@ -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,