feat: de-clutter navbar

fix: backup option not working
refactor: persistent writable stores

[deploy]
This commit is contained in:
2023-07-29 22:50:18 +02:00
parent 7d148d0c2c
commit 4cc9462655
16 changed files with 166 additions and 244 deletions

View File

@@ -9,6 +9,16 @@ const de = {
DOWNLOAD: "Kopie Speichern",
RESTORE: "Wiederherstellen",
},
profile: {
TITLE: "Profil",
LANGUAGE: "Sprache",
theme: {
TITLE: "Darstellung",
COLOR_SCHEME: "Farbschema",
DARK_MODE: "Dunkel",
LIGHT_MODE: "Hell",
},
},
deviceManager: {
TITLE: "Gerät",
AUTO_CONNECT: "Automatisch Verbinden",

View File

@@ -8,6 +8,16 @@ const en = {
DOWNLOAD: "Download Backup",
RESTORE: "Restore",
},
profile: {
TITLE: "Profile",
LANGUAGE: "Language",
theme: {
TITLE: "Theme",
COLOR_SCHEME: "Color scheme",
DARK_MODE: "Dark",
LIGHT_MODE: "Light",
},
},
deviceManager: {
TITLE: "Device",
AUTO_CONNECT: "Auto-connect",

View File

@@ -1,17 +1,17 @@
import {writable} from "svelte/store"
import type {Action} from "svelte/action"
import {persistentWritable} from "$lib/storage"
export interface UserPreferences {
backup: boolean
autoConnect: boolean
}
export const theme = writable({
export const theme = persistentWritable("user-theme", {
color: "#6D81C7",
mode: "dark" as "light" | "dark" | "auto",
})
export const userPreferences = writable<UserPreferences>({
export const userPreferences = persistentWritable<UserPreferences>("user-preferences", {
backup: false,
autoConnect: true,
})

View File

@@ -3,6 +3,8 @@ import {CharaDevice} from "$lib/serial/device"
import type {Chord} from "$lib/serial/chord"
import type {Writable} from "svelte/store"
import type {CharaLayout} from "$lib/serialization/layout"
import {persistentWritable} from "$lib/storage"
import {userPreferences} from "$lib/preferences"
export const serialPort = writable<CharaDevice>()
@@ -13,9 +15,13 @@ export interface SerialLogEntry {
export const serialLog = writable<SerialLogEntry[]>([])
export const chords = writable<Chord[]>([])
export const chords = persistentWritable<Chord[]>("chord-library", [], () => get(userPreferences).backup)
export const layout = writable<CharaLayout>([[], [], []])
export const layout = persistentWritable<CharaLayout>(
"layout",
[[], [], []],
() => get(userPreferences).backup,
)
export const settings = writable({})

View File

@@ -1,33 +0,0 @@
import {chords, layout} from "$lib/serial/connection"
import {userPreferences} from "$lib/preferences"
const PROFILE_KEY = "profiles"
const CHORD_LIBRARY_STORAGE_KEY = "chord-library"
const LAYOUT_STORAGE_KEY = "layouts"
const PREFERENCES = "user-preferences"
export function initLocalStorage() {
const storedPreferences = localStorage.getItem(PREFERENCES)
if (storedPreferences) {
userPreferences.set(JSON.parse(storedPreferences))
}
userPreferences.subscribe(preferences => {
localStorage.setItem(PREFERENCES, JSON.stringify(preferences))
})
const storedLayout = localStorage.getItem(LAYOUT_STORAGE_KEY)
if (storedLayout) {
layout.set(JSON.parse(storedLayout))
}
const storedChords = localStorage.getItem(CHORD_LIBRARY_STORAGE_KEY)
if (storedChords) {
chords.set(JSON.parse(storedChords))
}
layout.subscribe(layout => {
localStorage.setItem(LAYOUT_STORAGE_KEY, JSON.stringify(layout))
})
chords.subscribe(chords => {
localStorage.setItem(CHORD_LIBRARY_STORAGE_KEY, JSON.stringify(chords))
})
}

17
src/lib/storage.ts Normal file
View File

@@ -0,0 +1,17 @@
import type {Writable} from "svelte/store"
import {writable} from "svelte/store"
import {browser} from "$app/environment"
export function persistentWritable<T>(key: string, value: T, condition?: () => boolean): Writable<T> {
if (browser) {
const persistedValue = localStorage.getItem(key)
const store = persistedValue !== null ? writable(JSON.parse(persistedValue)) : writable(value)
store.subscribe(value => {
if (!condition || condition()) localStorage.setItem(key, JSON.stringify(value))
})
return store
} else {
return writable(value)
}
}

View File

@@ -25,9 +25,9 @@ $padding: 16px;
}
.tippy-box[data-theme~="search-completion"] {
overflow: hidden;
filter: none;
border-radius: 0 0 16px 16px;
overflow: hidden;
.tippy-content {
padding: 0;

View File

@@ -12,7 +12,6 @@
import {pwaInfo} from "virtual:pwa-info"
import type {LayoutServerData} from "./$types"
import type {RegisterSWOptions} from "vite-plugin-pwa/types"
import {initLocalStorage} from "$lib/serial/storage"
import {browser} from "$app/environment"
import BrowserWarning from "./BrowserWarning.svelte"
import "tippy.js/animations/shift-away.css"
@@ -47,7 +46,6 @@
const dark = it.mode === "dark" // window.matchMedia("(prefers-color-scheme: dark)").matches
applyTheme(theme, {target: document.body, dark})
})
initLocalStorage()
if (pwaInfo) {
const {registerSW} = await import("virtual:pwa-register")

View File

@@ -28,7 +28,7 @@
URL.revokeObjectURL(downloadUrl)
}
async function restoreBackup(event: InputEvent) {
async function restoreBackup(event: Event) {
const input = (event.target as HTMLInputElement).files![0]
if (!input) return
const backup = await parseCompressed<Backup>(input)
@@ -119,10 +119,10 @@
border-radius: 32px;
transition: all 250ms ease;
}
&.primary {
button.primary {
color: var(--md-sys-color-on-primary);
background: var(--md-sys-color-primary);
}
}
</style>

View File

@@ -82,10 +82,6 @@
mask: url("/browsers/googlechrome.svg");
}
&.brave::before {
mask: url("/browsers/brave.svg");
}
&.edge::before {
mask: url("/browsers/microsoftedge.svg");
}

View File

@@ -1,18 +0,0 @@
<script lang="ts">
import {locales} from "../i18n/i18n-util"
import type {Locales} from "../i18n/i18n-types"
import {loadLocaleAsync} from "../i18n/i18n-util.async"
import {setLocale} from "../i18n/i18n-svelte"
async function applyLocale(locale: Locales) {
localStorage.setItem("locale", locale)
await loadLocaleAsync(locale)
setLocale(locale)
}
</script>
<ul>
{#each locales as locale}
<li><button on:click={() => applyLocale(locale)}>{locale}</button></li>
{/each}
</ul>

View File

@@ -9,9 +9,8 @@
import {canAutoConnect} from "$lib/serial/device"
import {browser} from "$app/environment"
import {userPreferences} from "$lib/preferences"
import Theme from "./Theme.svelte"
import Languages from "./Languages.svelte"
import LL from "../i18n/i18n-svelte"
import Profile from "./Profile.svelte"
const training = [
{slug: "cpm", title: "CPM - Characters Per Minute", icon: "music_note"},
@@ -45,7 +44,7 @@
<div class="actions">
{#if $canShare}
<a transition:fly={{x: -8}} class="icon" on:click={triggerShare}>share</a>
<button transition:fly={{x: -8}} class="icon" on:click={triggerShare}>share</button>
<div transition:slide class="separator" />
{/if}
{#await import("$lib/components/PwaStatus.svelte") then { default: PwaStatus }}
@@ -64,7 +63,6 @@
{/if}
</button>
{/if}
<button class="icon" use:popup={Languages}>translate</button>
<button
bind:this={connectButton}
title="Devices"
@@ -74,8 +72,7 @@
>
cable
</button>
<button title="Theme" use:popup={Theme} class="icon">format_paint</button>
<a href="/stats/" title="Statistics" class="icon account">person</a>
<button title={$LL.profile.TITLE()} use:popup={Profile} class="icon account">person</button>
</div>
</nav>

109
src/routes/Profile.svelte Normal file
View File

@@ -0,0 +1,109 @@
<script lang="ts">
import LL, {setLocale} from "../i18n/i18n-svelte"
import {theme} from "$lib/preferences"
import {tick} from "svelte"
import {detectLocale, locales} from "../i18n/i18n-util"
import {loadLocaleAsync} from "../i18n/i18n-util.async"
import type {Locales} from "../i18n/i18n-types"
let locale = (localStorage.getItem("locale") as Locales) || detectLocale()
$: (async () => {
localStorage.setItem("locale", locale)
await loadLocaleAsync(locale)
setLocale(locale)
})()
</script>
<section>
<h2>{$LL.profile.TITLE()}</h2>
<fieldset>
<legend>
<span class="icon">format_paint</span>
{$LL.profile.theme.TITLE()}
</legend>
<input title={$LL.profile.theme.COLOR_SCHEME()} type="color" bind:value={$theme.color} />
<button
title={$theme.mode === "light" ? $LL.profile.theme.LIGHT_MODE() : $LL.profile.theme.DARK_MODE()}
class="icon"
on:click={() => {
document.startViewTransition(async () => {
$theme.mode = $theme.mode === "light" ? "dark" : "light"
await tick()
})
}}
>
{#if $theme.mode === "light"}
light_mode
{:else if $theme.mode === "dark"}
dark_mode
{:else}
TODO
{/if}
</button>
</fieldset>
<fieldset>
<legend>
<span class="icon">translate</span>
{$LL.profile.LANGUAGE()}
</legend>
{#each locales as code}
<label>{code}<input bind:group={locale} type="radio" value={code} name="language" /></label>
{/each}
</fieldset>
</section>
<style lang="scss">
h2 {
grid-column: 1 / span 2;
}
section {
display: grid;
grid-template-columns: auto auto;
min-width: 300px;
}
fieldset {
display: flex;
justify-content: space-around;
border: 1px solid var(--md-sys-color-outline);
border-radius: 16px;
}
legend {
display: flex;
gap: 4px;
align-items: center;
justify-content: center;
}
button,
input[type="color"] {
cursor: pointer;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
inline-size: 24px;
block-size: 24px;
margin: 0;
padding: 0;
color: inherit;
background: transparent;
border: none;
border-radius: 50%;
&::-webkit-color-swatch-wrapper {
padding: 0;
}
&::-webkit-color-swatch {
border: none;
}
}
</style>

View File

@@ -1,61 +0,0 @@
<script>
import {theme} from "$lib/preferences"
import {tick} from "svelte"
</script>
<section>
<input type="color" bind:value={$theme.color} />
<button
class="icon"
on:click={() => {
document.startViewTransition(async () => {
$theme.mode = $theme.mode === "light" ? "dark" : "light"
await tick()
})
}}
>
{#if $theme.mode === "light"}
light_mode
{:else if $theme.mode === "dark"}
dark_mode
{:else}
TODO
{/if}
</button>
</section>
<style lang="scss">
section {
display: flex;
gap: 16px;
}
button,
input[type="color"] {
cursor: pointer;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
inline-size: 24px;
block-size: 24px;
margin: 0;
padding: 0;
color: inherit;
background: transparent;
border: none;
border-radius: 50%;
&::-webkit-color-swatch-wrapper {
padding: 0;
}
&::-webkit-color-swatch {
border: none;
}
}
</style>

View File

@@ -1,108 +0,0 @@
<script lang="ts">
import {getSharableUrl, parseCompressed, stringifyCompressed} from "$lib/serial/serialization"
import {chords, layout} from "$lib/serial/connection"
import type {Chord} from "$lib/serial/chord"
import type {CharaLayout} from "$lib/serialization/layout"
interface CharaBackup {
isCharaBackup: "v1.0"
chords: Chord[]
layout: CharaLayout
}
async function downloadBackup() {
const downloadUrl = URL.createObjectURL(
await stringifyCompressed<CharaBackup>({
isCharaBackup: "v1.0",
chords: $chords,
layout: $layout,
}),
)
const element = document.createElement("a")
element.setAttribute("download", "chords.chb")
element.href = downloadUrl
element.setAttribute("target", "_blank")
element.click()
URL.revokeObjectURL(downloadUrl)
}
async function restoreBackup(event: Event) {
const input = (event.target as HTMLInputElement).files![0]
if (!input) return
const backup = await parseCompressed<CharaBackup>(input)
if (backup.isCharaBackup !== "v1.0") throw new Error("Invalid Backup")
if (backup.chords) {
$chords = backup.chords
}
if (backup.layout) {
$layout = backup.layout
}
}
async function createShareUrl() {
console.log(await getSharableUrl("chords", $chords))
}
</script>
<section>
<h1>Backup & Restore</h1>
<p class="disclaimer">
<i
>We automatically backup your device settings. Backups remain on your computer and are never shared or
uploaded to our servers.</i
>
</p>
<div class="save">
<button class="primary" on:click={downloadBackup}><span class="icon">save</span> Download Backup</button>
<label class="button"
><input on:input={restoreBackup} type="file" /><span class="icon">settings_backup_restore</span> Restore</label
>
</div>
</section>
<style lang="scss">
.disclaimer {
max-width: 16cm;
font-size: 12px;
opacity: 0.7;
}
input[type="file"] {
display: none;
}
.save {
display: flex;
gap: 4px;
}
.button,
button {
cursor: pointer;
display: flex;
gap: 4px;
align-items: center;
justify-content: center;
padding-block: 8px;
padding-inline: 16px;
font-family: "Noto Sans Mono", monospace;
font-weight: 600;
color: var(--md-sys-color-on-background);
background: transparent;
border: none;
border-radius: 32px;
transition: all 250ms ease;
}
button.primary {
color: var(--md-sys-color-on-primary);
background: var(--md-sys-color-primary);
}
</style>

View File

@@ -1 +0,0 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Brave</title><path d="M15.68 0l2.096 2.38s1.84-.512 2.709.358c.868.87 1.584 1.638 1.584 1.638l-.562 1.381.715 2.047s-2.104 7.98-2.35 8.955c-.486 1.919-.818 2.66-2.198 3.633-1.38.972-3.884 2.66-4.293 2.916-.409.256-.92.692-1.38.692-.46 0-.97-.436-1.38-.692a185.796 185.796 0 01-4.293-2.916c-1.38-.973-1.712-1.714-2.197-3.633-.247-.975-2.351-8.955-2.351-8.955l.715-2.047-.562-1.381s.716-.768 1.585-1.638c.868-.87 2.708-.358 2.708-.358L8.321 0h7.36zm-3.679 14.936c-.14 0-1.038.317-1.758.69-.72.373-1.242.637-1.409.742-.167.104-.065.301.087.409.152.107 2.194 1.69 2.393 1.866.198.175.489.464.687.464.198 0 .49-.29.688-.464.198-.175 2.24-1.759 2.392-1.866.152-.108.254-.305.087-.41-.167-.104-.689-.368-1.41-.741-.72-.373-1.617-.69-1.757-.69zm0-11.278s-.409.001-1.022.206-1.278.46-1.584.46c-.307 0-2.581-.434-2.581-.434S4.119 7.152 4.119 7.849c0 .697.339.881.68 1.243l2.02 2.149c.192.203.59.511.356 1.066-.235.555-.58 1.26-.196 1.977.384.716 1.042 1.194 1.464 1.115.421-.08 1.412-.598 1.776-.834.364-.237 1.518-1.19 1.518-1.554 0-.365-1.193-1.02-1.413-1.168-.22-.15-1.226-.725-1.247-.95-.02-.227-.012-.293.284-.851.297-.559.831-1.304.742-1.8-.089-.495-.95-.753-1.565-.986-.615-.232-1.799-.671-1.947-.74-.148-.068-.11-.133.339-.175.448-.043 1.719-.212 2.292-.052.573.16 1.552.403 1.632.532.079.13.149.134.067.579-.081.445-.5 2.581-.541 2.96-.04.38-.12.63.288.724.409.094 1.097.256 1.333.256s.924-.162 1.333-.256c.408-.093.329-.344.288-.723-.04-.38-.46-2.516-.541-2.961-.082-.445-.012-.45.067-.579.08-.129 1.059-.372 1.632-.532.573-.16 1.845.009 2.292.052.449.042.487.107.339.175-.148.069-1.332.508-1.947.74-.615.233-1.476.49-1.565.986-.09.496.445 1.241.742 1.8.297.558.304.624.284.85-.02.226-1.026.802-1.247.95-.22.15-1.413.804-1.413 1.169 0 .364 1.154 1.317 1.518 1.554.364.236 1.355.755 1.776.834.422.079 1.08-.4 1.464-1.115.384-.716.039-1.422-.195-1.977-.235-.555.163-.863.355-1.066l2.02-2.149c.341-.362.68-.546.68-1.243 0-.697-2.695-3.96-2.695-3.96s-2.274.436-2.58.436c-.307 0-.972-.256-1.585-.461-.613-.205-1.022-.206-1.022-.206z"/></svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB