feat(KAN-70): integrate native workspace and update settings
CI/CD / preconditions (pull_request) Successful in 1s
Native JavaScript checks / Native launcher and release tests (pull_request) Successful in 1m2s
macOS checks / Native Rust tests and unsigned build (pull_request) Successful in 1m38s
CI/CD / test_and_check_app (pull_request) Successful in 6m47s
CI/CD / Publish immutable image (pull_request) Skipped
CI/CD / preconditions (pull_request) Successful in 1s
Native JavaScript checks / Native launcher and release tests (pull_request) Successful in 1m2s
macOS checks / Native Rust tests and unsigned build (pull_request) Successful in 1m38s
CI/CD / test_and_check_app (pull_request) Successful in 6m47s
CI/CD / Publish immutable image (pull_request) Skipped
This commit is contained in:
@@ -0,0 +1,313 @@
|
||||
<script setup lang="ts">
|
||||
import { Head, Link, usePage } from '@inertiajs/vue3';
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { useLocalization } from '@/composables/useLocalization';
|
||||
import { useNativeApp } from '@/composables/useNativeApp';
|
||||
import AppLayout from '@/layouts/AppLayout.vue';
|
||||
import SettingsLayout from '@/layouts/settings/Layout.vue';
|
||||
import { home } from '@/routes';
|
||||
import { edit } from '@/routes/native-app';
|
||||
import type { TranslationKey } from '@/types/localization';
|
||||
|
||||
const { t } = useLocalization();
|
||||
const { state, busy, error, act, poll } = useNativeApp();
|
||||
const page = usePage();
|
||||
const signedIn = computed(() => !!page.props.auth.user);
|
||||
const address = ref('');
|
||||
const confirmSwitch = ref(false);
|
||||
const confirmInstall = ref(false);
|
||||
const updates = computed(() => state.value?.updates);
|
||||
watch(
|
||||
() => updates.value?.version,
|
||||
() => {
|
||||
confirmInstall.value = false;
|
||||
},
|
||||
);
|
||||
const breadcrumbs = computed(() => [
|
||||
{ title: t('nativeApp.title'), href: edit() },
|
||||
]);
|
||||
watch(
|
||||
() => state.value?.workspace,
|
||||
(workspace) => {
|
||||
if (workspace) {
|
||||
address.value = workspace;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
watch(address, () => {
|
||||
confirmSwitch.value = false;
|
||||
});
|
||||
const knownErrors: Record<string, TranslationKey> = {
|
||||
invalidServer: 'nativeApp.invalidServer',
|
||||
storageError: 'nativeApp.storageError',
|
||||
networkError: 'nativeApp.networkError',
|
||||
metadataError: 'nativeApp.metadataError',
|
||||
signatureError: 'nativeApp.signatureError',
|
||||
verificationError: 'nativeApp.verificationError',
|
||||
installationError: 'nativeApp.installationError',
|
||||
preferencesError: 'nativeApp.preferencesError',
|
||||
updateBusy: 'nativeApp.updateBusy',
|
||||
};
|
||||
const errorMessage = computed(() => {
|
||||
const value = error.value ?? updates.value?.error;
|
||||
return value ? t(knownErrors[value] ?? 'nativeApp.bridgeError') : null;
|
||||
});
|
||||
const progress = computed(() =>
|
||||
updates.value?.total
|
||||
? Math.min(
|
||||
100,
|
||||
Math.round(
|
||||
(updates.value.downloaded / updates.value.total) * 100,
|
||||
),
|
||||
)
|
||||
: undefined,
|
||||
);
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
let active = true;
|
||||
async function refresh() {
|
||||
if (state.value) {
|
||||
await poll();
|
||||
}
|
||||
if (active) {
|
||||
timer = setTimeout(refresh, 1500);
|
||||
}
|
||||
}
|
||||
onMounted(() => {
|
||||
void refresh();
|
||||
});
|
||||
onUnmounted(() => {
|
||||
active = false;
|
||||
clearTimeout(timer);
|
||||
});
|
||||
async function switchWorkspace() {
|
||||
if (await act('setWorkspace', { server: address.value })) {
|
||||
confirmSwitch.value = false;
|
||||
}
|
||||
}
|
||||
async function installUpdate() {
|
||||
const version = updates.value?.version;
|
||||
if (version && (await act('installUpdate', { version }))) {
|
||||
confirmInstall.value = false;
|
||||
}
|
||||
}
|
||||
function setAutomatic(event: Event) {
|
||||
void act('setUpdatePreferences', {
|
||||
automatic: (event.target as HTMLInputElement).checked,
|
||||
intervalHours: updates.value?.preferences.intervalHours,
|
||||
});
|
||||
}
|
||||
function setIntervalHours(event: Event) {
|
||||
void act('setUpdatePreferences', {
|
||||
automatic: updates.value?.preferences.automatic,
|
||||
intervalHours: Number((event.target as HTMLSelectElement).value),
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="signedIn ? AppLayout : 'main'"
|
||||
:breadcrumbs="breadcrumbs"
|
||||
class="min-h-dvh"
|
||||
>
|
||||
<Head :title="t('nativeApp.title')" />
|
||||
<component
|
||||
:is="signedIn ? SettingsLayout : 'div'"
|
||||
:class="signedIn ? '' : 'mx-auto grid max-w-3xl gap-6 p-6'"
|
||||
>
|
||||
<Link
|
||||
v-if="!signedIn"
|
||||
:href="home()"
|
||||
class="text-primary underline"
|
||||
>{{ t('auth.home') }}</Link
|
||||
>
|
||||
<header class="grid gap-2">
|
||||
<h2 class="text-title">{{ t('nativeApp.title') }}</h2>
|
||||
<p class="text-muted-foreground">
|
||||
{{ t('nativeApp.description') }}
|
||||
</p>
|
||||
</header>
|
||||
<p v-if="!state" role="status">{{ t('nativeApp.unavailable') }}</p>
|
||||
<template v-else>
|
||||
<form class="grid gap-4" @submit.prevent="confirmSwitch = true">
|
||||
<h3 class="text-lg font-semibold">
|
||||
{{ t('nativeApp.workspace') }}
|
||||
</h3>
|
||||
<Label for="native-server">{{
|
||||
t('nativeApp.server')
|
||||
}}</Label>
|
||||
<Input
|
||||
id="native-server"
|
||||
v-model="address"
|
||||
type="url"
|
||||
required
|
||||
autocomplete="url"
|
||||
:disabled="busy"
|
||||
/>
|
||||
<p class="text-muted-foreground text-sm">
|
||||
{{ t('nativeApp.serverHelp') }}
|
||||
</p>
|
||||
<Button
|
||||
type="submit"
|
||||
:disabled="busy || address === state.workspace"
|
||||
class="justify-self-start"
|
||||
>{{ t('nativeApp.switch') }}</Button
|
||||
>
|
||||
<div
|
||||
v-if="confirmSwitch"
|
||||
class="grid gap-3 rounded-lg border p-4"
|
||||
role="group"
|
||||
:aria-label="t('nativeApp.switch')"
|
||||
>
|
||||
<p>{{ t('nativeApp.switchWarning') }}</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
:disabled="busy"
|
||||
@click="switchWorkspace"
|
||||
>{{ t('nativeApp.confirmSwitch') }}</Button
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
:disabled="busy"
|
||||
@click="confirmSwitch = false"
|
||||
>{{ t('nativeApp.cancel') }}</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<section
|
||||
v-if="updates"
|
||||
class="grid gap-4 border-t pt-6"
|
||||
aria-labelledby="native-updates"
|
||||
>
|
||||
<h3 id="native-updates" class="text-lg font-semibold">
|
||||
{{ t('nativeApp.updates') }}
|
||||
</h3>
|
||||
<p>
|
||||
{{
|
||||
t('nativeApp.version', {
|
||||
version: updates.currentVersion,
|
||||
build: updates.build,
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<p role="status" aria-live="polite">
|
||||
{{ t(`nativeApp.phase.${updates.phase}`) }}
|
||||
</p>
|
||||
<template
|
||||
v-if="
|
||||
['downloading', 'verifying', 'installing'].includes(
|
||||
updates.phase,
|
||||
)
|
||||
"
|
||||
>
|
||||
<progress
|
||||
:value="progress"
|
||||
max="100"
|
||||
class="w-full"
|
||||
:aria-label="t('nativeApp.progress')"
|
||||
/>
|
||||
<span v-if="progress !== undefined"
|
||||
>{{ progress }}%</span
|
||||
>
|
||||
</template>
|
||||
<Label class="flex items-center gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="updates.preferences.automatic"
|
||||
:disabled="busy"
|
||||
@change="setAutomatic"
|
||||
/>
|
||||
{{ t('nativeApp.automatic') }}
|
||||
</Label>
|
||||
<Label for="native-update-interval">{{
|
||||
t('nativeApp.interval')
|
||||
}}</Label>
|
||||
<select
|
||||
id="native-update-interval"
|
||||
:value="updates.preferences.intervalHours"
|
||||
:disabled="busy"
|
||||
class="bg-background rounded-md border p-2"
|
||||
@change="setIntervalHours"
|
||||
>
|
||||
<option
|
||||
v-for="hours in [1, 6, 24]"
|
||||
:key="hours"
|
||||
:value="hours"
|
||||
>
|
||||
{{ t('nativeApp.hours', { hours }) }}
|
||||
</option>
|
||||
</select>
|
||||
<p class="text-muted-foreground text-sm">
|
||||
{{ t('nativeApp.updateHelp') }}
|
||||
</p>
|
||||
<Button
|
||||
:disabled="busy"
|
||||
class="justify-self-start"
|
||||
@click="act('checkForUpdates')"
|
||||
>{{ t('nativeApp.check') }}</Button
|
||||
>
|
||||
<template v-if="updates.version">
|
||||
<p>
|
||||
{{
|
||||
t('nativeApp.availableVersion', {
|
||||
version: updates.version,
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<p v-if="updates.notes" class="whitespace-pre-wrap">
|
||||
{{ updates.notes }}
|
||||
</p>
|
||||
<Button
|
||||
:disabled="busy"
|
||||
class="justify-self-start"
|
||||
@click="confirmInstall = true"
|
||||
>{{ t('nativeApp.install') }}</Button
|
||||
>
|
||||
<div
|
||||
v-if="confirmInstall"
|
||||
class="grid gap-3 rounded-lg border p-4"
|
||||
role="group"
|
||||
:aria-label="t('nativeApp.install')"
|
||||
>
|
||||
<p>{{ t('nativeApp.installWarning') }}</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button
|
||||
:disabled="busy"
|
||||
@click="installUpdate"
|
||||
>{{ t('nativeApp.installRestart') }}</Button
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
:disabled="busy"
|
||||
@click="confirmInstall = false"
|
||||
>{{ t('nativeApp.cancel') }}</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
<p v-else class="text-muted-foreground text-sm">
|
||||
{{
|
||||
t('nativeApp.mobileVersion', { version: state.version })
|
||||
}}
|
||||
</p>
|
||||
<p v-if="errorMessage" class="text-destructive" role="alert">
|
||||
{{ errorMessage }}
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
class="justify-self-start"
|
||||
@click="act('openSettings')"
|
||||
>{{ t('nativeApp.recovery') }}</Button
|
||||
>
|
||||
</template>
|
||||
</component>
|
||||
</component>
|
||||
</template>
|
||||
Reference in New Issue
Block a user