diff --git a/client/package-lock.json b/client/package-lock.json index 05b612a..dacf5a9 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -16,6 +16,7 @@ "svelte-french-toast": "^1.2.0" }, "devDependencies": { + "@svelte-plugins/tooltips": "^3.0.0", "@sveltejs/adapter-static": "^3.0.1", "@sveltejs/vite-plugin-svelte": "^3.0.2", "@tsconfig/svelte": "^5.0.2", @@ -713,6 +714,15 @@ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" }, + "node_modules/@svelte-plugins/tooltips": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@svelte-plugins/tooltips/-/tooltips-3.0.0.tgz", + "integrity": "sha512-lE7LKU01OY8XYwsWmRxEcBZJqVv+f/TP5JQP5eqzb9ppS9UeN8DF/mFP9i2BfELRfYdwl4qwUENTPYBndMI3LA==", + "dev": true, + "peerDependencies": { + "svelte": "^3.54.0 || ^4.0.0 || ^5.0.0-next.0" + } + }, "node_modules/@sveltejs/adapter-static": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.1.tgz", diff --git a/client/package.json b/client/package.json index b93d26b..a3cddcf 100644 --- a/client/package.json +++ b/client/package.json @@ -10,6 +10,7 @@ "check": "svelte-check --tsconfig ./tsconfig.json" }, "devDependencies": { + "@svelte-plugins/tooltips": "^3.0.0", "@sveltejs/adapter-static": "^3.0.1", "@sveltejs/vite-plugin-svelte": "^3.0.2", "@tsconfig/svelte": "^5.0.2", diff --git a/client/src/App.svelte b/client/src/App.svelte index 4bec224..29449d9 100644 --- a/client/src/App.svelte +++ b/client/src/App.svelte @@ -9,6 +9,9 @@ import { onMount } from 'svelte' import { Toaster } from 'svelte-french-toast' import { initializationSequence } from './lib/Sequences/sequences' + import Loading from './lib/Loading/Loading.svelte' + import { settingsStore } from './lib/stores/settingsStore' + import getSettings from './lib/utils/getSettings' let activeApp: App = 'camera' let topics: TelemetryTopics = { @@ -27,13 +30,25 @@ booleans: ['ebrake', 'reorient', 'gpws'], } + let loading = $settingsStore.fastStartup ? false : true + onMount(() => { + let savedSettings = getSettings() + if (savedSettings !== false) { + settingsStore.set(savedSettings) + } initializeTelemetry(topics, 200) - initializationSequence() + setTimeout(() => { + loading = false + initializationSequence() + }, 3000) }) -
+
@@ -50,9 +65,14 @@
-
+{#if loading} + +{/if} + + + diff --git a/client/src/lib/Loading/Loading.svelte b/client/src/lib/Loading/Loading.svelte new file mode 100644 index 0000000..d92a2df --- /dev/null +++ b/client/src/lib/Loading/Loading.svelte @@ -0,0 +1,30 @@ + + +
+
+ +
+
+ + diff --git a/client/src/lib/Loading/SvelteLogo.svelte b/client/src/lib/Loading/SvelteLogo.svelte new file mode 100644 index 0000000..c36e6b1 --- /dev/null +++ b/client/src/lib/Loading/SvelteLogo.svelte @@ -0,0 +1,63 @@ + + +{#if visible} + + + + + + + +
+ {#each 'JANKBOARD' as char, i} + {char} + {/each} +
+{/if} + + diff --git a/client/src/lib/Loading/customTransitions.ts b/client/src/lib/Loading/customTransitions.ts new file mode 100644 index 0000000..ac38cc2 --- /dev/null +++ b/client/src/lib/Loading/customTransitions.ts @@ -0,0 +1,18 @@ +import { cubicOut } from 'svelte/easing' +import type { EasingFunction } from 'svelte/transition' + +export function expand( + node: Element, + params: { delay: number; duration: number; easing: EasingFunction } +) { + const { delay = 0, duration = 400, easing = cubicOut } = params + + const w = parseFloat(getComputedStyle(node).strokeWidth) + + return { + delay, + duration, + easing, + css: (t: number) => `opacity: ${t}; stroke-width: ${t * w}`, + } +} diff --git a/client/src/lib/Loading/shape.ts b/client/src/lib/Loading/shape.ts new file mode 100644 index 0000000..9d33931 --- /dev/null +++ b/client/src/lib/Loading/shape.ts @@ -0,0 +1,2 @@ +export const inner = `M45.41,108.86A21.81,21.81,0,0,1,22,100.18,20.2,20.2,0,0,1,18.53,84.9a19,19,0,0,1,.65-2.57l.52-1.58,1.41,1a35.32,35.32,0,0,0,10.75,5.37l1,.31-.1,1a6.2,6.2,0,0,0,1.11,4.08A6.57,6.57,0,0,0,41,95.19a6,6,0,0,0,1.68-.74L70.11,76.94a5.76,5.76,0,0,0,2.59-3.83,6.09,6.09,0,0,0-1-4.6,6.58,6.58,0,0,0-7.06-2.62,6.21,6.21,0,0,0-1.69.74L52.43,73.31a19.88,19.88,0,0,1-5.58,2.45,21.82,21.82,0,0,1-23.43-8.68A20.2,20.2,0,0,1,20,51.8a19,19,0,0,1,8.56-12.7L56,21.59a19.88,19.88,0,0,1,5.58-2.45A21.81,21.81,0,0,1,85,27.82,20.2,20.2,0,0,1,88.47,43.1a19,19,0,0,1-.65,2.57l-.52,1.58-1.41-1a35.32,35.32,0,0,0-10.75-5.37l-1-.31.1-1a6.2,6.2,0,0,0-1.11-4.08,6.57,6.57,0,0,0-7.06-2.62,6,6,0,0,0-1.68.74L36.89,51.06a5.71,5.71,0,0,0-2.58,3.83,6,6,0,0,0,1,4.6,6.58,6.58,0,0,0,7.06,2.62,6.21,6.21,0,0,0,1.69-.74l10.48-6.68a19.88,19.88,0,0,1,5.58-2.45,21.82,21.82,0,0,1,23.43,8.68A20.2,20.2,0,0,1,87,76.2a19,19,0,0,1-8.56,12.7L51,106.41a19.88,19.88,0,0,1-5.58,2.45` +export const outer = `M65,34 L37,52 A1 1 0 0 0 44 60 L70.5,44.5 A1 1 0 0 0 65,34Z M64,67 L36,85 A1 1 0 0 0 42 94 L68,77.5 A1 1 0 0 0 64,67Z` diff --git a/client/src/lib/Notifications/WarnIcon.svelte b/client/src/lib/Notifications/WarnIcon.svelte new file mode 100644 index 0000000..8468359 --- /dev/null +++ b/client/src/lib/Notifications/WarnIcon.svelte @@ -0,0 +1,10 @@ + + + + warning + diff --git a/client/src/lib/Notifications/notifications.ts b/client/src/lib/Notifications/notifications.ts index 737bae7..f58265a 100644 --- a/client/src/lib/Notifications/notifications.ts +++ b/client/src/lib/Notifications/notifications.ts @@ -2,6 +2,7 @@ import { toast } from 'svelte-french-toast' import type { ToastOptions } from 'svelte-french-toast' import InfoIcon from './InfoIcon.svelte' import { Howl } from 'howler' +import WarnIcon from './WarnIcon.svelte' interface NotificationOptions extends ToastOptions { withAudio?: boolean @@ -62,7 +63,7 @@ export class Notifications { sendToast(this.defaultDuration) } } - public static info(message: string, options?: any) { + public static info(message: string, options?: NotificationOptions) { const sendToast = (duration: number) => { toast(message, { style: 'padding: 25px; font-size: 1.5rem;', @@ -83,6 +84,28 @@ export class Notifications { sendToast(this.defaultDuration) } } + public static warn(message: string, options?: NotificationOptions) { + const sendToast = (duration: number) => { + toast(message, { + style: + 'padding: 25px; font-size: 1.5rem; background-color: #f59e0b; color: #fafafa;', + icon: WarnIcon, + duration, + ...options, + }) + } + if (options?.withAudio && options?.src) { + let sound: Howl + sound = new Howl({ + src: [options.src], + preload: true, + autoplay: true, + onload: () => sendToast(sound.duration() * 1000), + }) + } else { + sendToast(this.defaultDuration) + } + } public static playAudio(src: string) { new Howl({ src: [src], diff --git a/client/src/lib/Sequences/sequences.ts b/client/src/lib/Sequences/sequences.ts index 3c8fb86..3060f0a 100644 --- a/client/src/lib/Sequences/sequences.ts +++ b/client/src/lib/Sequences/sequences.ts @@ -3,26 +3,148 @@ define various sequences to play out in this file for example, we can define an initialization sequence that plays out some series of notifications, and call it whenever we need it, or a sequence to change the screen color and play some audio queues for a crash +these sequences should be self contained and not rely on any external state outside of +that in sequenceStore so that they can be easily invoked from anywhere */ import { Notifications } from '../Notifications/notifications' +import { sequenceStore } from '../stores/sequenceStore' +import { settingsStore } from '../stores/settingsStore' +import { get } from 'svelte/store' import getVoicePath from '../utils/getVoicePath' +import { tick } from 'svelte' -export const initializationSequence = () => { +// await a "tick" (a svelte update frame) at the start of every sequence so that +// state is synced and no weird side effects occur + +export const initializationSequence = async () => { + await tick() Notifications.info('Jankboard initialized!', { withAudio: true, src: getVoicePath('jankboard-initialized', 'en'), }) setTimeout(() => { - Notifications.success('LittenOS is online.', { + if (get(settingsStore).goWoke) return + Notifications.success('LittenOS is online', { withAudio: true, src: getVoicePath('littenos-is-online', 'en'), }) setTimeout(() => { - Notifications.error('Breaching Monte Vista codebase.', { + Notifications.warn('Breaching Monte Vista codebase', { withAudio: true, src: getVoicePath('breaching-monte-vista', 'en'), }) + setTimeout(() => { + Notifications.playAudio(getVoicePath('hello-virtual-assistant', 'en')) + }, 3000) }, 3000) }, 3000) } + +export const criticalFailureIminentSequence = async () => { + Notifications.error('Critical robot failure imminent', { + withAudio: true, + src: getVoicePath('critical-robot-failure', 'en'), + }) +} + +export const collisionDetectedSequence = async () => { + Notifications.error('Collision detected', { + withAudio: true, + src: getVoicePath('collision-detected', 'en'), + }) +} + +export const collisionImminentSequence = async () => { + Notifications.error('Collision imminent', { + withAudio: true, + src: getVoicePath('collision-imminent', 'en'), + }) +} + +export const cruiseControlEngagedSequence = async () => { + if (get(settingsStore).disableAnnoyances) return + Notifications.success('Cruise control engaged', { + withAudio: true, + src: getVoicePath('cruise-control-engaged', 'en'), + }) +} + +export const retardSequence = async () => { + if (get(settingsStore).goWoke) return + Notifications.warn('Retard', { + withAudio: true, + src: getVoicePath('retard', 'en'), + }) +} + +export const breaching254Sequence = async () => { + if (get(settingsStore).disableAnnoyances) return + Notifications.info('Breaching 254 mainframe', { + withAudio: true, + src: getVoicePath('breaching-254-mainframe', 'en'), + }) +} + +export const breaching1323Sequence = async () => { + if (get(settingsStore).disableAnnoyances) return + Notifications.info('Breaching 1323 mainframe', { + withAudio: true, + src: getVoicePath('breaching-1323-mainframe', 'en'), + }) +} + +export const bullyingRohanSequence = async () => { + if (get(settingsStore).disableAnnoyances) return + Notifications.info('Bullying Rohan', { + withAudio: true, + src: getVoicePath('bullying-rohan', 'en'), + }) +} + +export const userErrorDetectedSequence = async () => { + Notifications.error('User error detected', { + withAudio: true, + src: getVoicePath('user-error-detected', 'en'), + }) +} + +export const infotainmentBootupSequence = async () => { + if ( + get(sequenceStore).infotainmentStartedFirstTime || + get(settingsStore).disableAnnoyances + ) + return + + await tick() + + sequenceStore.update('infotainmentStartedFirstTime', true) + + Notifications.info('Infotainment system buffering', { + withAudio: true, + src: getVoicePath('infotainment-system-buffering', 'en'), + }) + setTimeout(() => { + Notifications.success('Infotainment system online', { + withAudio: true, + src: getVoicePath('infotainment-system-online', 'en'), + }) + }, 3000) +} + +export const musicPlayerBootupSequence = async () => { + if ( + get(sequenceStore).musicStartedFirstTime || + get(settingsStore).disableAnnoyances + ) + return + + await tick() + + sequenceStore.update('musicStartedFirstTime', true) + + Notifications.info('Downloading copyrighted music...', { + withAudio: true, + src: getVoicePath('downloading-copyrighted-music', 'en'), + }) +} diff --git a/client/src/lib/stores/sequenceStore.ts b/client/src/lib/stores/sequenceStore.ts new file mode 100644 index 0000000..eec35b0 --- /dev/null +++ b/client/src/lib/stores/sequenceStore.ts @@ -0,0 +1,32 @@ +/* in this store, put stateful variables that sequences need to access */ + +import { writable } from 'svelte/store' + +interface SequenceStoreData { + infotainmentStartedFirstTime: boolean + musicStartedFirstTime: boolean +} + +let defaults: SequenceStoreData = { + infotainmentStartedFirstTime: false, // for infotainment bootup sequence + musicStartedFirstTime: false, +} + +const createSequenceStore = () => { + const { subscribe, set, update } = writable(defaults) + return { + subscribe, + update: ( + data: keyof SequenceStoreData, + newValue: SequenceStoreData[typeof data] + ) => { + update(store => { + store[data] = newValue + return store + }) + }, + reset: () => set(defaults), + } +} + +export const sequenceStore = createSequenceStore() diff --git a/client/src/lib/stores/settingsStore.ts b/client/src/lib/stores/settingsStore.ts new file mode 100644 index 0000000..e6a063e --- /dev/null +++ b/client/src/lib/stores/settingsStore.ts @@ -0,0 +1,35 @@ +/* stores global app wide settings */ + +import { writable } from 'svelte/store' + +export interface SettingsStoreData { + disableAnnoyances: boolean + goWoke: boolean + fastStartup: boolean +} + +export const defaults: SettingsStoreData = { + disableAnnoyances: false, // disable non-critical notifications + goWoke: false, // go woke (for showing parents or other officials where DEI has taken over), disables "offensive" sequences + fastStartup: false, // skip the loading splash screen (for development purposes. Setting this from within the app has no effect.) +} + +const createSequenceStore = () => { + const { subscribe, set, update } = writable(defaults) + return { + subscribe, + update: ( + data: keyof SettingsStoreData, + newValue: SettingsStoreData[typeof data] + ) => { + update(store => { + store[data] = newValue + return store + }) + }, + reset: () => set(defaults), + set: (data: SettingsStoreData) => set(data), + } +} + +export const settingsStore = createSequenceStore() diff --git a/client/src/lib/utils/getSettings.ts b/client/src/lib/utils/getSettings.ts new file mode 100644 index 0000000..496c32f --- /dev/null +++ b/client/src/lib/utils/getSettings.ts @@ -0,0 +1,7 @@ +export default function getSettings() { + if (localStorage.getItem('settings') !== null) { + return JSON.parse(localStorage.getItem('settings') as string) + } else { + return false + } +}