mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-01-22 09:52:50 +00:00
feat: de-clutter navbar
fix: backup option not working refactor: persistent writable stores [deploy]
This commit is contained in:
@@ -9,6 +9,16 @@ const de = {
|
|||||||
DOWNLOAD: "Kopie Speichern",
|
DOWNLOAD: "Kopie Speichern",
|
||||||
RESTORE: "Wiederherstellen",
|
RESTORE: "Wiederherstellen",
|
||||||
},
|
},
|
||||||
|
profile: {
|
||||||
|
TITLE: "Profil",
|
||||||
|
LANGUAGE: "Sprache",
|
||||||
|
theme: {
|
||||||
|
TITLE: "Darstellung",
|
||||||
|
COLOR_SCHEME: "Farbschema",
|
||||||
|
DARK_MODE: "Dunkel",
|
||||||
|
LIGHT_MODE: "Hell",
|
||||||
|
},
|
||||||
|
},
|
||||||
deviceManager: {
|
deviceManager: {
|
||||||
TITLE: "Gerät",
|
TITLE: "Gerät",
|
||||||
AUTO_CONNECT: "Automatisch Verbinden",
|
AUTO_CONNECT: "Automatisch Verbinden",
|
||||||
|
|||||||
@@ -8,6 +8,16 @@ const en = {
|
|||||||
DOWNLOAD: "Download Backup",
|
DOWNLOAD: "Download Backup",
|
||||||
RESTORE: "Restore",
|
RESTORE: "Restore",
|
||||||
},
|
},
|
||||||
|
profile: {
|
||||||
|
TITLE: "Profile",
|
||||||
|
LANGUAGE: "Language",
|
||||||
|
theme: {
|
||||||
|
TITLE: "Theme",
|
||||||
|
COLOR_SCHEME: "Color scheme",
|
||||||
|
DARK_MODE: "Dark",
|
||||||
|
LIGHT_MODE: "Light",
|
||||||
|
},
|
||||||
|
},
|
||||||
deviceManager: {
|
deviceManager: {
|
||||||
TITLE: "Device",
|
TITLE: "Device",
|
||||||
AUTO_CONNECT: "Auto-connect",
|
AUTO_CONNECT: "Auto-connect",
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import {writable} from "svelte/store"
|
|
||||||
import type {Action} from "svelte/action"
|
import type {Action} from "svelte/action"
|
||||||
|
import {persistentWritable} from "$lib/storage"
|
||||||
|
|
||||||
export interface UserPreferences {
|
export interface UserPreferences {
|
||||||
backup: boolean
|
backup: boolean
|
||||||
autoConnect: boolean
|
autoConnect: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const theme = writable({
|
export const theme = persistentWritable("user-theme", {
|
||||||
color: "#6D81C7",
|
color: "#6D81C7",
|
||||||
mode: "dark" as "light" | "dark" | "auto",
|
mode: "dark" as "light" | "dark" | "auto",
|
||||||
})
|
})
|
||||||
|
|
||||||
export const userPreferences = writable<UserPreferences>({
|
export const userPreferences = persistentWritable<UserPreferences>("user-preferences", {
|
||||||
backup: false,
|
backup: false,
|
||||||
autoConnect: true,
|
autoConnect: true,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import {CharaDevice} from "$lib/serial/device"
|
|||||||
import type {Chord} from "$lib/serial/chord"
|
import type {Chord} from "$lib/serial/chord"
|
||||||
import type {Writable} from "svelte/store"
|
import type {Writable} from "svelte/store"
|
||||||
import type {CharaLayout} from "$lib/serialization/layout"
|
import type {CharaLayout} from "$lib/serialization/layout"
|
||||||
|
import {persistentWritable} from "$lib/storage"
|
||||||
|
import {userPreferences} from "$lib/preferences"
|
||||||
|
|
||||||
export const serialPort = writable<CharaDevice>()
|
export const serialPort = writable<CharaDevice>()
|
||||||
|
|
||||||
@@ -13,9 +15,13 @@ export interface SerialLogEntry {
|
|||||||
|
|
||||||
export const serialLog = writable<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({})
|
export const settings = writable({})
|
||||||
|
|
||||||
|
|||||||
@@ -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
17
src/lib/storage.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,9 +25,9 @@ $padding: 16px;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tippy-box[data-theme~="search-completion"] {
|
.tippy-box[data-theme~="search-completion"] {
|
||||||
|
overflow: hidden;
|
||||||
filter: none;
|
filter: none;
|
||||||
border-radius: 0 0 16px 16px;
|
border-radius: 0 0 16px 16px;
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.tippy-content {
|
.tippy-content {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
import {pwaInfo} from "virtual:pwa-info"
|
import {pwaInfo} from "virtual:pwa-info"
|
||||||
import type {LayoutServerData} from "./$types"
|
import type {LayoutServerData} from "./$types"
|
||||||
import type {RegisterSWOptions} from "vite-plugin-pwa/types"
|
import type {RegisterSWOptions} from "vite-plugin-pwa/types"
|
||||||
import {initLocalStorage} from "$lib/serial/storage"
|
|
||||||
import {browser} from "$app/environment"
|
import {browser} from "$app/environment"
|
||||||
import BrowserWarning from "./BrowserWarning.svelte"
|
import BrowserWarning from "./BrowserWarning.svelte"
|
||||||
import "tippy.js/animations/shift-away.css"
|
import "tippy.js/animations/shift-away.css"
|
||||||
@@ -47,7 +46,6 @@
|
|||||||
const dark = it.mode === "dark" // window.matchMedia("(prefers-color-scheme: dark)").matches
|
const dark = it.mode === "dark" // window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||||
applyTheme(theme, {target: document.body, dark})
|
applyTheme(theme, {target: document.body, dark})
|
||||||
})
|
})
|
||||||
initLocalStorage()
|
|
||||||
|
|
||||||
if (pwaInfo) {
|
if (pwaInfo) {
|
||||||
const {registerSW} = await import("virtual:pwa-register")
|
const {registerSW} = await import("virtual:pwa-register")
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
URL.revokeObjectURL(downloadUrl)
|
URL.revokeObjectURL(downloadUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function restoreBackup(event: InputEvent) {
|
async function restoreBackup(event: Event) {
|
||||||
const input = (event.target as HTMLInputElement).files![0]
|
const input = (event.target as HTMLInputElement).files![0]
|
||||||
if (!input) return
|
if (!input) return
|
||||||
const backup = await parseCompressed<Backup>(input)
|
const backup = await parseCompressed<Backup>(input)
|
||||||
@@ -119,10 +119,10 @@
|
|||||||
border-radius: 32px;
|
border-radius: 32px;
|
||||||
|
|
||||||
transition: all 250ms ease;
|
transition: all 250ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
&.primary {
|
button.primary {
|
||||||
color: var(--md-sys-color-on-primary);
|
color: var(--md-sys-color-on-primary);
|
||||||
background: var(--md-sys-color-primary);
|
background: var(--md-sys-color-primary);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -82,10 +82,6 @@
|
|||||||
mask: url("/browsers/googlechrome.svg");
|
mask: url("/browsers/googlechrome.svg");
|
||||||
}
|
}
|
||||||
|
|
||||||
&.brave::before {
|
|
||||||
mask: url("/browsers/brave.svg");
|
|
||||||
}
|
|
||||||
|
|
||||||
&.edge::before {
|
&.edge::before {
|
||||||
mask: url("/browsers/microsoftedge.svg");
|
mask: url("/browsers/microsoftedge.svg");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -9,9 +9,8 @@
|
|||||||
import {canAutoConnect} from "$lib/serial/device"
|
import {canAutoConnect} from "$lib/serial/device"
|
||||||
import {browser} from "$app/environment"
|
import {browser} from "$app/environment"
|
||||||
import {userPreferences} from "$lib/preferences"
|
import {userPreferences} from "$lib/preferences"
|
||||||
import Theme from "./Theme.svelte"
|
|
||||||
import Languages from "./Languages.svelte"
|
|
||||||
import LL from "../i18n/i18n-svelte"
|
import LL from "../i18n/i18n-svelte"
|
||||||
|
import Profile from "./Profile.svelte"
|
||||||
|
|
||||||
const training = [
|
const training = [
|
||||||
{slug: "cpm", title: "CPM - Characters Per Minute", icon: "music_note"},
|
{slug: "cpm", title: "CPM - Characters Per Minute", icon: "music_note"},
|
||||||
@@ -45,7 +44,7 @@
|
|||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
{#if $canShare}
|
{#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" />
|
<div transition:slide class="separator" />
|
||||||
{/if}
|
{/if}
|
||||||
{#await import("$lib/components/PwaStatus.svelte") then { default: PwaStatus }}
|
{#await import("$lib/components/PwaStatus.svelte") then { default: PwaStatus }}
|
||||||
@@ -64,7 +63,6 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
<button class="icon" use:popup={Languages}>translate</button>
|
|
||||||
<button
|
<button
|
||||||
bind:this={connectButton}
|
bind:this={connectButton}
|
||||||
title="Devices"
|
title="Devices"
|
||||||
@@ -74,8 +72,7 @@
|
|||||||
>
|
>
|
||||||
cable
|
cable
|
||||||
</button>
|
</button>
|
||||||
<button title="Theme" use:popup={Theme} class="icon">format_paint</button>
|
<button title={$LL.profile.TITLE()} use:popup={Profile} class="icon account">person</button>
|
||||||
<a href="/stats/" title="Statistics" class="icon account">person</a>
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|||||||
109
src/routes/Profile.svelte
Normal file
109
src/routes/Profile.svelte
Normal 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>
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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 |
Reference in New Issue
Block a user