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
314 lines
12 KiB
Vue
314 lines
12 KiB
Vue
<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>
|