Skip to content

Commit

Permalink
refactor: Wire up Tauri listeners in onMount of root layout (#322)
Browse files Browse the repository at this point in the history
* Wire up Tauri state-changed listener in `onMount` of root layout

* Wire up Tauri error listener in root layout

* Update comments
  • Loading branch information
maiertech authored Aug 28, 2024
1 parent 1c6dffe commit 2b5590e
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 73 deletions.
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

0 comments on commit 2b5590e

Please sign in to comment.