Cleanup and refactoring (#60)
* Use production infra * Move theme out of subdirectory * Remove unused example test * Separate webnative module into init and account * Move state off of module global state * Move utils out of common directory * Organize imports consistently * Replace last remaining Toast with a Notification * Rename theme to themeStore Renaming keeps the theme store naming consistent with the other stores. * Replace uuid helper with WebCrypto randomUUID * Enfore minimum node version at >=16.9 * Refactor delegate-account and link-device routes Use component views for consistency with the other routes * Use FilesystemActivity component in Register * Remove console logs * Add extractSearchParam Generalizes extracting a search param from a URL. * Hide app name in header on mobile * Remove deviceStore We weren't using it in the app. Mobile styles are handled with tailwind utility classes. * Add social preview image * Remove navigate event from AreYouSure component The event was unused. * Replace convertUint8ToString with uint8arrays * Add initializeFilesystem Separates out app-specific resource creation from the required registration code * Allow minor webnative version upgrades * Update package-lock.json
This commit is contained in:
parent
05b70fd159
commit
102ecd9b3e
|
|
@ -10,7 +10,8 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"clipboard-copy": "^4.0.1",
|
"clipboard-copy": "^4.0.1",
|
||||||
"qrcode-svg": "^1.1.0",
|
"qrcode-svg": "^1.1.0",
|
||||||
"webnative": "0.34.1"
|
"uint8arrays": "^3.1.0",
|
||||||
|
"webnative": "^0.34.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-static": "1.0.0-next.36",
|
"@sveltejs/adapter-static": "1.0.0-next.36",
|
||||||
|
|
@ -37,6 +38,9 @@
|
||||||
"tslib": "^2.0.0",
|
"tslib": "^2.0.0",
|
||||||
"typescript": "^4.4.4",
|
"typescript": "^4.4.4",
|
||||||
"vite": "^3.0.0"
|
"vite": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
|
|
@ -47,6 +51,100 @@
|
||||||
"@babel/highlight": "^7.10.4"
|
"@babel/highlight": "^7.10.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/helper-validator-identifier": {
|
||||||
|
"version": "7.18.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz",
|
||||||
|
"integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/highlight": {
|
||||||
|
"version": "7.18.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz",
|
||||||
|
"integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/helper-validator-identifier": "^7.18.6",
|
||||||
|
"chalk": "^2.0.0",
|
||||||
|
"js-tokens": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/highlight/node_modules/ansi-styles": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"color-convert": "^1.9.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/highlight/node_modules/chalk": {
|
||||||
|
"version": "2.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
||||||
|
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^3.2.1",
|
||||||
|
"escape-string-regexp": "^1.0.5",
|
||||||
|
"supports-color": "^5.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/highlight/node_modules/color-convert": {
|
||||||
|
"version": "1.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||||
|
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"color-name": "1.1.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/highlight/node_modules/color-name": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/@babel/highlight/node_modules/escape-string-regexp": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/highlight/node_modules/has-flag": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/highlight/node_modules/supports-color": {
|
||||||
|
"version": "5.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||||
|
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"has-flag": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@cspotcode/source-map-support": {
|
"node_modules/@cspotcode/source-map-support": {
|
||||||
"version": "0.8.1",
|
"version": "0.8.1",
|
||||||
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
||||||
|
|
@ -3235,6 +3333,12 @@
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/js-tokens": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
"version": "3.14.1",
|
"version": "3.14.1",
|
||||||
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
|
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
|
||||||
|
|
@ -5712,6 +5816,81 @@
|
||||||
"@babel/highlight": "^7.10.4"
|
"@babel/highlight": "^7.10.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@babel/helper-validator-identifier": {
|
||||||
|
"version": "7.18.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz",
|
||||||
|
"integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"@babel/highlight": {
|
||||||
|
"version": "7.18.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz",
|
||||||
|
"integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@babel/helper-validator-identifier": "^7.18.6",
|
||||||
|
"chalk": "^2.0.0",
|
||||||
|
"js-tokens": "^4.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"color-convert": "^1.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"chalk": {
|
||||||
|
"version": "2.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
||||||
|
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"ansi-styles": "^3.2.1",
|
||||||
|
"escape-string-regexp": "^1.0.5",
|
||||||
|
"supports-color": "^5.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"color-convert": {
|
||||||
|
"version": "1.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||||
|
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"color-name": "1.1.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"color-name": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"escape-string-regexp": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"has-flag": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"supports-color": {
|
||||||
|
"version": "5.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||||
|
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"has-flag": "^3.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@cspotcode/source-map-support": {
|
"@cspotcode/source-map-support": {
|
||||||
"version": "0.8.1",
|
"version": "0.8.1",
|
||||||
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
||||||
|
|
@ -7996,6 +8175,12 @@
|
||||||
"integrity": "sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==",
|
"integrity": "sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"js-tokens": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"js-yaml": {
|
"js-yaml": {
|
||||||
"version": "3.14.1",
|
"version": "3.14.1",
|
||||||
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
|
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,10 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"clipboard-copy": "^4.0.1",
|
"clipboard-copy": "^4.0.1",
|
||||||
"qrcode-svg": "^1.1.0",
|
"qrcode-svg": "^1.1.0",
|
||||||
"webnative": "0.34.1"
|
"uint8arrays": "^3.1.0",
|
||||||
|
"webnative": "^0.34.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation'
|
import { goto } from '$app/navigation'
|
||||||
import { sessionStore, theme } from '../stores'
|
|
||||||
import { storeTheme, type Theme } from '$lib/theme/index'
|
|
||||||
|
|
||||||
import { appName } from '$lib/app-info'
|
import { appName } from '$lib/app-info'
|
||||||
|
import { sessionStore, themeStore } from '../stores'
|
||||||
|
import { storeTheme, type Theme } from '$lib/theme'
|
||||||
import Brand from '$components/icons/Brand.svelte'
|
import Brand from '$components/icons/Brand.svelte'
|
||||||
import Shield from '$components/icons/Shield.svelte'
|
|
||||||
import LightMode from '$components/icons/LightMode.svelte'
|
|
||||||
import DarkMode from '$components/icons/DarkMode.svelte'
|
import DarkMode from '$components/icons/DarkMode.svelte'
|
||||||
|
import LightMode from '$components/icons/LightMode.svelte'
|
||||||
|
import Shield from '$components/icons/Shield.svelte'
|
||||||
|
|
||||||
const setTheme = (newTheme: Theme) => {
|
const setTheme = (newTheme: Theme) => {
|
||||||
theme.set(newTheme)
|
themeStore.set(newTheme)
|
||||||
storeTheme(newTheme)
|
storeTheme(newTheme)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
<header class="navbar bg-base-100 pt-0">
|
<header class="navbar bg-base-100 pt-0">
|
||||||
<div class="flex-1 cursor-pointer hover:underline" on:click={() => goto('/')}>
|
<div class="flex-1 cursor-pointer hover:underline" on:click={() => goto('/')}>
|
||||||
<Brand />
|
<Brand />
|
||||||
<span class="text-xl ml-2">{appName}</span>
|
<span class="text-xl ml-2 hidden md:inline">{appName}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if !$sessionStore.loading && !$sessionStore.authed}
|
{#if !$sessionStore.loading && !$sessionStore.authed}
|
||||||
|
|
@ -40,7 +40,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<span class="ml-2">
|
<span class="ml-2">
|
||||||
{#if $theme === 'light'}
|
{#if $themeStore === 'light'}
|
||||||
<span on:click={() => setTheme('dark')}>
|
<span on:click={() => setTheme('dark')}>
|
||||||
<LightMode />
|
<LightMode />
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,8 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { goto } from '$app/navigation'
|
||||||
|
|
||||||
import { filesystemStore, sessionStore } from '../../../stores'
|
import { filesystemStore, sessionStore } from '../../../stores'
|
||||||
import { setBackupStatus } from '$lib/auth/backup'
|
import { setBackupStatus } from '$lib/auth/backup'
|
||||||
import type { BackupView } from '$lib/views'
|
|
||||||
import { goto } from '$app/navigation'
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
|
||||||
|
|
||||||
const navigate = (view: BackupView) => {
|
|
||||||
dispatch('navigate', { view })
|
|
||||||
}
|
|
||||||
|
|
||||||
const skipBackup = () => {
|
const skipBackup = () => {
|
||||||
setBackupStatus($filesystemStore, { created: false })
|
setBackupStatus($filesystemStore, { created: false })
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation'
|
||||||
|
import clipboardCopy from 'clipboard-copy'
|
||||||
|
|
||||||
|
import ClipboardIcon from '$components/icons/ClipboardIcon.svelte'
|
||||||
|
|
||||||
|
export let qrcode
|
||||||
|
export let connectionLink: string
|
||||||
|
export let backupCreated: boolean
|
||||||
|
|
||||||
|
const copyLink = async () => {
|
||||||
|
await clipboardCopy(connectionLink)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<input type="checkbox" id="backup-device-modal" checked class="modal-toggle" />
|
||||||
|
<div class="modal">
|
||||||
|
<div
|
||||||
|
class="modal-box w-80 relative text-center dark:border-slate-600 dark:border"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h3 class="pb-1 text-xl font-serif">Connect a backup device</h3>
|
||||||
|
{@html qrcode}
|
||||||
|
<p class="pt-1 mb-8">
|
||||||
|
Scan this code on the new device, or share the connection link.
|
||||||
|
</p>
|
||||||
|
<button class="btn btn-primary btn-outline" on:click={copyLink}>
|
||||||
|
<ClipboardIcon />
|
||||||
|
<span class="ml-2">Copy connection link</span>
|
||||||
|
</button>
|
||||||
|
{#if !backupCreated}
|
||||||
|
<button
|
||||||
|
class="btn btn-xs btn-link text-base text-error font-normal underline mt-4"
|
||||||
|
on:click={() => goto('/backup?view=are-you-sure')}
|
||||||
|
>
|
||||||
|
Skip for now
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from 'svelte'
|
||||||
|
|
||||||
|
export let pinInput: string
|
||||||
|
export let pinError: boolean
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
const cancelConnection = () => {
|
||||||
|
dispatch('cancel')
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkPin = () => {
|
||||||
|
dispatch('checkpin')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="delegate-account-modal"
|
||||||
|
checked
|
||||||
|
class="modal-toggle"
|
||||||
|
/>
|
||||||
|
<div class="modal">
|
||||||
|
<div
|
||||||
|
class="modal-box w-80 relative text-center dark:border-slate-600 dark:border"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h3 class="mb-7 text-xl font-serif">
|
||||||
|
A new device would like to connect to your account
|
||||||
|
</h3>
|
||||||
|
<div class="mb-5">
|
||||||
|
<input
|
||||||
|
id="pin"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered w-full max-w-xs mb-2 rounded-full h-16 font-mono text-3xl text-center tracking-[0.18em] font-light dark:border-slate-300"
|
||||||
|
bind:value={pinInput}
|
||||||
|
/>
|
||||||
|
<label for="pin" class="label">
|
||||||
|
{#if !pinError}
|
||||||
|
<span class="label-text-alt text-slate-500">
|
||||||
|
Enter the connection code to approve the connection.
|
||||||
|
</span>
|
||||||
|
{:else}
|
||||||
|
<span class="label-text-alt text-error">
|
||||||
|
Entered pin does not match a pin from a known device.
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-primary mb-5 w-full" on:click={checkPin}>
|
||||||
|
Approve the connection
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary btn-outline w-full"
|
||||||
|
on:click={cancelConnection}
|
||||||
|
>
|
||||||
|
Cancel Request
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from 'svelte'
|
||||||
|
|
||||||
|
export let pin: string
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
const cancelConnection = () => {
|
||||||
|
dispatch('cancel')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<input type="checkbox" id="my-modal-5" checked class="modal-toggle" />
|
||||||
|
<div class="modal">
|
||||||
|
<div
|
||||||
|
class="modal-box w-80 relative text-center dark:border-slate-600 dark:border"
|
||||||
|
>
|
||||||
|
<div class="grid grid-flow-row auto-rows-max gap-7">
|
||||||
|
<h3 class="text-xl font-serif">Connection Requested</h3>
|
||||||
|
<div class="grid grid-flow-row auto-rows-max gap-4 justify-items-center">
|
||||||
|
{#if pin}
|
||||||
|
<span
|
||||||
|
class="btn bg-blue-100 dark:bg-blue-900 hover:bg-blue-100 dark:hover:bg-blue-900 border-0 btn-lg rounded-full text-3xl tracking-[.18em] w-3/4 cursor-default font-mono font-light"
|
||||||
|
>
|
||||||
|
{pin}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
<span class="text-md">Enter this code on your connected device.</span>
|
||||||
|
<div
|
||||||
|
class="grid grid-flow-col auto-cols-max gap-4 justify-center items-center text-slate-500"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="rounded-lg border-t-2 border-l-2 border-slate-600 dark:border-slate-50 w-4 h-4 block animate-spin"
|
||||||
|
/>
|
||||||
|
Waiting for a response...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary btn-outline text-base font-normal mt-4"
|
||||||
|
on:click={cancelConnection}
|
||||||
|
>
|
||||||
|
Cancel Request
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -1,196 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import clipboardCopy from 'clipboard-copy'
|
|
||||||
import QRCode from 'qrcode-svg'
|
|
||||||
import { goto } from '$app/navigation'
|
|
||||||
import { onDestroy, onMount } from 'svelte'
|
|
||||||
|
|
||||||
import { addNotification } from '$lib/notifications'
|
|
||||||
import { createAccountLinkingProducer } from '$lib/auth/linking'
|
|
||||||
import { filesystemStore, sessionStore, theme } from '../../../stores'
|
|
||||||
import { getBackupStatus, setBackupStatus } from '$lib/auth/backup'
|
|
||||||
import ClipboardIcon from '$components/icons/ClipboardIcon.svelte'
|
|
||||||
|
|
||||||
let view: 'backup-device' | 'delegate-account' = 'backup-device'
|
|
||||||
|
|
||||||
let connectionLink = null
|
|
||||||
let qrcode = null
|
|
||||||
|
|
||||||
let fs = null
|
|
||||||
let backupCreated = true
|
|
||||||
|
|
||||||
let pin: number[]
|
|
||||||
let pinInput = ''
|
|
||||||
let pinError = false
|
|
||||||
let confirmPin = () => {}
|
|
||||||
let rejectPin = () => {}
|
|
||||||
|
|
||||||
let unsubscribeFilesystemStore = () => {}
|
|
||||||
let unsubscribeSessionStore = () => {}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
unsubscribeFilesystemStore = filesystemStore.subscribe(async val => {
|
|
||||||
fs = val
|
|
||||||
|
|
||||||
if (fs) {
|
|
||||||
const backupStatus = await getBackupStatus(fs)
|
|
||||||
backupCreated = backupStatus.created
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
unsubscribeSessionStore = sessionStore.subscribe(async val => {
|
|
||||||
const username = val.username
|
|
||||||
|
|
||||||
if (username) {
|
|
||||||
const origin = window.location.origin
|
|
||||||
|
|
||||||
connectionLink = `${origin}/link-device?username=${username}`
|
|
||||||
qrcode = new QRCode({
|
|
||||||
content: connectionLink,
|
|
||||||
color: $theme === 'light' ? '#334155' : '#E2E8F0',
|
|
||||||
background: '#ffffff00'
|
|
||||||
}).svg()
|
|
||||||
|
|
||||||
initAccountLinkingProducer(username)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const initAccountLinkingProducer = async (username: string) => {
|
|
||||||
const accountLinkingProducer = await createAccountLinkingProducer(username)
|
|
||||||
|
|
||||||
accountLinkingProducer.on('challenge', detail => {
|
|
||||||
pin = detail.pin
|
|
||||||
confirmPin = detail.confirmPin
|
|
||||||
rejectPin = detail.rejectPin
|
|
||||||
|
|
||||||
view = 'delegate-account'
|
|
||||||
})
|
|
||||||
|
|
||||||
accountLinkingProducer.on('link', async ({ approved }) => {
|
|
||||||
if (approved) {
|
|
||||||
sessionStore.update(session => ({
|
|
||||||
...session,
|
|
||||||
backupCreated: true
|
|
||||||
}))
|
|
||||||
|
|
||||||
if (fs) {
|
|
||||||
await setBackupStatus(fs, { created: true })
|
|
||||||
|
|
||||||
addNotification("You've connected a backup device!", 'success')
|
|
||||||
goto('/')
|
|
||||||
} else {
|
|
||||||
addNotification(
|
|
||||||
'Missing filesystem. Unable to create a backup device.',
|
|
||||||
'error'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const cancelConnection = () => {
|
|
||||||
rejectPin()
|
|
||||||
|
|
||||||
addNotification('The connection attempt was cancelled', 'info')
|
|
||||||
goto('/')
|
|
||||||
}
|
|
||||||
|
|
||||||
const copyLink = async () => {
|
|
||||||
await clipboardCopy(connectionLink)
|
|
||||||
}
|
|
||||||
|
|
||||||
const checkPin = () => {
|
|
||||||
if (pin.join('') === pinInput) {
|
|
||||||
confirmPin()
|
|
||||||
} else {
|
|
||||||
pinError = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onDestroy(() => {
|
|
||||||
unsubscribeFilesystemStore()
|
|
||||||
unsubscribeSessionStore()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if view === 'backup-device'}
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="backup-device-modal"
|
|
||||||
checked
|
|
||||||
class="modal-toggle"
|
|
||||||
/>
|
|
||||||
<div class="modal">
|
|
||||||
<div
|
|
||||||
class="modal-box w-80 relative text-center dark:border-slate-600 dark:border"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<h3 class="pb-1 text-xl font-serif">Connect a backup device</h3>
|
|
||||||
{@html qrcode}
|
|
||||||
<p class="pt-1 mb-8">
|
|
||||||
Scan this code on the new device, or share the connection link.
|
|
||||||
</p>
|
|
||||||
<button class="btn btn-primary btn-outline" on:click={copyLink}>
|
|
||||||
<ClipboardIcon />
|
|
||||||
<span class="ml-2">Copy connection link</span>
|
|
||||||
</button>
|
|
||||||
{#if !backupCreated}
|
|
||||||
<button
|
|
||||||
class="btn btn-xs btn-link text-base text-error font-normal underline mt-4"
|
|
||||||
on:click={() => goto('/backup?view=are-you-sure')}
|
|
||||||
>
|
|
||||||
Skip for now
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else if view === 'delegate-account'}
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="delegate-account-modal"
|
|
||||||
checked
|
|
||||||
class="modal-toggle"
|
|
||||||
/>
|
|
||||||
<div class="modal">
|
|
||||||
<div
|
|
||||||
class="modal-box w-80 relative text-center dark:border-slate-600 dark:border"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<h3 class="mb-7 text-xl font-serif">
|
|
||||||
A new device would like to connect to your account
|
|
||||||
</h3>
|
|
||||||
<div class="mb-5">
|
|
||||||
<input
|
|
||||||
id="pin"
|
|
||||||
type="text"
|
|
||||||
class="input input-bordered w-full max-w-xs mb-2 rounded-full h-16 font-mono text-3xl text-center tracking-[0.18em] font-light dark:border-slate-300"
|
|
||||||
bind:value={pinInput}
|
|
||||||
/>
|
|
||||||
<label for="pin" class="label">
|
|
||||||
{#if !pinError}
|
|
||||||
<span class="label-text-alt text-slate-500">
|
|
||||||
Enter the connection code to approve the connection.
|
|
||||||
</span>
|
|
||||||
{:else}
|
|
||||||
<span class="label-text-alt text-error">
|
|
||||||
Entered pin does not match a pin from a known device.
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button class="btn btn-primary mb-5 w-full" on:click={checkPin}>
|
|
||||||
Approve the connection
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn btn-primary btn-outline w-full"
|
|
||||||
on:click={cancelConnection}
|
|
||||||
>
|
|
||||||
Cancel Request
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import type { account } from 'webnative'
|
|
||||||
import { goto } from '$app/navigation'
|
|
||||||
import { page } from '$app/stores'
|
|
||||||
|
|
||||||
import { addNotification } from '$lib/notifications'
|
|
||||||
import { createAccountLinkingConsumer } from '$lib/auth/linking'
|
|
||||||
import { loadAccount } from '$lib/common/webnative'
|
|
||||||
|
|
||||||
let accountLinkingConsumer: account.AccountLinkingConsumer
|
|
||||||
|
|
||||||
let loadingFilesystem = false
|
|
||||||
|
|
||||||
let displayPin: string = ''
|
|
||||||
let url = $page.url
|
|
||||||
const username = url.searchParams.get('username')
|
|
||||||
|
|
||||||
// clear the params
|
|
||||||
url.searchParams.delete('username')
|
|
||||||
history.replaceState(null, document.title, url.toString())
|
|
||||||
|
|
||||||
const initAccountLinkingConsumer = async () => {
|
|
||||||
accountLinkingConsumer = await createAccountLinkingConsumer(username)
|
|
||||||
|
|
||||||
accountLinkingConsumer.on('challenge', ({ pin }) => {
|
|
||||||
displayPin = pin.join('')
|
|
||||||
})
|
|
||||||
|
|
||||||
accountLinkingConsumer.on('link', async ({ approved, username }) => {
|
|
||||||
if (approved) {
|
|
||||||
loadingFilesystem = true
|
|
||||||
|
|
||||||
await loadAccount(username)
|
|
||||||
|
|
||||||
addNotification("You're now connected!", 'success')
|
|
||||||
goto('/')
|
|
||||||
} else {
|
|
||||||
addNotification('The connection attempt was cancelled', 'info')
|
|
||||||
goto('/')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const cancelConnection = async () => {
|
|
||||||
addNotification('The connection attempt was cancelled', 'info')
|
|
||||||
|
|
||||||
await accountLinkingConsumer?.cancel()
|
|
||||||
goto('/')
|
|
||||||
}
|
|
||||||
|
|
||||||
initAccountLinkingConsumer()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<input type="checkbox" id="my-modal-5" checked class="modal-toggle" />
|
|
||||||
{#if loadingFilesystem}
|
|
||||||
<div class="modal">
|
|
||||||
<div
|
|
||||||
class="modal-box rounded-lg shadow-sm bg-slate-100 w-80 relative text-center dark:bg-slate-900 dark:border-slate-600 dark:border "
|
|
||||||
>
|
|
||||||
<p class="text-slate-500 dark:text-slate-50">
|
|
||||||
<span
|
|
||||||
class="rounded-lg border-t-2 border-l-2 border-slate-500 dark:border-slate-50 w-4 h-4 inline-block animate-spin mr-1"
|
|
||||||
/>
|
|
||||||
Loading file system...
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="modal">
|
|
||||||
<div
|
|
||||||
class="modal-box w-80 relative text-center dark:border-slate-600 dark:border"
|
|
||||||
>
|
|
||||||
<div class="grid grid-flow-row auto-rows-max gap-7">
|
|
||||||
<h3 class="text-xl font-serif">Connection Requested</h3>
|
|
||||||
<div
|
|
||||||
class="grid grid-flow-row auto-rows-max gap-4 justify-items-center"
|
|
||||||
>
|
|
||||||
{#if displayPin}
|
|
||||||
<span
|
|
||||||
class="btn bg-blue-100 dark:bg-blue-900 hover:bg-blue-100 dark:hover:bg-blue-900 border-0 btn-lg rounded-full text-3xl tracking-[.18em] w-3/4 cursor-default font-mono font-light"
|
|
||||||
>
|
|
||||||
{displayPin}
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
<span class="text-md">Enter this code on your connected device.</span>
|
|
||||||
<div
|
|
||||||
class="grid grid-flow-col auto-cols-max gap-4 justify-center items-center text-slate-500"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="rounded-lg border-t-2 border-l-2 border-slate-600 dark:border-slate-50 w-4 h-4 block animate-spin"
|
|
||||||
/>
|
|
||||||
Waiting for a response...
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
class="btn btn-primary btn-outline text-base font-normal mt-4"
|
|
||||||
on:click={cancelConnection}
|
|
||||||
>
|
|
||||||
Cancel Request
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
@ -4,9 +4,10 @@
|
||||||
isUsernameValid,
|
isUsernameValid,
|
||||||
isUsernameAvailable,
|
isUsernameAvailable,
|
||||||
register
|
register
|
||||||
} from '$lib/common/webnative'
|
} from '$lib/auth/account'
|
||||||
import CheckIcon from '$components/icons/CheckIcon.svelte'
|
import CheckIcon from '$components/icons/CheckIcon.svelte'
|
||||||
import XIcon from '$components/icons/XIcon.svelte'
|
import XIcon from '$components/icons/XIcon.svelte'
|
||||||
|
import FilesystemActivity from '$components/common/FilesystemActivity.svelte'
|
||||||
|
|
||||||
let username: string = ''
|
let username: string = ''
|
||||||
let usernameValid = true
|
let usernameValid = true
|
||||||
|
|
@ -14,6 +15,8 @@
|
||||||
let registrationSuccess = true
|
let registrationSuccess = true
|
||||||
let checkingUsername = false
|
let checkingUsername = false
|
||||||
|
|
||||||
|
let initializingFilesystem = false
|
||||||
|
|
||||||
const checkUsername = async (event: Event) => {
|
const checkUsername = async (event: Event) => {
|
||||||
const { value } = event.target as HTMLInputElement
|
const { value } = event.target as HTMLInputElement
|
||||||
|
|
||||||
|
|
@ -28,30 +31,17 @@
|
||||||
checkingUsername = false
|
checkingUsername = false
|
||||||
}
|
}
|
||||||
|
|
||||||
let authInProcess = false
|
|
||||||
const registerUser = async () => {
|
const registerUser = async () => {
|
||||||
authInProcess = true
|
initializingFilesystem = true
|
||||||
|
|
||||||
registrationSuccess = await register(username)
|
registrationSuccess = await register(username)
|
||||||
|
|
||||||
if (!registrationSuccess) authInProcess = false
|
if (!registrationSuccess) initializingFilesystem = false
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if authInProcess}
|
{#if initializingFilesystem}
|
||||||
<input type="checkbox" id="initializing" checked class="modal-toggle" />
|
<FilesystemActivity activity="Initializing" />
|
||||||
<div class="modal">
|
|
||||||
<div
|
|
||||||
class="modal-box rounded-lg shadow-sm bg-slate-100 w-80 relative text-center dark:bg-slate-900 dark:border-slate-600 dark:border "
|
|
||||||
>
|
|
||||||
<p class="text-slate-500 dark:text-slate-50">
|
|
||||||
<span
|
|
||||||
class="rounded-lg border-t-2 border-l-2 border-slate-500 dark:border-slate-50 w-4 h-4 inline-block animate-spin mr-1"
|
|
||||||
/>
|
|
||||||
Initializing file system...
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else}
|
{:else}
|
||||||
<input type="checkbox" id="register-modal" checked class="modal-toggle" />
|
<input type="checkbox" id="register-modal" checked class="modal-toggle" />
|
||||||
<div class="modal">
|
<div class="modal">
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { appName } from '$lib/app-info'
|
||||||
import { sessionStore } from '../../../stores'
|
import { sessionStore } from '../../../stores'
|
||||||
import WelcomeCheckIcon from '$components/icons/WelcomeCheckIcon.svelte'
|
import WelcomeCheckIcon from '$components/icons/WelcomeCheckIcon.svelte'
|
||||||
import { appName } from '$lib/app-info'
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<input type="checkbox" id="link-device-modal" checked class="modal-toggle" />
|
<input type="checkbox" id="link-device-modal" checked class="modal-toggle" />
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<script lang="ts">
|
||||||
|
export let activity: 'Initializing' | 'Loading'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<input type="checkbox" id="my-modal-5" checked class="modal-toggle" />
|
||||||
|
<div class="modal">
|
||||||
|
<div
|
||||||
|
class="modal-box rounded-lg shadow-sm bg-slate-100 w-80 relative text-center dark:bg-slate-900 dark:border-slate-600 dark:border "
|
||||||
|
>
|
||||||
|
<p class="text-slate-500 dark:text-slate-50">
|
||||||
|
<span
|
||||||
|
class="rounded-lg border-t-2 border-l-2 border-slate-500 dark:border-slate-50 w-4 h-4 inline-block animate-spin mr-1"
|
||||||
|
/>
|
||||||
|
{activity} file system...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onDestroy } from 'svelte'
|
import { onDestroy } from 'svelte'
|
||||||
|
|
||||||
import { galleryStore } from '../../../stores'
|
import { galleryStore } from '../../../stores'
|
||||||
import { AREAS, getImagesFromWNFS } from '$lib/gallery'
|
import { AREAS, getImagesFromWNFS } from '$lib/gallery'
|
||||||
import type { Image } from '$lib/gallery'
|
import type { Image } from '$lib/gallery'
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
|
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
|
||||||
|
|
||||||
|
import { deleteImageFromWNFS } from '$lib/gallery'
|
||||||
import { galleryStore } from '../../../stores'
|
import { galleryStore } from '../../../stores'
|
||||||
import type { Gallery, Image } from '$lib/gallery'
|
import type { Gallery, Image } from '$lib/gallery'
|
||||||
import { deleteImageFromWNFS } from '$lib/gallery'
|
|
||||||
|
|
||||||
export let image: Image
|
export let image: Image
|
||||||
export let isModalOpen: boolean = false
|
export let isModalOpen: boolean = false
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getImagesFromWNFS, uploadImageToWNFS } from '$lib/gallery'
|
|
||||||
import { addNotification } from '$lib/notifications'
|
import { addNotification } from '$lib/notifications'
|
||||||
|
import { getImagesFromWNFS, uploadImageToWNFS } from '$lib/gallery'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detect when a user drags a file in or out of the dropzone to change the styles
|
* Detect when a user drags a file in or out of the dropzone to change the styles
|
||||||
|
|
@ -19,8 +19,6 @@
|
||||||
|
|
||||||
const files = Array.from(event.dataTransfer.items)
|
const files = Array.from(event.dataTransfer.items)
|
||||||
|
|
||||||
console.log(`${files.length} file${files.length > 1 ? 's' : ''} dropped`)
|
|
||||||
|
|
||||||
// Iterate over the dropped files and upload them to WNFS
|
// Iterate over the dropped files and upload them to WNFS
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
files.map(async (item, index) => {
|
files.map(async (item, index) => {
|
||||||
|
|
@ -30,9 +28,7 @@
|
||||||
// If the dropped files aren't images, we don't want them!
|
// If the dropped files aren't images, we don't want them!
|
||||||
if (!file.type.match('image/*')) {
|
if (!file.type.match('image/*')) {
|
||||||
addNotification('Please upload images only', 'error')
|
addNotification('Please upload images only', 'error')
|
||||||
console.error('Please upload images only')
|
|
||||||
} else {
|
} else {
|
||||||
console.log(`file[${index + 1}].name = ${file.name}`)
|
|
||||||
await uploadImageToWNFS(file)
|
await uploadImageToWNFS(file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { fade, fly } from 'svelte/transition'
|
import { fade, fly } from 'svelte/transition'
|
||||||
|
|
||||||
|
import { themeStore } from '../../stores'
|
||||||
|
import type { Notification } from '$lib/notifications'
|
||||||
import CheckThinIcon from '$components/icons/CheckThinIcon.svelte'
|
import CheckThinIcon from '$components/icons/CheckThinIcon.svelte'
|
||||||
import XThinIcon from '$components/icons/XThinIcon.svelte'
|
import XThinIcon from '$components/icons/XThinIcon.svelte'
|
||||||
import { theme as themeStore } from '../../stores'
|
|
||||||
import type { Notification } from '$lib/notifications'
|
|
||||||
|
|
||||||
export let notification: Notification
|
export let notification: Notification
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { flip } from 'svelte/animate'
|
import { flip } from 'svelte/animate'
|
||||||
import Notification from '$components/notifications/Notification.svelte'
|
|
||||||
import { notificationStore } from '../../stores'
|
import { notificationStore } from '../../stores'
|
||||||
|
import Notification from '$components/notifications/Notification.svelte'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $notificationStore.length}
|
{#if $notificationStore.length}
|
||||||
|
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { createEventDispatcher } from 'svelte'
|
|
||||||
import { quintOut } from 'svelte/easing'
|
|
||||||
import { fade } from 'svelte/transition'
|
|
||||||
|
|
||||||
import { theme } from '../../stores'
|
|
||||||
import CheckThinIcon from '$components/icons/CheckThinIcon.svelte'
|
|
||||||
import XThinIcon from '$components/icons/XThinIcon.svelte'
|
|
||||||
|
|
||||||
export let kind: 'success' | 'error'
|
|
||||||
export let message: string
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
|
||||||
|
|
||||||
const clearNotification = () => {
|
|
||||||
dispatch('clear')
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="toast"
|
|
||||||
on:click={clearNotification}
|
|
||||||
out:fade={{ duration: 800, easing: quintOut }}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="alert {kind === 'success' ? 'alert-success' : 'alert-error'} text-sm"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
{#if kind === 'success'}
|
|
||||||
<CheckThinIcon color={$theme === 'light' ? '#b8ffd3' : '#002e12'} />
|
|
||||||
{:else if kind === 'error'}
|
|
||||||
<XThinIcon color={$theme === 'light' ? '#ffd6d7' : '#fec3c3'} />
|
|
||||||
{/if}
|
|
||||||
<span class="pl-1">{message}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
export const appName = 'Awesome Webnative App'
|
export const appName = 'Awesome Webnative App'
|
||||||
export const appDescription = 'This is another awesome Webnative app.'
|
export const appDescription = 'This is another awesome Webnative app.'
|
||||||
export const appURL = 'https://webnative.netlify.app'
|
export const appURL = 'https://webnative.netlify.app'
|
||||||
|
export const appImageURL = `${appURL}/preview.png`
|
||||||
|
|
|
||||||
|
|
@ -1,75 +1,10 @@
|
||||||
import * as webnative from 'webnative'
|
import * as webnative from 'webnative'
|
||||||
import { setup } from 'webnative'
|
import type FileSystem from 'webnative/fs/index'
|
||||||
|
|
||||||
import { asyncDebounce } from '$lib/common/utils'
|
import { asyncDebounce } from '$lib/utils'
|
||||||
import { filesystemStore, sessionStore } from '../../stores'
|
import { filesystemStore, sessionStore } from '../../stores'
|
||||||
|
import { getBackupStatus } from '$lib/auth/backup'
|
||||||
import { AREAS, GALLERY_DIRS } from '$lib/gallery'
|
import { AREAS, GALLERY_DIRS } from '$lib/gallery'
|
||||||
import { getBackupStatus, type BackupStatus } from '$lib/auth/backup'
|
|
||||||
|
|
||||||
// runfission.net = staging
|
|
||||||
setup.endpoints({
|
|
||||||
api: 'https://runfission.net',
|
|
||||||
lobby: 'https://auth.runfission.net',
|
|
||||||
user: 'fissionuser.net'
|
|
||||||
})
|
|
||||||
|
|
||||||
let state: webnative.AppState
|
|
||||||
|
|
||||||
// TODO: Add a flag or script to turn debugging on/off
|
|
||||||
setup.debug({ enabled: false })
|
|
||||||
|
|
||||||
export const initialize = async (): Promise<void> => {
|
|
||||||
try {
|
|
||||||
let backupStatus: BackupStatus = null
|
|
||||||
|
|
||||||
state = await webnative.app({ useWnfs: true })
|
|
||||||
|
|
||||||
switch (state.scenario) {
|
|
||||||
case webnative.AppScenario.NotAuthed:
|
|
||||||
sessionStore.set({
|
|
||||||
username: '',
|
|
||||||
authed: false,
|
|
||||||
loading: false,
|
|
||||||
backupCreated: null
|
|
||||||
})
|
|
||||||
break
|
|
||||||
|
|
||||||
case webnative.AppScenario.Authed:
|
|
||||||
backupStatus = await getBackupStatus(state.fs)
|
|
||||||
|
|
||||||
sessionStore.set({
|
|
||||||
username: state.username,
|
|
||||||
authed: state.authenticated,
|
|
||||||
loading: false,
|
|
||||||
backupCreated: backupStatus.created
|
|
||||||
})
|
|
||||||
|
|
||||||
filesystemStore.set(state.fs)
|
|
||||||
break
|
|
||||||
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
switch (error) {
|
|
||||||
case webnative.InitialisationError.InsecureContext:
|
|
||||||
sessionStore.update(session => ({
|
|
||||||
...session,
|
|
||||||
loading: false,
|
|
||||||
error: 'Insecure Context'
|
|
||||||
}))
|
|
||||||
break
|
|
||||||
|
|
||||||
case webnative.InitialisationError.UnsupportedBrowser:
|
|
||||||
sessionStore.update(session => ({
|
|
||||||
...session,
|
|
||||||
loading: false,
|
|
||||||
error: 'Unsupported Browser'
|
|
||||||
}))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isUsernameValid = async (username: string): Promise<boolean> => {
|
export const isUsernameValid = async (username: string): Promise<boolean> => {
|
||||||
return webnative.account.isUsernameValid(username)
|
return webnative.account.isUsernameValid(username)
|
||||||
|
|
@ -94,9 +29,8 @@ export const register = async (username: string): Promise<boolean> => {
|
||||||
const fs = await webnative.bootstrapRootFileSystem()
|
const fs = await webnative.bootstrapRootFileSystem()
|
||||||
filesystemStore.set(fs)
|
filesystemStore.set(fs)
|
||||||
|
|
||||||
// Create public and private directories for the gallery
|
// TODO Remove if only public and private directories are needed
|
||||||
await fs.mkdir(webnative.path.directory(...GALLERY_DIRS[AREAS.PUBLIC]))
|
await initializeFilesystem(fs)
|
||||||
await fs.mkdir(webnative.path.directory(...GALLERY_DIRS[AREAS.PRIVATE]))
|
|
||||||
|
|
||||||
sessionStore.update(session => ({
|
sessionStore.update(session => ({
|
||||||
...session,
|
...session,
|
||||||
|
|
@ -107,6 +41,16 @@ export const register = async (username: string): Promise<boolean> => {
|
||||||
return success
|
return success
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create additional directories and files needed by the app
|
||||||
|
*
|
||||||
|
* @param fs FileSystem
|
||||||
|
*/
|
||||||
|
const initializeFilesystem = async (fs: FileSystem): Promise<void> => {
|
||||||
|
await fs.mkdir(webnative.path.directory(...GALLERY_DIRS[AREAS.PUBLIC]))
|
||||||
|
await fs.mkdir(webnative.path.directory(...GALLERY_DIRS[AREAS.PRIVATE]))
|
||||||
|
}
|
||||||
|
|
||||||
export const loadAccount = async (username: string): Promise<void> => {
|
export const loadAccount = async (username: string): Promise<void> => {
|
||||||
await checkDataRoot(username)
|
await checkDataRoot(username)
|
||||||
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
import test from 'ava'
|
|
||||||
|
|
||||||
test('my passing test', t => {
|
|
||||||
t.pass()
|
|
||||||
})
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
export function asyncDebounce<A extends unknown[], R>(
|
|
||||||
fn: (...args: A) => Promise<R>,
|
|
||||||
wait: number
|
|
||||||
): (...args: A) => Promise<R> {
|
|
||||||
let lastTimeoutId: ReturnType<typeof setTimeout> | undefined = undefined
|
|
||||||
|
|
||||||
return (...args: A): Promise<R> => {
|
|
||||||
clearTimeout(lastTimeoutId)
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const currentTimeoutId = setTimeout(async () => {
|
|
||||||
try {
|
|
||||||
if (currentTimeoutId === lastTimeoutId) {
|
|
||||||
const result = await fn(...args)
|
|
||||||
resolve(result)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
reject(err)
|
|
||||||
}
|
|
||||||
}, wait)
|
|
||||||
|
|
||||||
lastTimeoutId = currentTimeoutId
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Util to convert a Uint8Array to a string
|
|
||||||
* @param u8array
|
|
||||||
* @returns string
|
|
||||||
*/
|
|
||||||
export const convertUint8ToString: (u8array: Uint8Array) => string = u8array => {
|
|
||||||
const CHUNK_SZ = 0x8000
|
|
||||||
const c = []
|
|
||||||
for (let i = 0; i < u8array.length; i += CHUNK_SZ) {
|
|
||||||
c.push(String.fromCharCode.apply(null, u8array.subarray(i, i + CHUNK_SZ)))
|
|
||||||
}
|
|
||||||
return c.join('')
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a new uuid
|
|
||||||
* @returns uuid
|
|
||||||
*/
|
|
||||||
export const uuid: () => string = () =>
|
|
||||||
// @ts-expect-error disable number[] + number warning
|
|
||||||
([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c: number) =>
|
|
||||||
(
|
|
||||||
c ^
|
|
||||||
(crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))
|
|
||||||
).toString(16)
|
|
||||||
)
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
export type Device = {
|
|
||||||
isMobile: boolean
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
|
import * as uint8arrays from 'uint8arrays'
|
||||||
import { get as getStore } from 'svelte/store'
|
import { get as getStore } from 'svelte/store'
|
||||||
|
|
||||||
import * as wn from 'webnative'
|
import * as wn from 'webnative'
|
||||||
import { filesystemStore, galleryStore } from '../stores'
|
|
||||||
import { convertUint8ToString } from '$lib/common/utils'
|
|
||||||
import { addNotification } from '$lib/notifications'
|
import { addNotification } from '$lib/notifications'
|
||||||
|
import { filesystemStore, galleryStore } from '../stores'
|
||||||
|
|
||||||
export enum AREAS {
|
export enum AREAS {
|
||||||
PUBLIC = 'Public',
|
PUBLIC = 'Public',
|
||||||
|
|
@ -60,9 +61,7 @@ export const getImagesFromWNFS: () => Promise<void> = async () => {
|
||||||
const cid = isPrivate ? file.header.content.toString() : file.cid.toString()
|
const cid = isPrivate ? file.header.content.toString() : file.cid.toString()
|
||||||
|
|
||||||
// Create a base64 string to use as the image `src`
|
// Create a base64 string to use as the image `src`
|
||||||
const src = `data:image/jpeg;base64, ${btoa(
|
const src = `data:image/jpeg;base64, ${uint8arrays.toString(file.content, 'base64')}`
|
||||||
convertUint8ToString(file.content as Uint8Array)
|
|
||||||
)}`
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cid,
|
cid,
|
||||||
|
|
@ -90,7 +89,6 @@ export const getImagesFromWNFS: () => Promise<void> = async () => {
|
||||||
loading: false,
|
loading: false,
|
||||||
}))
|
}))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
|
||||||
galleryStore.update(store => ({
|
galleryStore.update(store => ({
|
||||||
...store,
|
...store,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
|
@ -132,12 +130,10 @@ export const uploadImageToWNFS: (
|
||||||
// Announce the changes to the server
|
// Announce the changes to the server
|
||||||
await fs.publish()
|
await fs.publish()
|
||||||
|
|
||||||
console.log(`${image.name} image has been published`)
|
|
||||||
addNotification(`${image.name} image has been published`, 'success')
|
addNotification(`${image.name} image has been published`, 'success')
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
addNotification(error.message, 'error')
|
addNotification(error.message, 'error')
|
||||||
console.log(error)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -161,7 +157,6 @@ export const deleteImageFromWNFS: (name: string) => Promise<void> = async (name)
|
||||||
// Announce the changes to the server
|
// Announce the changes to the server
|
||||||
await fs.publish()
|
await fs.publish()
|
||||||
|
|
||||||
console.log(`${name} image has been deleted`)
|
|
||||||
addNotification(`${name} image has been deleted`, 'success')
|
addNotification(`${name} image has been deleted`, 'success')
|
||||||
|
|
||||||
// Refetch images and update galleryStore
|
// Refetch images and update galleryStore
|
||||||
|
|
@ -171,7 +166,6 @@ export const deleteImageFromWNFS: (name: string) => Promise<void> = async (name)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
addNotification(error.message, 'error')
|
addNotification(error.message, 'error')
|
||||||
console.error(error)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
import * as webnative from 'webnative'
|
||||||
|
import { setup } from 'webnative'
|
||||||
|
|
||||||
|
import { filesystemStore, sessionStore } from '../stores'
|
||||||
|
import { getBackupStatus, type BackupStatus } from '$lib/auth/backup'
|
||||||
|
|
||||||
|
// TODO: Add a flag or script to turn debugging on/off
|
||||||
|
setup.debug({ enabled: false })
|
||||||
|
|
||||||
|
export const initialize = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
let backupStatus: BackupStatus = null
|
||||||
|
|
||||||
|
const state: webnative.AppState = await webnative.app({ useWnfs: true })
|
||||||
|
|
||||||
|
switch (state.scenario) {
|
||||||
|
case webnative.AppScenario.NotAuthed:
|
||||||
|
sessionStore.set({
|
||||||
|
username: '',
|
||||||
|
authed: false,
|
||||||
|
loading: false,
|
||||||
|
backupCreated: null
|
||||||
|
})
|
||||||
|
break
|
||||||
|
|
||||||
|
case webnative.AppScenario.Authed:
|
||||||
|
backupStatus = await getBackupStatus(state.fs)
|
||||||
|
|
||||||
|
sessionStore.set({
|
||||||
|
username: state.username,
|
||||||
|
authed: state.authenticated,
|
||||||
|
loading: false,
|
||||||
|
backupCreated: backupStatus.created
|
||||||
|
})
|
||||||
|
|
||||||
|
filesystemStore.set(state.fs)
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
switch (error) {
|
||||||
|
case webnative.InitialisationError.InsecureContext:
|
||||||
|
sessionStore.update(session => ({
|
||||||
|
...session,
|
||||||
|
loading: false,
|
||||||
|
error: 'Insecure Context'
|
||||||
|
}))
|
||||||
|
break
|
||||||
|
|
||||||
|
case webnative.InitialisationError.UnsupportedBrowser:
|
||||||
|
sessionStore.update(session => ({
|
||||||
|
...session,
|
||||||
|
loading: false,
|
||||||
|
error: 'Unsupported Browser'
|
||||||
|
}))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { notificationStore } from '../stores'
|
import { notificationStore } from '../stores'
|
||||||
import { uuid } from '$lib/common/utils'
|
|
||||||
|
|
||||||
export type Notification = {
|
export type Notification = {
|
||||||
id?: string
|
id?: string
|
||||||
|
|
@ -22,7 +21,7 @@ export const addNotification: (
|
||||||
timeout?: number
|
timeout?: number
|
||||||
) => void = (msg, type = 'info', timeout = 5000) => {
|
) => void = (msg, type = 'info', timeout = 5000) => {
|
||||||
// uuid for each notification
|
// uuid for each notification
|
||||||
const id = uuid()
|
const id = crypto.randomUUID()
|
||||||
|
|
||||||
// adding new notifications to the bottom of the list so they stack from bottom to top
|
// adding new notifications to the bottom of the list so they stack from bottom to top
|
||||||
notificationStore.update(rest => [
|
notificationStore.update(rest => [
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
export function asyncDebounce<A extends unknown[], R>(
|
||||||
|
fn: (...args: A) => Promise<R>,
|
||||||
|
wait: number
|
||||||
|
): (...args: A) => Promise<R> {
|
||||||
|
let lastTimeoutId: ReturnType<typeof setTimeout> | undefined = undefined
|
||||||
|
|
||||||
|
return (...args: A): Promise<R> => {
|
||||||
|
clearTimeout(lastTimeoutId)
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const currentTimeoutId = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
if (currentTimeoutId === lastTimeoutId) {
|
||||||
|
const result = await fn(...args)
|
||||||
|
resolve(result)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
}, wait)
|
||||||
|
|
||||||
|
lastTimeoutId = currentTimeoutId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const extractSearchParam = (url: URL, param: string): string | null => {
|
||||||
|
const val = url.searchParams.get(param)
|
||||||
|
|
||||||
|
// clear the param from the URL
|
||||||
|
url.searchParams.delete(param)
|
||||||
|
history.replaceState(null, document.title, url.toString())
|
||||||
|
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
export type BackupView = 'backup' | 'are-you-sure'
|
export type BackupView = 'backup' | 'are-you-sure'
|
||||||
|
|
||||||
export type ConnectView = 'connect' | 'open-connected-device'
|
export type ConnectView = 'connect' | 'open-connected-device'
|
||||||
|
|
||||||
|
export type DelegateAccountView = 'connect-backup-device' | 'delegate-account'
|
||||||
|
|
||||||
|
export type LinkDeviceView = 'link-device' | 'load-filesystem'
|
||||||
|
|
@ -1,42 +1,24 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte'
|
|
||||||
|
|
||||||
import '../global.css'
|
import '../global.css'
|
||||||
import { appDescription, appName, appURL } from '$lib/app-info'
|
import { addNotification } from '$lib/notifications'
|
||||||
import { initialize } from '$lib/common/webnative'
|
import { appDescription, appImageURL, appName, appURL } from '$lib/app-info'
|
||||||
import { deviceStore, sessionStore, theme } from '../stores'
|
import { sessionStore, themeStore } from '../stores'
|
||||||
import { errorToMessage, type Session } from '$lib/session'
|
import { errorToMessage } from '$lib/session'
|
||||||
import Toast from '$components/notifications/Toast.svelte'
|
import { initialize } from '$lib/init'
|
||||||
import Notifications from '$components/notifications/Notifications.svelte'
|
|
||||||
import Header from '$components/Header.svelte'
|
import Header from '$components/Header.svelte'
|
||||||
|
import Notifications from '$components/notifications/Notifications.svelte'
|
||||||
|
|
||||||
let session: Session = null
|
sessionStore.subscribe(session => {
|
||||||
|
if (session.error) {
|
||||||
sessionStore.subscribe(val => {
|
const message = errorToMessage(session.error)
|
||||||
session = val
|
addNotification(message, 'error')
|
||||||
})
|
|
||||||
|
|
||||||
onMount(() => { setDevice() })
|
|
||||||
|
|
||||||
const setDevice = () => {
|
|
||||||
if (window.innerWidth <= 768) {
|
|
||||||
deviceStore.set({ isMobile: true })
|
|
||||||
} else {
|
|
||||||
deviceStore.set({ isMobile: false })
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
await initialize()
|
await initialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearNotification = () => {
|
|
||||||
sessionStore.update(session => ({
|
|
||||||
...session,
|
|
||||||
error: null
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
init()
|
init()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -51,14 +33,14 @@
|
||||||
<meta property="og:description" content={appDescription} />
|
<meta property="og:description" content={appDescription} />
|
||||||
<meta property="og:url" content={appURL} />
|
<meta property="og:url" content={appURL} />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:image" content="TODO" />
|
<meta property="og:image" content={appImageURL} />
|
||||||
<meta property="og:image:alt" content="WebNative Template" />
|
<meta property="og:image:alt" content="WebNative Template" />
|
||||||
<meta property="og:image:width" content="1200" />
|
<meta property="og:image:width" content="1250" />
|
||||||
<meta property="og:image:height" content="630" />
|
<meta property="og:image:height" content="358" />
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
<meta name="twitter:title" content={appName} />
|
<meta name="twitter:title" content={appName} />
|
||||||
<meta name="twitter:description" content={appDescription} />
|
<meta name="twitter:description" content={appDescription} />
|
||||||
<meta name="twitter:image" content="TODO" />
|
<meta name="twitter:image" content={appImageURL} />
|
||||||
<meta name="twitter:image:alt" content={appName} />
|
<meta name="twitter:image:alt" content={appName} />
|
||||||
|
|
||||||
<!-- See https://evilmartians.com/chronicles/how-to-favicon-in-2021-six-files-that-fit-most-needs for description. -->
|
<!-- See https://evilmartians.com/chronicles/how-to-favicon-in-2021-six-files-that-fit-most-needs for description. -->
|
||||||
|
|
@ -68,19 +50,8 @@
|
||||||
<link rel="manifest" href="/manifest.webmanifest" />
|
<link rel="manifest" href="/manifest.webmanifest" />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<svelte:window on:resize={setDevice} />
|
<div data-theme={$themeStore} class="min-h-screen">
|
||||||
|
|
||||||
<div data-theme={$theme} class="min-h-screen">
|
|
||||||
<Header />
|
<Header />
|
||||||
<Notifications />
|
<Notifications />
|
||||||
|
|
||||||
{#if session.error}
|
|
||||||
<Toast
|
|
||||||
kind="error"
|
|
||||||
message={errorToMessage(session.error)}
|
|
||||||
on:clear={clearNotification}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,12 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores'
|
import { page } from '$app/stores'
|
||||||
|
|
||||||
|
import { extractSearchParam } from '$lib/utils'
|
||||||
import type { BackupView } from '$lib/views'
|
import type { BackupView } from '$lib/views'
|
||||||
import Backup from '$components/auth/backup/Backup.svelte'
|
|
||||||
import AreYouSure from '$components/auth/backup/AreYouSure.svelte'
|
import AreYouSure from '$components/auth/backup/AreYouSure.svelte'
|
||||||
|
import Backup from '$components/auth/backup/Backup.svelte'
|
||||||
|
|
||||||
let url = $page.url
|
let view = extractSearchParam($page.url, 'view') ?? 'backup'
|
||||||
let view = url.searchParams.get('view') ?? 'backup'
|
|
||||||
|
|
||||||
// clear the params
|
|
||||||
url.searchParams.delete('view')
|
|
||||||
history.replaceState(null, document.title, url.toString())
|
|
||||||
|
|
||||||
const navigate = (event: CustomEvent<{ view: BackupView }>) => {
|
const navigate = (event: CustomEvent<{ view: BackupView }>) => {
|
||||||
view = event.detail.view
|
view = event.detail.view
|
||||||
|
|
@ -19,5 +16,5 @@
|
||||||
{#if view === 'backup'}
|
{#if view === 'backup'}
|
||||||
<Backup on:navigate={navigate} />
|
<Backup on:navigate={navigate} />
|
||||||
{:else if view === 'are-you-sure'}
|
{:else if view === 'are-you-sure'}
|
||||||
<AreYouSure on:navigate={navigate} />
|
<AreYouSure />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import type { ConnectView } from '$lib/views'
|
||||||
import Connect from '$components/auth/connect/Connect.svelte'
|
import Connect from '$components/auth/connect/Connect.svelte'
|
||||||
import OpenConnectedDevice from '$components/auth/connect/OpenConnectedDevice.svelte'
|
import OpenConnectedDevice from '$components/auth/connect/OpenConnectedDevice.svelte'
|
||||||
import type { ConnectView } from '$lib/views'
|
|
||||||
|
|
||||||
let view: ConnectView = 'connect'
|
let view: ConnectView = 'connect'
|
||||||
|
|
||||||
const navigate = (event: CustomEvent<{ view: ConnectView }>) => {
|
const navigate = (event: CustomEvent<{ view: ConnectView }>) => {
|
||||||
console.log(event.detail.view)
|
|
||||||
view = event.detail.view
|
view = event.detail.view
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,123 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import DelegateAccount from '$components/auth/link/DelegateAccount.svelte'
|
import { goto } from '$app/navigation'
|
||||||
|
import { onDestroy, onMount } from 'svelte'
|
||||||
|
import QRCode from 'qrcode-svg'
|
||||||
|
|
||||||
|
import { addNotification } from '$lib/notifications'
|
||||||
|
import { createAccountLinkingProducer } from '$lib/auth/linking'
|
||||||
|
import { filesystemStore, sessionStore, themeStore } from '../stores'
|
||||||
|
import { getBackupStatus, setBackupStatus } from '$lib/auth/backup'
|
||||||
|
import ConnectBackupDevice from '$components/auth/delegate-account/ConnectBackupDevice.svelte'
|
||||||
|
import DelegateAccount from '$components/auth/delegate-account/DelegateAccount.svelte'
|
||||||
|
|
||||||
|
import type { DelegateAccountView } from '$lib/views'
|
||||||
|
|
||||||
|
let view: DelegateAccountView = 'connect-backup-device'
|
||||||
|
|
||||||
|
let connectionLink = null
|
||||||
|
let qrcode = null
|
||||||
|
|
||||||
|
let fs = null
|
||||||
|
let backupCreated = true
|
||||||
|
|
||||||
|
let pin: number[]
|
||||||
|
let pinInput = ''
|
||||||
|
let pinError = false
|
||||||
|
let confirmPin = () => {}
|
||||||
|
let rejectPin = () => {}
|
||||||
|
|
||||||
|
let unsubscribeFilesystemStore = () => {}
|
||||||
|
let unsubscribeSessionStore = () => {}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
unsubscribeFilesystemStore = filesystemStore.subscribe(async val => {
|
||||||
|
fs = val
|
||||||
|
|
||||||
|
if (fs) {
|
||||||
|
const backupStatus = await getBackupStatus(fs)
|
||||||
|
backupCreated = backupStatus.created
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
unsubscribeSessionStore = sessionStore.subscribe(async val => {
|
||||||
|
const username = val.username
|
||||||
|
|
||||||
|
if (username) {
|
||||||
|
const origin = window.location.origin
|
||||||
|
|
||||||
|
connectionLink = `${origin}/link-device?username=${username}`
|
||||||
|
qrcode = new QRCode({
|
||||||
|
content: connectionLink,
|
||||||
|
color: $themeStore === 'light' ? '#334155' : '#E2E8F0',
|
||||||
|
background: '#ffffff00'
|
||||||
|
}).svg()
|
||||||
|
|
||||||
|
initAccountLinkingProducer(username)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const initAccountLinkingProducer = async (username: string) => {
|
||||||
|
const accountLinkingProducer = await createAccountLinkingProducer(username)
|
||||||
|
|
||||||
|
accountLinkingProducer.on('challenge', detail => {
|
||||||
|
pin = detail.pin
|
||||||
|
confirmPin = detail.confirmPin
|
||||||
|
rejectPin = detail.rejectPin
|
||||||
|
|
||||||
|
view = 'delegate-account'
|
||||||
|
})
|
||||||
|
|
||||||
|
accountLinkingProducer.on('link', async ({ approved }) => {
|
||||||
|
if (approved) {
|
||||||
|
sessionStore.update(session => ({
|
||||||
|
...session,
|
||||||
|
backupCreated: true
|
||||||
|
}))
|
||||||
|
|
||||||
|
if (fs) {
|
||||||
|
await setBackupStatus(fs, { created: true })
|
||||||
|
|
||||||
|
addNotification("You've connected a backup device!", 'success')
|
||||||
|
goto('/')
|
||||||
|
} else {
|
||||||
|
addNotification(
|
||||||
|
'Missing filesystem. Unable to create a backup device.',
|
||||||
|
'error'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkPin = () => {
|
||||||
|
if (pin.join('') === pinInput) {
|
||||||
|
confirmPin()
|
||||||
|
} else {
|
||||||
|
pinError = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelConnection = () => {
|
||||||
|
rejectPin()
|
||||||
|
|
||||||
|
addNotification('The connection attempt was cancelled', 'info')
|
||||||
|
goto('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
unsubscribeFilesystemStore()
|
||||||
|
unsubscribeSessionStore()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DelegateAccount />
|
{#if view === 'connect-backup-device'}
|
||||||
|
<ConnectBackupDevice {qrcode} {connectionLink} {backupCreated} />
|
||||||
|
{:else if view === 'delegate-account'}
|
||||||
|
<DelegateAccount
|
||||||
|
bind:pinInput
|
||||||
|
bind:pinError
|
||||||
|
on:cancel={cancelConnection}
|
||||||
|
on:checkpin={checkPin}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onDestroy } from 'svelte'
|
|
||||||
import { goto } from '$app/navigation'
|
import { goto } from '$app/navigation'
|
||||||
import { galleryStore, sessionStore, theme as themeStore } from '../stores'
|
import { onDestroy } from 'svelte'
|
||||||
|
|
||||||
|
import { galleryStore, sessionStore, themeStore } from '../stores'
|
||||||
|
import { AREAS } from '$lib/gallery'
|
||||||
import Dropzone from '$components/gallery/upload/Dropzone.svelte'
|
import Dropzone from '$components/gallery/upload/Dropzone.svelte'
|
||||||
import ImageGallery from '$components/gallery/imageGallery/ImageGallery.svelte'
|
import ImageGallery from '$components/gallery/imageGallery/ImageGallery.svelte'
|
||||||
import { AREAS } from '$lib/gallery'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tab between the public/private areas and load associated images
|
* Tab between the public/private areas and load associated images
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
import { goto } from '$app/navigation'
|
import { goto } from '$app/navigation'
|
||||||
import { onDestroy } from 'svelte'
|
import { onDestroy } from 'svelte'
|
||||||
|
|
||||||
import { sessionStore } from '../stores'
|
|
||||||
import { appName } from '$lib/app-info'
|
import { appName } from '$lib/app-info'
|
||||||
|
import { sessionStore } from '../stores'
|
||||||
import type { Session } from '$lib/session'
|
import type { Session } from '$lib/session'
|
||||||
import Shield from '$components/icons/Shield.svelte'
|
import Shield from '$components/icons/Shield.svelte'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,60 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import LinkDevice from '$components/auth/link/LinkDevice.svelte'
|
import type { account } from 'webnative'
|
||||||
|
import { goto } from '$app/navigation'
|
||||||
|
import { page } from '$app/stores'
|
||||||
|
|
||||||
|
import { addNotification } from '$lib/notifications'
|
||||||
|
import { createAccountLinkingConsumer } from '$lib/auth/linking'
|
||||||
|
import { loadAccount } from '$lib/auth/account'
|
||||||
|
import type { LinkDeviceView } from '$lib/views'
|
||||||
|
import FilesystemActivity from '$components/common/FilesystemActivity.svelte'
|
||||||
|
import LinkDevice from '$components/auth/link-device/LinkDevice.svelte'
|
||||||
|
|
||||||
|
import { extractSearchParam } from '$lib/utils'
|
||||||
|
|
||||||
|
let view: LinkDeviceView = 'link-device'
|
||||||
|
|
||||||
|
let accountLinkingConsumer: account.AccountLinkingConsumer
|
||||||
|
let displayPin: string = ''
|
||||||
|
|
||||||
|
const username = extractSearchParam($page.url, 'username')
|
||||||
|
|
||||||
|
const initAccountLinkingConsumer = async () => {
|
||||||
|
accountLinkingConsumer = await createAccountLinkingConsumer(username)
|
||||||
|
|
||||||
|
accountLinkingConsumer.on('challenge', ({ pin }) => {
|
||||||
|
displayPin = pin.join('')
|
||||||
|
})
|
||||||
|
|
||||||
|
accountLinkingConsumer.on('link', async ({ approved, username }) => {
|
||||||
|
if (approved) {
|
||||||
|
view = 'load-filesystem'
|
||||||
|
|
||||||
|
await loadAccount(username)
|
||||||
|
|
||||||
|
addNotification("You're now connected!", 'success')
|
||||||
|
goto('/')
|
||||||
|
} else {
|
||||||
|
addNotification('The connection attempt was cancelled', 'info')
|
||||||
|
goto('/')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelConnection = async () => {
|
||||||
|
addNotification('The connection attempt was cancelled', 'info')
|
||||||
|
|
||||||
|
await accountLinkingConsumer?.cancel()
|
||||||
|
goto('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
initAccountLinkingConsumer()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<LinkDevice />
|
<input type="checkbox" id="my-modal-5" checked class="modal-toggle" />
|
||||||
|
|
||||||
|
{#if view === 'link-device'}
|
||||||
|
<LinkDevice pin={displayPin} on:cancel={cancelConnection} />
|
||||||
|
{:else if view === 'load-filesystem'}
|
||||||
|
<FilesystemActivity activity="Loading" />
|
||||||
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,13 @@ import type { Writable } from 'svelte/store'
|
||||||
import type FileSystem from 'webnative/fs/index'
|
import type FileSystem from 'webnative/fs/index'
|
||||||
|
|
||||||
import { loadTheme } from '$lib/theme'
|
import { loadTheme } from '$lib/theme'
|
||||||
import type { Device } from '$lib/device'
|
|
||||||
import { AREAS } from '$lib/gallery'
|
import { AREAS } from '$lib/gallery'
|
||||||
import type { Gallery } from '$lib/gallery'
|
import type { Gallery } from '$lib/gallery'
|
||||||
import type { Notification } from '$lib/notifications'
|
import type { Notification } from '$lib/notifications'
|
||||||
import type { Session } from '$lib/session'
|
import type { Session } from '$lib/session'
|
||||||
import type { Theme } from '$lib/theme'
|
import type { Theme } from '$lib/theme'
|
||||||
|
|
||||||
export const theme: Writable<Theme> = writable(loadTheme())
|
export const themeStore: Writable<Theme> = writable(loadTheme())
|
||||||
|
|
||||||
export const sessionStore: Writable<Session> = writable({
|
export const sessionStore: Writable<Session> = writable({
|
||||||
username: null,
|
username: null,
|
||||||
|
|
@ -21,8 +20,6 @@ export const sessionStore: Writable<Session> = writable({
|
||||||
|
|
||||||
export const filesystemStore: Writable<FileSystem | null> = writable(null)
|
export const filesystemStore: Writable<FileSystem | null> = writable(null)
|
||||||
|
|
||||||
export const deviceStore: Writable<Device> = writable({ isMobile: true })
|
|
||||||
|
|
||||||
export const galleryStore: Writable<Gallery> = writable({
|
export const galleryStore: Writable<Gallery> = writable({
|
||||||
loading: true,
|
loading: true,
|
||||||
publicImages: [],
|
publicImages: [],
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
|
|
@ -49,4 +49,9 @@ module.exports = {
|
||||||
rtl: false,
|
rtl: false,
|
||||||
darkTheme: "dark",
|
darkTheme: "dark",
|
||||||
},
|
},
|
||||||
|
purge: {
|
||||||
|
options: {
|
||||||
|
safelist: ['alert-success', 'alert-error', 'alert-info', 'alert-warning']
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
Loading…
Reference in New Issue