feat: add more sequences, loading splash, and settings

This commit is contained in:
Youwen Wu 2024-02-24 17:40:47 -08:00
parent 3508746e07
commit cfdb9094ce
21 changed files with 530 additions and 20 deletions

View file

@ -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",

View file

@ -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",

View file

@ -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)
setTimeout(() => {
loading = false
initializationSequence()
}, 3000)
})
</script>
<main class="select-none">
<main
class="select-none transition-opacity duration-300"
class:opacity-0={loading}
>
<!-- driver dashboard -->
<div class="h-screen w-[35vw] fixed shadow-lg shadow-slate-800 z-10">
<Dashboard />
@ -50,9 +65,14 @@
</div>
<!-- toast service -->
<Toaster />
</main>
{#if loading}
<Loading />
{/if}
<Toaster />
<style lang="postcss">
main {
font-family: 'Roboto', sans-serif;

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="26.6" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 308"><path fill="#FF3E00" d="M239.682 40.707C211.113-.182 154.69-12.301 113.895 13.69L42.247 59.356a82.198 82.198 0 0 0-37.135 55.056a86.566 86.566 0 0 0 8.536 55.576a82.425 82.425 0 0 0-12.296 30.719a87.596 87.596 0 0 0 14.964 66.244c28.574 40.893 84.997 53.007 125.787 27.016l71.648-45.664a82.182 82.182 0 0 0 37.135-55.057a86.601 86.601 0 0 0-8.53-55.577a82.409 82.409 0 0 0 12.29-30.718a87.573 87.573 0 0 0-14.963-66.244"></path><path fill="#FFF" d="M106.889 270.841c-23.102 6.007-47.497-3.036-61.103-22.648a52.685 52.685 0 0 1-9.003-39.85a49.978 49.978 0 0 1 1.713-6.693l1.35-4.115l3.671 2.697a92.447 92.447 0 0 0 28.036 14.007l2.663.808l-.245 2.659a16.067 16.067 0 0 0 2.89 10.656a17.143 17.143 0 0 0 18.397 6.828a15.786 15.786 0 0 0 4.403-1.935l71.67-45.672a14.922 14.922 0 0 0 6.734-9.977a15.923 15.923 0 0 0-2.713-12.011a17.156 17.156 0 0 0-18.404-6.832a15.78 15.78 0 0 0-4.396 1.933l-27.35 17.434a52.298 52.298 0 0 1-14.553 6.391c-23.101 6.007-47.497-3.036-61.101-22.649a52.681 52.681 0 0 1-9.004-39.849a49.428 49.428 0 0 1 22.34-33.114l71.664-45.677a52.218 52.218 0 0 1 14.563-6.398c23.101-6.007 47.497 3.036 61.101 22.648a52.685 52.685 0 0 1 9.004 39.85a50.559 50.559 0 0 1-1.713 6.692l-1.35 4.116l-3.67-2.693a92.373 92.373 0 0 0-28.037-14.013l-2.664-.809l.246-2.658a16.099 16.099 0 0 0-2.89-10.656a17.143 17.143 0 0 0-18.398-6.828a15.786 15.786 0 0 0-4.402 1.935l-71.67 45.674a14.898 14.898 0 0 0-6.73 9.975a15.9 15.9 0 0 0 2.709 12.012a17.156 17.156 0 0 0 18.404 6.832a15.841 15.841 0 0 0 4.402-1.935l27.345-17.427a52.147 52.147 0 0 1 14.552-6.397c23.101-6.006 47.497 3.037 61.102 22.65a52.681 52.681 0 0 1 9.003 39.848a49.453 49.453 0 0 1-22.34 33.12l-71.664 45.673a52.218 52.218 0 0 1-14.563 6.398"></path></svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -7,9 +7,15 @@
-->
<script lang="ts">
import { onDestroy } from 'svelte'
import { fade } from 'svelte/transition'
import { infotainmentBootupSequence } from '../Sequences/sequences'
export let useContainer: boolean = true
onDestroy(() => {
infotainmentBootupSequence()
})
</script>
<div

View file

@ -7,6 +7,12 @@
import Song from './Song.svelte'
import { songList } from '../../Dashboard/MediaPlayer/songList'
import AppContainer from '../AppContainer.svelte'
import { onMount } from 'svelte'
import { musicPlayerBootupSequence } from '../../Sequences/sequences'
onMount(() => {
setTimeout(musicPlayerBootupSequence, 5000)
})
</script>
<AppContainer

View file

@ -1,22 +1,44 @@
<script lang="ts">
import { Notifications } from '../../Notifications/notifications'
import { settingsStore } from '../../stores/settingsStore'
import AppContainer from '../AppContainer.svelte'
import SettingsToggle from './SettingsToggle.svelte'
const handleClick = () => {
Notifications.error('Jankboard initialized', {
src: '/static/voices/en/jankboard-initialized.wav',
settingsStore.subscribe(async value => {
window.localStorage.setItem('settings', JSON.stringify(value))
})
Notifications.playAudio('static/voices/en/jankboard-initialized.wav')
const resetSettings = () => {
window.localStorage.setItem('settings', '')
settingsStore.reset()
Notifications.success('Settings reset! Refresh for all changes to apply.')
}
</script>
<AppContainer
class="flex gap-4 bg-blue-200 bg-opacity-25 backdrop-blur-xl media-background rounded-3xl flex-wrap px-10 py-20"
class="flex gap-6 bg-blue-200 bg-opacity-25 backdrop-blur-xl media-background rounded-3xl flex-wrap px-10 py-20"
>
<button
class="px-4 py-2 bg-blue-500 rounded-md hover:brightness-75"
on:click={handleClick}
<h1 class="text-5xl font-medium text-slate-100 basis-full">Settings</h1>
<h2 class="text-2xl font-medium text-slate-200 mt-4 basis-full">General</h2>
<div class="flex flex-col gap-2">
<SettingsToggle
setting="disableAnnoyances"
tooltip="Disable non-critical popups and audio cues."
>Disable Annoyances</SettingsToggle
>
Test Toast
</button>
<SettingsToggle
setting="goWoke"
tooltip="Disables content that could be perceived as offensive for PR and DEI purposes."
>Go Woke</SettingsToggle
>
<button
class="mt-10 px-4 py-2 bg-blue-500 hover:brightness-75 text-medium rounded-lg w-min"
on:click={resetSettings}>Reset</button
>
<footer class="bottom-0 -mb-10 mt-10 text-slate-300">
Settings are synced to the browser's local storage. If things seem broken,
try clearing the local Jankboard data and try again.
</footer>
</div>
</AppContainer>

View file

@ -0,0 +1,27 @@
<!--
@component
@param setting - The setting to be toggled
@param inverted? - If false, the toggle syncs to the setting (toggle = on, setting = true). If true, the toggle syncs to the setting's inverse (toggle = off, setting = true).
@param tooltip - Helpful tooltip for the setting
@children The setting's label
-->
<script lang="ts">
import type { SettingsStoreData } from '../../stores/settingsStore'
import { settingsStore } from '../../stores/settingsStore'
import Switch from './Switch.svelte'
export let setting: keyof SettingsStoreData
export let inverted: boolean = false
export let tooltip: string = ''
$: value = inverted ? !$settingsStore[setting] : $settingsStore[setting]
const handleClick = () => {
settingsStore.update(setting, !value)
}
</script>
<Switch bind:checked={value} on:click={handleClick} {tooltip}><slot /></Switch>

View file

@ -0,0 +1,77 @@
<script lang="ts">
import { Tooltip } from '@svelte-plugins/tooltips'
export let checked: boolean
export let tooltip: string
</script>
<div class="flex gap-2">
<label class="switch">
<input type="checkbox" bind:checked on:click />
<span class="slider" />
</label>
{#if tooltip !== ''}
<Tooltip content={tooltip} arrow={false}>
<span class="flex-grow text-xl text-slate-100 font-medium"
><slot />
</span>
</Tooltip>
{:else}
<span class="flex-grow text-xl text-slate-100 font-medium"><slot /> </span>
{/if}
</div>
<style>
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: 0.4s;
transition: 0.4s;
border-radius: 34px;
}
.slider:before {
position: absolute;
content: '';
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: 0.4s;
transition: 0.4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #2196f3;
}
input:checked + .slider {
box-shadow: 0 0 1px #2196f3;
}
input:checked + .slider:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}
</style>

View file

@ -0,0 +1,30 @@
<script lang="ts">
import { blur } from 'svelte/transition'
import SvelteLogo from './SvelteLogo.svelte'
</script>
<div
class="absolute w-screen h-screen flex justify-center items-center flex-col overflow-hidden bg"
transition:blur={{ duration: 300, amount: 0.5 }}
>
<div class="max-w-64">
<SvelteLogo />
</div>
</div>
<style lang="postcss">
.bg {
background: #2c3e50; /* fallback for old browsers */
background: -webkit-linear-gradient(
to right,
#2c3e50,
#fd746c
); /* Chrome 10-25, Safari 5.1-6 */
background: linear-gradient(
to right,
#2c3e50,
#fd746c
); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
}
</style>

View file

@ -0,0 +1,63 @@
<script lang="ts">
import { quintOut } from 'svelte/easing'
import { fade, draw, fly } from 'svelte/transition'
import { expand } from './customTransitions'
import { inner, outer } from './shape'
import { onMount } from 'svelte'
let visible = false
onMount(() => {
visible = true
})
</script>
{#if visible}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 103 124">
<g out:fade={{ duration: 200 }} opacity="0.3">
<path
in:expand={{ duration: 400, delay: 1000, easing: quintOut }}
style="stroke: #ff3e00; fill: #ff3e00; stroke-width: 50;"
d={outer}
/>
<path
in:draw={{ duration: 1000 }}
style="stroke:#ff3e00; stroke-width: 1.5"
d={inner}
/>
</g>
</svg>
<div class="centered" out:fly={{ y: -20, duration: 800 }}>
{#each 'JANKBOARD' as char, i}
<span in:fade|global={{ delay: 1000 + i * 150, duration: 800 }}
>{char}</span
>
{/each}
</div>
{/if}
<style lang="postcss">
svg {
width: 100%;
height: 100%;
}
path {
fill: white;
opacity: 1;
}
.centered {
@apply text-8xl absolute text-slate-300;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
letter-spacing: 0.12em;
font-weight: 400;
}
.centered span {
will-change: filter;
}
</style>

View file

@ -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}`,
}
}

View file

@ -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`

View file

@ -0,0 +1,10 @@
<script lang="ts">
export let size = '1.5rem'
</script>
<span
class="material-symbols-outlined text-[#fafafa]"
style="font-size: {size}"
>
warning
</span>

View file

@ -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],

View file

@ -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'),
})
}

View file

@ -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<SequenceStoreData>(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()

View file

@ -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<SettingsStoreData>(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()

View file

@ -0,0 +1,7 @@
export default function getSettings() {
if (localStorage.getItem('settings') !== null) {
return JSON.parse(localStorage.getItem('settings') as string)
} else {
return false
}
}