feat: add support for external apps
And added GBA Emulator external app
This commit is contained in:
parent
cf8062c83c
commit
a6922ac448
7 changed files with 104 additions and 12 deletions
BIN
app/static/app-icons/gba-emulator.png
Normal file
BIN
app/static/app-icons/gba-emulator.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 498 KiB |
26
client/src/lib/Apps/GBAEmulator/GBAEmulator.svelte
Normal file
26
client/src/lib/Apps/GBAEmulator/GBAEmulator.svelte
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import AppContainer from '../AppContainer.svelte'
|
||||||
|
import { Notifications } from '../../Notifications/notifications'
|
||||||
|
import { onMount } from 'svelte'
|
||||||
|
import { gbaEmulatorBootupSequence } from '../../Sequences/sequences'
|
||||||
|
|
||||||
|
const handleError = () => {
|
||||||
|
Notifications.warn(
|
||||||
|
'Failed to load the GBA Emulator app. Did you add it to the app/static/external-apps directory?'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
gbaEmulatorBootupSequence()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AppContainer useContainer={false} class="h-screen w-full">
|
||||||
|
<iframe
|
||||||
|
title="GBA Emulator"
|
||||||
|
src="/static/external-apps/gba-emulator/index.html"
|
||||||
|
class="w-full h-screen rounded-xl"
|
||||||
|
frameborder="0"
|
||||||
|
on:error={handleError}
|
||||||
|
/>
|
||||||
|
</AppContainer>
|
|
@ -11,7 +11,7 @@
|
||||||
import { musicPlayerBootupSequence } from '../../Sequences/sequences'
|
import { musicPlayerBootupSequence } from '../../Sequences/sequences'
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
setTimeout(musicPlayerBootupSequence, 5000)
|
musicPlayerBootupSequence()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -2,20 +2,45 @@ import Camera from './Camera/Camera.svelte'
|
||||||
import MusicBrowser from './MusicBrowser/MusicBrowser.svelte'
|
import MusicBrowser from './MusicBrowser/MusicBrowser.svelte'
|
||||||
import Settings from './Settings/Settings.svelte'
|
import Settings from './Settings/Settings.svelte'
|
||||||
|
|
||||||
export const appList = {
|
type Format = 'png' | 'jpg' | 'webp'
|
||||||
|
const resolveIconPath = (slug: keyof typeof appList, format: Format) => {
|
||||||
|
return `/static/app-icons/${slug}.${format}`
|
||||||
|
}
|
||||||
|
|
||||||
|
import GBAEmulator from './GBAEmulator/GBAEmulator.svelte'
|
||||||
|
|
||||||
|
interface AppList {
|
||||||
|
[key: string]: {
|
||||||
|
name: string
|
||||||
|
component: any
|
||||||
|
icon: string
|
||||||
|
external: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const appList: AppList = {
|
||||||
'camera': {
|
'camera': {
|
||||||
name: 'Camera',
|
name: 'Camera',
|
||||||
component: Camera,
|
component: Camera,
|
||||||
icon: '/static/app-icons/camera.png',
|
icon: resolveIconPath('camera', 'png'),
|
||||||
|
external: false,
|
||||||
},
|
},
|
||||||
'media-player': {
|
'media-player': {
|
||||||
name: 'Media Player',
|
name: 'Media Player',
|
||||||
component: MusicBrowser,
|
component: MusicBrowser,
|
||||||
icon: '/static/app-icons/media-player.png',
|
icon: resolveIconPath('media-player', 'png'),
|
||||||
|
external: false,
|
||||||
},
|
},
|
||||||
'settings': {
|
'settings': {
|
||||||
name: 'Settings',
|
name: 'Settings',
|
||||||
component: Settings,
|
component: Settings,
|
||||||
icon: '/static/app-icons/settings.webp',
|
icon: resolveIconPath('settings', 'webp'),
|
||||||
|
external: false,
|
||||||
|
},
|
||||||
|
'gba-emulator': {
|
||||||
|
name: 'GBA Emulator',
|
||||||
|
component: GBAEmulator,
|
||||||
|
icon: resolveIconPath('gba-emulator', 'png'),
|
||||||
|
external: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ export class Notifications {
|
||||||
const sendToast = (duration: number) => {
|
const sendToast = (duration: number) => {
|
||||||
toast.success(message, {
|
toast.success(message, {
|
||||||
style:
|
style:
|
||||||
'padding: 25px; font-size: 1.5rem; background-color: #15803d; color: #fafafa;',
|
'padding: 25px; font-size: 1.5rem; background-color: #15803d; color: #fafafa; gap: 0.5rem;',
|
||||||
duration,
|
duration,
|
||||||
...options,
|
...options,
|
||||||
})
|
})
|
||||||
|
@ -45,7 +45,7 @@ export class Notifications {
|
||||||
const sendToast = (duration: number) => {
|
const sendToast = (duration: number) => {
|
||||||
toast.error(message, {
|
toast.error(message, {
|
||||||
style:
|
style:
|
||||||
'padding: 25px; font-size: 1.5rem; background-color: #dc2626; color: #fafafa;',
|
'padding: 25px; font-size: 1.5rem; background-color: #dc2626; color: #fafafa; gap: 0.5rem;',
|
||||||
duration,
|
duration,
|
||||||
...options,
|
...options,
|
||||||
})
|
})
|
||||||
|
@ -66,7 +66,7 @@ export class Notifications {
|
||||||
public static info(message: string, options?: NotificationOptions) {
|
public static info(message: string, options?: NotificationOptions) {
|
||||||
const sendToast = (duration: number) => {
|
const sendToast = (duration: number) => {
|
||||||
toast(message, {
|
toast(message, {
|
||||||
style: 'padding: 25px; font-size: 1.5rem;',
|
style: 'padding: 25px; font-size: 1.5rem; gap: 0.5rem;',
|
||||||
icon: InfoIcon,
|
icon: InfoIcon,
|
||||||
duration,
|
duration,
|
||||||
...options,
|
...options,
|
||||||
|
@ -88,7 +88,7 @@ export class Notifications {
|
||||||
const sendToast = (duration: number) => {
|
const sendToast = (duration: number) => {
|
||||||
toast(message, {
|
toast(message, {
|
||||||
style:
|
style:
|
||||||
'padding: 25px; font-size: 1.5rem; background-color: #f59e0b; color: #fafafa;',
|
'padding: 25px; font-size: 1.5rem; background-color: #f59e0b; color: #fafafa; gap: 0.5rem;',
|
||||||
icon: WarnIcon,
|
icon: WarnIcon,
|
||||||
duration,
|
duration,
|
||||||
...options,
|
...options,
|
||||||
|
|
|
@ -196,6 +196,25 @@ export const infotainmentBootupSequence = async () => {
|
||||||
}, 3000)
|
}, 3000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits for the infotainment system to boot up before executing the given sequence.
|
||||||
|
* Designed to be used by apps who want to play a bootup sequence but not overlap with the default one.
|
||||||
|
* If it's already booted, the sequence will be executed immediately.
|
||||||
|
*
|
||||||
|
* @param sequence - The sequence to execute after infotainment bootup, or immediately it already booted.
|
||||||
|
* @param delay? - The delay in milliseconds to wait if infotainment system is currently booting. Defaults to 5000ms
|
||||||
|
*/
|
||||||
|
const waitForInfotainmentBootup = (
|
||||||
|
sequence: () => void,
|
||||||
|
delay: number = 5000
|
||||||
|
) => {
|
||||||
|
if (!get(sequenceStore).infotainmentStartedFirstTime) {
|
||||||
|
setTimeout(sequence, delay)
|
||||||
|
} else {
|
||||||
|
sequence()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const musicPlayerBootupSequence = async () => {
|
export const musicPlayerBootupSequence = async () => {
|
||||||
if (
|
if (
|
||||||
get(sequenceStore).musicStartedFirstTime ||
|
get(sequenceStore).musicStartedFirstTime ||
|
||||||
|
@ -207,8 +226,28 @@ export const musicPlayerBootupSequence = async () => {
|
||||||
|
|
||||||
sequenceStore.update('musicStartedFirstTime', true)
|
sequenceStore.update('musicStartedFirstTime', true)
|
||||||
|
|
||||||
Notifications.info('Downloading copyrighted music...', {
|
waitForInfotainmentBootup(() => {
|
||||||
withAudio: true,
|
Notifications.info('Downloading copyrighted music...', {
|
||||||
src: getVoicePath('downloading-copyrighted-music', 'en'),
|
withAudio: true,
|
||||||
|
src: getVoicePath('downloading-copyrighted-music', 'en'),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const gbaEmulatorBootupSequence = async () => {
|
||||||
|
if (
|
||||||
|
get(sequenceStore).gbaEmulatorStartedFirstTime ||
|
||||||
|
get(settingsStore).disableAnnoyances
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
await tick()
|
||||||
|
sequenceStore.update('gbaEmulatorStartedFirstTime', true)
|
||||||
|
|
||||||
|
waitForInfotainmentBootup(() => {
|
||||||
|
Notifications.info('Loading pirated Nintendo ROMs', {
|
||||||
|
withAudio: true,
|
||||||
|
src: getVoicePath('loading-pirated-nintendo', 'en'),
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,13 @@ import { writable } from 'svelte/store'
|
||||||
interface SequenceStoreData {
|
interface SequenceStoreData {
|
||||||
infotainmentStartedFirstTime: boolean
|
infotainmentStartedFirstTime: boolean
|
||||||
musicStartedFirstTime: boolean
|
musicStartedFirstTime: boolean
|
||||||
|
gbaEmulatorStartedFirstTime: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
let defaults: SequenceStoreData = {
|
let defaults: SequenceStoreData = {
|
||||||
infotainmentStartedFirstTime: false, // for infotainment bootup sequence
|
infotainmentStartedFirstTime: false, // for infotainment bootup sequence
|
||||||
musicStartedFirstTime: false,
|
musicStartedFirstTime: false,
|
||||||
|
gbaEmulatorStartedFirstTime: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
const createSequenceStore = () => {
|
const createSequenceStore = () => {
|
||||||
|
|
Loading…
Reference in a new issue