Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,6 @@ act/.directory
act/artifacts/*
bin/act
/settings/

# macOS
.DS_Store
243 changes: 243 additions & 0 deletions frontend/src/components/AnnouncementsDisplay.tsx
Original file line number Diff line number Diff line change
@@ -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.',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where should people direct feedback?

(Also i assume this shows the first time you use decky? Might want to add something like start with looking at the store)

Copy link
Contributor

@RodoMa92 RodoMa92 Sep 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point, but there is another blocking issue. The text need to be moved in en-US.json and not hardcoded here.

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 changes: 2 additions & 0 deletions frontend/src/components/PluginView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -41,6 +42,7 @@ const PluginView: FC = () => {
paddingTop: '16px',
}}
>
<AnnouncementsDisplay />
<PanelSection>
{pluginList.map(({ name, icon }) => (
<PanelSectionRow key={name}>
Expand Down
49 changes: 49 additions & 0 deletions frontend/src/store.tsx → frontend/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,62 @@ 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>;

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,
Expand Down