Files
hikari-desktop/src/routes/+page.svelte
T
hikari ad9c914fb1 feat: add auto-update checker
Implements issue #17 - the app now checks for updates on startup and shows
a notification when a newer version is available. Users can disable this
in settings. Uses Gitea releases API with semver comparison.

Closes #17
2026-01-23 15:37:10 -08:00

176 lines
5.4 KiB
Svelte

<script lang="ts">
import { onMount, onDestroy } from "svelte";
import { invoke } from "@tauri-apps/api/core";
import { get } from "svelte/store";
import { initializeTauriListeners, cleanupTauriListeners } from "$lib/tauri";
import { configStore, applyTheme } from "$lib/stores/config";
import { initNotificationSync, cleanupNotificationSync } from "$lib/stores/notifications";
import { conversationsStore } from "$lib/stores/conversations";
import { claudeStore, isClaudeProcessing } from "$lib/stores/claude";
import { getCurrentWindow } from "@tauri-apps/api/window";
import "$lib/notifications/testNotifications";
import Terminal from "$lib/components/Terminal.svelte";
import InputBar from "$lib/components/InputBar.svelte";
import StatusBar from "$lib/components/StatusBar.svelte";
import AnimeGirl from "$lib/components/AnimeGirl.svelte";
import PermissionModal from "$lib/components/PermissionModal.svelte";
import UserQuestionModal from "$lib/components/UserQuestionModal.svelte";
import ConfigSidebar from "$lib/components/ConfigSidebar.svelte";
import AchievementNotification from "$lib/components/AchievementNotification.svelte";
import AchievementsPanel from "$lib/components/AchievementsPanel.svelte";
import UpdateNotification from "$lib/components/UpdateNotification.svelte";
let initialized = false;
let updateNotification: UpdateNotification;
let achievementPanelOpen = $state(false);
// Global keyboard shortcuts
function handleGlobalKeydown(event: KeyboardEvent) {
// Don't trigger shortcuts when typing in inputs (except for specific ones)
const target = event.target as HTMLElement;
const isInputFocused = target.tagName === "INPUT" || target.tagName === "TEXTAREA";
// Escape closes panels (always works)
if (event.key === "Escape") {
// Check if any panels are open and close them
if (achievementPanelOpen) {
achievementPanelOpen = false;
event.preventDefault();
return;
}
// ConfigSidebar handles its own escape via store
if (get(configStore.isSidebarOpen)) {
configStore.closeSidebar();
event.preventDefault();
return;
}
}
// Skip other shortcuts if user is typing in an input
if (isInputFocused) return;
// Ctrl+L - Clear terminal
if (event.ctrlKey && event.key === "l") {
event.preventDefault();
claudeStore.clearTerminal();
return;
}
// Ctrl+, - Open settings
if (event.ctrlKey && event.key === ",") {
event.preventDefault();
configStore.openSidebar();
return;
}
// Ctrl+C - Interrupt (only when processing)
if (event.ctrlKey && event.key === "c") {
if (get(isClaudeProcessing)) {
event.preventDefault();
handleInterrupt();
return;
}
}
}
async function handleInterrupt() {
try {
const conversationId = get(claudeStore.activeConversationId);
if (!conversationId) return;
await invoke("interrupt_claude", { conversationId });
claudeStore.addLine("system", "Process interrupted");
} catch (error) {
console.error("Failed to interrupt:", error);
}
}
onMount(async () => {
if (!initialized) {
initialized = true;
// Initialize conversations store first to ensure activeConversationId is set
conversationsStore.initialize();
await initializeTauriListeners();
await configStore.loadConfig();
// Apply saved settings on startup
const config = configStore.getConfig();
applyTheme(config.theme);
// Apply always-on-top setting
if (config.always_on_top) {
const window = getCurrentWindow();
await window.setAlwaysOnTop(true);
}
// Initialize notification settings sync
initNotificationSync();
// Add global keyboard shortcut listener
window.addEventListener("keydown", handleGlobalKeydown);
// Check for updates on startup
if (config.update_checks_enabled) {
updateNotification?.checkForUpdates();
}
}
});
onDestroy(() => {
if (initialized) {
cleanupTauriListeners();
cleanupNotificationSync();
window.removeEventListener("keydown", handleGlobalKeydown);
initialized = false;
}
});
</script>
<div class="app-container h-screen w-screen flex flex-col bg-[var(--bg-primary)] overflow-hidden">
<StatusBar onToggleAchievements={() => (achievementPanelOpen = !achievementPanelOpen)} />
<main class="flex-1 flex overflow-hidden">
<!-- Left panel: Character display -->
<div
class="character-panel w-1/3 flex flex-col items-center justify-center border-r border-[var(--border-color)] bg-[var(--bg-secondary)]/50"
>
<AnimeGirl />
</div>
<!-- Right panel: Terminal and input -->
<div class="terminal-panel flex-1 flex flex-col">
<Terminal />
<InputBar />
</div>
</main>
<PermissionModal />
<UserQuestionModal />
<ConfigSidebar />
<AchievementNotification />
<AchievementsPanel
bind:isOpen={achievementPanelOpen}
onClose={() => (achievementPanelOpen = false)}
/>
<UpdateNotification bind:this={updateNotification} />
</div>
<style>
.app-container {
font-family:
"Inter",
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
sans-serif;
}
.character-panel {
min-width: 320px;
background: linear-gradient(180deg, var(--bg-secondary) 0%, var(--bg-primary) 100%);
}
</style>