Compare commits

...

23 Commits

Author SHA1 Message Date
Beebles 86d01db2b9 fix hide behaviour for potential testers
Builder Win / Build PluginLoader for Win (push) Has been cancelled
Builder / Build PluginLoader (push) Has been cancelled
Push Updated Plugin Stub to Template / copy-stub (push) Has been cancelled
Lint / Run linters (push) Has been cancelled
Type Check / Run type checkers (push) Has been cancelled
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
Builder Win / Build PluginLoader for Win (push) Has been cancelled
Builder / Build PluginLoader (push) Has been cancelled
Push Updated Plugin Stub to Template / copy-stub (push) Has been cancelled
Lint / Run linters (push) Has been cancelled
Type Check / Run type checkers (push) Has been cancelled
2025-08-17 11:13:52 -06:00
Beebles 1709a957f7 ensure nulls arent passed to sort
Builder Win / Build PluginLoader for Win (push) Has been cancelled
Builder / Build PluginLoader (push) Has been cancelled
Push Updated Plugin Stub to Template / copy-stub (push) Has been cancelled
Lint / Run linters (push) Has been cancelled
Type Check / Run type checkers (push) Has been cancelled
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
4 changed files with 297 additions and 0 deletions
+3
View File
@@ -165,3 +165,6 @@ act/.directory
act/artifacts/*
bin/act
/settings/
# macOS
.DS_Store
@@ -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>
);
}
+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}>
@@ -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,