302 lines
7.8 KiB
Svelte
302 lines
7.8 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from "svelte";
|
|
import Icon from "./Icon.svelte";
|
|
import { launcher } from "../lib/launcher.svelte";
|
|
// ODIAN's own mark, the same one the window and the taskbar carry.
|
|
import mark from "../assets/mark.png";
|
|
|
|
interface Props {
|
|
onOpenSettings: () => void;
|
|
onOpenMarket: () => void;
|
|
}
|
|
|
|
let { onOpenSettings, onOpenMarket }: Props = $props();
|
|
|
|
let maximized = $state(false);
|
|
|
|
const status = $derived(launcher.status);
|
|
const checking = $derived(launcher.refreshing || status?.phase === "checking");
|
|
// The licence, not a setting: an operator's launcher must show no trace of dev mode.
|
|
const licensed = $derived((status?.dev ?? null) !== null);
|
|
const devOn = $derived(launcher.devOn);
|
|
const saving = $derived(launcher.busy === "settings");
|
|
|
|
function windowAction(task: () => Promise<void>) {
|
|
return () => {
|
|
task().catch((e) => console.error("window control failed", e));
|
|
};
|
|
}
|
|
|
|
onMount(() => {
|
|
let disposed = false;
|
|
let unlisten: (() => void) | undefined;
|
|
const sync = () => {
|
|
launcher.win
|
|
.isMaximized()
|
|
.then((value) => (maximized = value))
|
|
.catch(() => {});
|
|
};
|
|
sync();
|
|
launcher.win
|
|
.onResized(sync)
|
|
.then((u) => (disposed ? u() : (unlisten = u)))
|
|
.catch(() => {});
|
|
return () => {
|
|
disposed = true;
|
|
unlisten?.();
|
|
};
|
|
});
|
|
</script>
|
|
|
|
<header class="titlebar" data-tauri-drag-region>
|
|
<div class="brand" data-tauri-drag-region>
|
|
<img class="mark" src={mark} width="18" height="18" alt="" draggable="false" data-tauri-drag-region />
|
|
<span class="wordmark" data-tauri-drag-region>ODIAN</span>
|
|
<span class="product" data-tauri-drag-region>Launcher</span>
|
|
</div>
|
|
|
|
{#if status}
|
|
<span class="divider" aria-hidden="true" data-tauri-drag-region></span>
|
|
<div class="site" data-tauri-drag-region>
|
|
<span class="site-name" title={status.site.url} data-tauri-drag-region>{launcher.siteName}</span>
|
|
<span
|
|
class="badge"
|
|
class:online={status.online}
|
|
class:checking
|
|
role="status"
|
|
aria-busy={checking}
|
|
data-tauri-drag-region
|
|
>
|
|
<span class="badge-dot" aria-hidden="true"></span>
|
|
{status.online ? "Online" : "Offline"}
|
|
</span>
|
|
<button
|
|
class="icon-btn refresh"
|
|
aria-label="Check the connection to your DHP"
|
|
title="Check connection"
|
|
disabled={checking}
|
|
onclick={() => launcher.refresh()}
|
|
>
|
|
<span class="refresh-icon" class:spinning={checking}><Icon name="refresh" size={14} /></span>
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
|
|
<div class="drag" data-tauri-drag-region></div>
|
|
|
|
{#if licensed}
|
|
<!-- Only on a PC that holds a dev licence. An operator's launcher never draws it, which
|
|
is the whole point of the licence - so this sits behind `status.dev`, not a setting. -->
|
|
<button
|
|
class="icon-btn dev"
|
|
class:on={devOn}
|
|
aria-label={devOn ? "Dev mode is on" : "Turn dev mode on"}
|
|
title={devOn ? "Dev mode is ON - ODIAN runs from your checkout" : "Dev mode"}
|
|
aria-pressed={devOn}
|
|
disabled={saving}
|
|
onclick={() => launcher.setDev({ dev_mode: !devOn })}
|
|
>
|
|
<Icon name="code" />
|
|
</button>
|
|
{/if}
|
|
|
|
<button class="icon-btn market" aria-label="Marketplace" title="Marketplace" aria-haspopup="dialog" onclick={onOpenMarket}>
|
|
<Icon name="market" />
|
|
</button>
|
|
|
|
<button class="icon-btn settings" aria-label="Settings" title="Settings" aria-haspopup="dialog" onclick={onOpenSettings}>
|
|
<Icon name="settings" />
|
|
</button>
|
|
|
|
<div class="window-controls">
|
|
<button class="win" aria-label="Minimize" title="Minimize" onclick={windowAction(() => launcher.win.minimize())}>
|
|
<Icon name="minimize" size={14} strokeWidth={1.25} />
|
|
</button>
|
|
<button
|
|
class="win"
|
|
aria-label={maximized ? "Restore" : "Maximize"}
|
|
title={maximized ? "Restore" : "Maximize"}
|
|
onclick={windowAction(() => launcher.win.toggleMaximize())}
|
|
>
|
|
<Icon name={maximized ? "restore" : "maximize"} size={14} strokeWidth={1.25} />
|
|
</button>
|
|
<button class="win close" aria-label="Close" title="Close" onclick={windowAction(() => launcher.win.close())}>
|
|
<Icon name="close" size={14} strokeWidth={1.25} />
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<style>
|
|
/* The dev switch. Off it is one more grey icon button; on it is unmistakably GREEN,
|
|
because "am I running my own build or the installed one" is the question this whole
|
|
mode exists to answer, and guessing it wrong wastes an afternoon. */
|
|
.icon-btn.dev.on {
|
|
color: #46d17f;
|
|
border-color: color-mix(in srgb, #46d17f 55%, transparent);
|
|
background: color-mix(in srgb, #46d17f 16%, transparent);
|
|
}
|
|
.icon-btn.dev.on:hover:not(:disabled) {
|
|
background: color-mix(in srgb, #46d17f 26%, transparent);
|
|
}
|
|
|
|
.titlebar {
|
|
position: relative;
|
|
z-index: 20;
|
|
display: flex;
|
|
align-items: center;
|
|
height: var(--titlebar-h);
|
|
padding-left: var(--space-4);
|
|
border-bottom: 1px solid rgba(35, 35, 38, 0.8);
|
|
background: linear-gradient(180deg, rgba(9, 9, 11, 0.9), rgba(9, 9, 11, 0.6));
|
|
backdrop-filter: blur(14px);
|
|
-webkit-backdrop-filter: blur(14px);
|
|
}
|
|
|
|
.brand {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
height: 100%;
|
|
}
|
|
|
|
.mark {
|
|
display: block;
|
|
border-radius: 4px;
|
|
user-select: none;
|
|
}
|
|
|
|
.wordmark {
|
|
margin-right: calc(var(--wordmark-tracking) * -1);
|
|
color: var(--ink);
|
|
font-family: var(--font-display);
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
letter-spacing: var(--wordmark-tracking);
|
|
}
|
|
|
|
.product {
|
|
margin-left: 6px;
|
|
color: var(--muted);
|
|
font-size: var(--text-xs);
|
|
letter-spacing: 0.04em;
|
|
}
|
|
|
|
.divider {
|
|
width: 1px;
|
|
height: 16px;
|
|
margin: 0 var(--space-4);
|
|
background: var(--line-strong);
|
|
}
|
|
|
|
.site {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
min-width: 0;
|
|
height: 100%;
|
|
font-size: var(--text-sm);
|
|
}
|
|
|
|
.site-name {
|
|
max-width: 280px;
|
|
overflow: hidden;
|
|
color: var(--ink-2);
|
|
white-space: nowrap;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
height: 22px;
|
|
padding: 0 8px;
|
|
border: 1px solid rgba(245, 180, 84, 0.3);
|
|
border-radius: var(--radius);
|
|
background: var(--warn-soft);
|
|
color: var(--warn);
|
|
font-family: var(--font-mono);
|
|
font-size: var(--text-xs);
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
transition:
|
|
color var(--dur) var(--ease),
|
|
background-color var(--dur) var(--ease),
|
|
border-color var(--dur) var(--ease);
|
|
}
|
|
.badge.online {
|
|
border-color: rgba(134, 239, 172, 0.28);
|
|
background: var(--ok-soft);
|
|
color: var(--ok);
|
|
}
|
|
|
|
.badge-dot {
|
|
width: 6px;
|
|
height: 6px;
|
|
border-radius: 50%;
|
|
background: currentColor;
|
|
}
|
|
.badge.checking .badge-dot {
|
|
animation: pulse 1.1s var(--ease) infinite;
|
|
}
|
|
|
|
.refresh {
|
|
width: 28px;
|
|
height: 28px;
|
|
}
|
|
.refresh:disabled {
|
|
opacity: 1;
|
|
}
|
|
|
|
.refresh-icon {
|
|
display: grid;
|
|
}
|
|
.spinning {
|
|
animation: spin 0.9s linear infinite;
|
|
}
|
|
|
|
.drag {
|
|
flex: 1;
|
|
align-self: stretch;
|
|
}
|
|
|
|
.market {
|
|
margin-right: var(--space-1);
|
|
}
|
|
|
|
.settings {
|
|
margin-right: var(--space-2);
|
|
}
|
|
|
|
.window-controls {
|
|
display: flex;
|
|
align-self: stretch;
|
|
border-left: 1px solid var(--line);
|
|
}
|
|
|
|
.win {
|
|
display: grid;
|
|
place-items: center;
|
|
width: 46px;
|
|
height: 100%;
|
|
padding: 0;
|
|
border: 0;
|
|
background: transparent;
|
|
color: var(--muted);
|
|
transition:
|
|
background-color var(--dur-fast) var(--ease),
|
|
color var(--dur-fast) var(--ease);
|
|
}
|
|
.win:hover {
|
|
background: var(--surface-2);
|
|
color: var(--ink);
|
|
}
|
|
.win.close:hover {
|
|
background: var(--danger);
|
|
color: var(--accent-ink);
|
|
}
|
|
.win:focus-visible {
|
|
outline-offset: -2px;
|
|
}
|
|
</style>
|