Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Wire up Tauri listeners in onMount of root layout #322

Merged
merged 4 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 7 additions & 42 deletions unime/src/lib/stores.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,7 @@
import { goto } from '$app/navigation';
import { setLocale } from '$i18n/i18n-svelte';
import type { Locales } from '$i18n/i18n-types';
import { writable } from 'svelte/store';

// TODO: run some copy task instead of importing across root to make the frontend independent
import type { AppState } from '@bindings/AppState';
import { listen } from '@tauri-apps/api/event';
import { error as err, info } from '@tauri-apps/plugin-log';

interface StateChangedEvent {
event: string;
payload: AppState;
id: number;
}

interface ErrorEvent {
event: string;
payload: string;
id: number;
}

interface OnboardingState {
name?: string;
Expand Down Expand Up @@ -57,36 +40,18 @@ const empty_state: AppState = {
};

/**
* A store that listens for updates to the application state emitted by the Rust backend.
* If the frontend intends to change the state, it must dispatch an action to the backend.
* This store contains the frontend state.
* It may be altered only by the `state-changed` Tauri event listener.
* The frontend must dispatch an action to the backend to change state.
*/
// TODO: make read-only
export const state = writable<AppState>(empty_state, (set) => {
listen('state-changed', (event: StateChangedEvent) => {
const state = event.payload;

set(state);
setLocale(state.profile_settings.locale as Locales);

if (state.current_user_prompt?.type === 'redirect') {
const redirect_target = state.current_user_prompt.target;
info(`Redirecting to: "/${redirect_target}"`);
goto(`/${redirect_target}`);
}
});
// TODO: unsubscribe from listener!
});
export const state = writable<AppState>(empty_state);

/**
* A store that listens for errors emitted by the Rust backend.
* This store contains errors to be displayed by an error toast.
* It may be altered only by the `error` Tauri event listener.
*/
export const error = writable<string | undefined>(undefined, (set) => {
listen('error', (event: ErrorEvent) => {
err(`Error: ${event.payload}`);
set(event.payload);
});
// TODO: unsubscribe from listener!
});
export const error = writable<string | undefined>(undefined);

/**
* This store is only used by the frontend for storing state during onboarding.
Expand Down
102 changes: 71 additions & 31 deletions unime/src/routes/+layout.svelte
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
<script lang="ts">
import { onMount, type SvelteComponent } from 'svelte';
import { onDestroy, onMount, type SvelteComponent } from 'svelte';

import { goto } from '$app/navigation';
import { PUBLIC_DEV_MODE_MENU_EXPANDED } from '$env/static/public';
import LL from '$i18n/i18n-svelte';
import LL, { setLocale } from '$i18n/i18n-svelte';
import { loadAllLocales } from '$i18n/i18n-util.sync';
import type { SvelteHTMLElements } from 'svelte/elements';
import { fly } from 'svelte/transition';

import { attachConsole } from '@tauri-apps/plugin-log';
import type { AppState } from '@bindings/AppState';
import type { ProfileSteps } from '@bindings/dev/ProfileSteps';
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
import { attachConsole, error, info } from '@tauri-apps/plugin-log';

import { Switch } from '$lib/components';
import { dispatch } from '$lib/dispatcher';
import {
ArrowLeftRegularIcon,
Expand All @@ -17,25 +22,67 @@
CaretUpBoldIcon,
TrashRegularIcon,
} from '$lib/icons';
import { error, state } from '$lib/stores';
import { state as appState, error as errorState } from '$lib/stores';

import ErrorToast from './ErrorToast.svelte';
import { determineTheme } from './utils';

import '../app.css';

import type { SvelteHTMLElements } from 'svelte/elements';
let detachConsole: UnlistenFn;
let unlistenError: UnlistenFn;
let unlistenStateChanged: UnlistenFn;

import type { ProfileSteps } from '@bindings/dev/ProfileSteps';
onMount(async () => {
detachConsole = await attachConsole();

import { Switch } from '$lib/components';
loadAllLocales(); //TODO: performance: only load locale on user request

import ErrorToast from './ErrorToast.svelte';
import { determineTheme } from './utils';
unlistenError = await listen('error', (event) => {
error(`Error: ${event.payload}`);
errorState.set(event.payload as string);
});

unlistenStateChanged = await listen('state-changed', (event) => {
// Set frontend state to state received from backend.
appState.set(event.payload as AppState);

// Update locale based on the frontend state.
setLocale($appState.profile_settings.locale);

let redirectPath: string | undefined;

if ($appState.current_user_prompt) {
// Generic redirect.
if ($appState.current_user_prompt.type === 'redirect') {
redirectPath = `/${$appState.current_user_prompt.target}`;
}
// Prompt redirect.
else {
redirectPath = `/prompt/${$appState.current_user_prompt.type}`;
}
}

if (redirectPath) {
info(`Redirecting to: ${redirectPath}.`);
try {
goto(redirectPath);
} catch (e) {
error(`Failed to redirect to ${redirectPath}: ${e}`);
}
}
});

onMount(async () => {
await attachConsole();
loadAllLocales(); //TODO: performance: only load locale on user request
dispatch({ type: '[App] Get state' });
});

onDestroy(() => {
// Destroy in reverse order.
unlistenStateChanged();
unlistenError();
detachConsole();
});

let expandedDevMenu = PUBLIC_DEV_MODE_MENU_EXPANDED === 'true';
let showDebugMessages = false;
let showDragonProfileSteps = false;
Expand All @@ -44,27 +91,20 @@
const systemColorScheme = window.matchMedia('(prefers-color-scheme: dark)');

systemColorScheme.addEventListener('change', (e) => {
if ($state?.profile_settings.profile?.theme) {
determineTheme(e.matches, $state.profile_settings.profile.theme);
if ($appState?.profile_settings.profile?.theme) {
determineTheme(e.matches, $appState.profile_settings.profile.theme);
} else {
determineTheme(systemColorScheme.matches, 'system');
}
});

$: {
// TODO: needs to be called at least once to trigger subscribers --> better way to do this?
if ($state?.profile_settings.profile?.theme) {
determineTheme(systemColorScheme.matches, $state.profile_settings.profile.theme);
if ($appState?.profile_settings.profile?.theme) {
determineTheme(systemColorScheme.matches, $appState.profile_settings.profile.theme);
} else {
determineTheme(systemColorScheme.matches, 'system');
}

// User prompt
let type = $state?.current_user_prompt?.type;

if (type && type !== 'redirect') {
goto(`/prompt/${type}`);
}
}

interface DevModeButton {
Expand Down Expand Up @@ -152,7 +192,7 @@

<main class="absolute h-screen">
<!-- Dev Mode: Navbar -->
{#if $state?.dev_mode !== 'Off'}
{#if $appState?.dev_mode !== 'Off'}
{#if expandedDevMenu}
<div
class="hide-scrollbar fixed z-20 flex w-full space-x-4 overflow-x-auto bg-gradient-to-r from-red-200 to-red-300 p-4 shadow-md"
Expand Down Expand Up @@ -193,7 +233,7 @@

<hr class="mx-8 h-1 bg-orange-800" />

{#each $state.debug_messages as message}
{#each $appState.debug_messages as message}
<div class="mx-2 mb-2 rounded bg-orange-200 p-2">
<div class="break-all font-mono text-xs text-orange-700">{message}</div>
</div>
Expand Down Expand Up @@ -229,16 +269,16 @@
<div class="fixed top-[var(--safe-area-inset-top)] h-auto w-full">
<slot />
<!-- Show error if exists -->
{#if $error}
{#if $errorState}
<div class="absolute bottom-4 right-4 w-[calc(100%_-_32px)]">
<ErrorToast
title={$state?.dev_mode !== 'Off' ? 'Error' : $LL.ERROR.TITLE()}
detail={$state?.dev_mode !== 'Off' ? $error : $LL.ERROR.DEFAULT_MESSAGE()}
title={$appState?.dev_mode !== 'Off' ? 'Error' : $LL.ERROR.TITLE()}
detail={$appState?.dev_mode !== 'Off' ? $errorState : $LL.ERROR.DEFAULT_MESSAGE()}
on:dismissed={() => {
// After the toast fires the "dismissed" event, we clear the current $error store.
$error = undefined;
// After the toast fires the "dismissed" event, we reset $errorStore.
errorState.set(undefined);
}}
autoDismissAfterMs={$state?.dev_mode !== 'Off' ? 0 : 5_000}
autoDismissAfterMs={$appState?.dev_mode !== 'Off' ? 0 : 5_000}
/>
</div>
{/if}
Expand Down
Loading