Compare commits
2 Commits
8b69e445b5
...
cf8f0d28a6
| Author | SHA1 | Date | |
|---|---|---|---|
| cf8f0d28a6 | |||
| 0252f95e35 |
@@ -903,7 +903,10 @@ impl Launcher {
|
|||||||
// launch: `dev_engine` would quietly hand back the installed version, and the window
|
// launch: `dev_engine` would quietly hand back the installed version, and the window
|
||||||
// would go on saying the run came from the checkout. Being told the folder is wrong
|
// would go on saying the run came from the checkout. Being told the folder is wrong
|
||||||
// while looking at it beats wondering later why an edit changed nothing.
|
// while looking at it beats wondering later why an edit changed nothing.
|
||||||
if next.dev_mode {
|
// Only a folder that was NAMED is checked. Dev mode is switched on from the hub now,
|
||||||
|
// and the checkout is chosen next to the Launch button afterwards, so refusing the
|
||||||
|
// switch for an empty folder would refuse the first half of a two-step job.
|
||||||
|
if next.dev_mode && !next.dev_root.is_empty() {
|
||||||
let build = PathBuf::from(&next.dev_root).join("ODIAN_Engine").join("tools").join("build").join("build.bat");
|
let build = PathBuf::from(&next.dev_root).join("ODIAN_Engine").join("tools").join("build").join("build.bat");
|
||||||
if !build.is_file() {
|
if !build.is_file() {
|
||||||
return Err(format!("{} is not there: the dev folder is the one HOLDING ODIAN_Engine.", build.display()));
|
return Err(format!("{} is not there: the dev folder is the one HOLDING ODIAN_Engine.", build.display()));
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||||
"productName": "ODIAN Launcher",
|
"productName": "ODIAN Launcher",
|
||||||
"version": "0.1.36",
|
"version": "0.1.38",
|
||||||
"identifier": "fr.odian.launcher",
|
"identifier": "fr.odian.launcher",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../dist",
|
"frontendDist": "../dist",
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 116 KiB |
@@ -97,7 +97,24 @@
|
|||||||
// Said out loud whenever this run is not the installed ODIAN. A dev client looks exactly
|
// Said out loud whenever this run is not the installed ODIAN. A dev client looks exactly
|
||||||
// like the real one on screen, and mistaking the two is how a bug gets chased in the
|
// like the real one on screen, and mistaking the two is how a bug gets chased in the
|
||||||
// wrong tree - or a demo gets given from an unbuilt branch.
|
// wrong tree - or a demo gets given from an unbuilt branch.
|
||||||
const devRun = $derived((s?.dev ?? null) !== null && (launcher.settings?.dev_mode ?? false));
|
const devOn = $derived(launcher.devOn);
|
||||||
|
const devRun = $derived(launcher.devReady);
|
||||||
|
const devRoot = $derived(launcher.settings?.dev_root ?? "");
|
||||||
|
const devTarget = $derived(launcher.settings?.dev_target ?? "client");
|
||||||
|
const devBuild = $derived(launcher.settings?.dev_build ?? false);
|
||||||
|
const savingDev = $derived(busy === "settings");
|
||||||
|
// One stack either way: both clients talk to the same API and the same database, which is
|
||||||
|
// the only way a .exe/web divergence is visible at all.
|
||||||
|
const TARGETS = [
|
||||||
|
{ key: "client", name: "ODIAN", note: "Odian.exe" },
|
||||||
|
{ key: "web", name: "Web", note: ":5173" },
|
||||||
|
{ key: "both", name: "Both", note: "one stack" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
async function browseCheckout() {
|
||||||
|
const path = await launcher.pickFolder();
|
||||||
|
if (path) await launcher.setDev({ dev_root: path });
|
||||||
|
}
|
||||||
// Named only when it is someone ELSE: running as yourself is not worth a warning.
|
// Named only when it is someone ELSE: running as yourself is not worth a warning.
|
||||||
const devUser = $derived.by(() => {
|
const devUser = $derived.by(() => {
|
||||||
const asked = launcher.settings?.dev_user?.trim() ?? "";
|
const asked = launcher.settings?.dev_user?.trim() ?? "";
|
||||||
@@ -221,16 +238,79 @@
|
|||||||
<button class="btn-link" disabled={blocked} onclick={() => launcher.openSync()}>Sync</button>
|
<button class="btn-link" disabled={blocked} onclick={() => launcher.openSync()}>Sync</button>
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
{#if devRun}
|
{#if devOn}
|
||||||
<p class="secondary attention">
|
<!-- Dev mode's own controls, where the launching happens. Everything that decides
|
||||||
<Icon name="alert" size={14} />
|
what Launch does is here rather than behind Settings: the target, the checkout,
|
||||||
<span>
|
and whether it builds first. -->
|
||||||
Dev mode: ODIAN runs from your checkout, not from the installed
|
<section class="dev" aria-label="Dev mode">
|
||||||
<strong>{s?.installed ?? "version"}</strong>.{#if devUser}
|
<header class="dev-head">
|
||||||
Running as <strong>{devUser}</strong> — sync is off.{/if}
|
<Icon name="code" size={13} />
|
||||||
</span>
|
<span class="dev-title">DEV</span>
|
||||||
|
<!-- What Launch will actually start, in three words. The folder is not repeated
|
||||||
|
here: it is in the field two rows down. -->
|
||||||
|
<span class="dev-from mono">{devRun ? TARGETS.find((o) => o.key === devTarget)?.name ?? "" : "no checkout"}</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="dev-targets" role="radiogroup" aria-label="Launch starts">
|
||||||
|
{#each TARGETS as option (option.key)}
|
||||||
|
<label class="dev-target" class:on={devTarget === option.key}>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="dev-target"
|
||||||
|
value={option.key}
|
||||||
|
checked={devTarget === option.key}
|
||||||
|
disabled={blocked || savingDev}
|
||||||
|
onchange={() => launcher.setDev({ dev_target: option.key })}
|
||||||
|
/>
|
||||||
|
<span class="dev-target-name">{option.name}</span>
|
||||||
|
<span class="dev-target-note mono">{option.note}</span>
|
||||||
|
</label>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dev-row">
|
||||||
|
<input
|
||||||
|
class="dev-path"
|
||||||
|
type="text"
|
||||||
|
spellcheck="false"
|
||||||
|
placeholder="Folder holding ODIAN_Engine"
|
||||||
|
value={devRoot}
|
||||||
|
disabled={blocked || savingDev}
|
||||||
|
onchange={(e) => launcher.setDev({ dev_root: e.currentTarget.value.trim() })}
|
||||||
|
/>
|
||||||
|
<button type="button" class="btn" disabled={blocked || savingDev} onclick={browseCheckout}>Browse…</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dev-row wrap">
|
||||||
|
<label class="dev-check">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={devBuild}
|
||||||
|
disabled={blocked || savingDev}
|
||||||
|
onchange={(e) => launcher.setDev({ dev_build: e.currentTarget.checked })}
|
||||||
|
/>
|
||||||
|
<span>Build first</span>
|
||||||
|
</label>
|
||||||
|
{#if s?.online}
|
||||||
|
<!-- The stack is the install's, so its tiles and its data are still worth
|
||||||
|
refreshing from the DHP while there is a line to it. -->
|
||||||
|
<button class="btn-link" disabled={blocked} onclick={() => launcher.syncTiles()}>Sync tiles</button>
|
||||||
|
<button class="btn-link" disabled={blocked} onclick={() => launcher.syncNow()}>Sync database</button>
|
||||||
|
{:else}
|
||||||
|
<span class="dev-offline mono">offline — tiles and data stay as they are</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if !devRun}
|
||||||
|
<p class="dev-note">Name the folder that holds <code>ODIAN_Engine</code> and Launch runs from it.</p>
|
||||||
|
{:else if devUser}
|
||||||
|
<p class="dev-note attention">
|
||||||
|
Running as <strong>{devUser}</strong> — this PC will not sync with the DHP.
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if unfinished}
|
{#if unfinished}
|
||||||
<p class="secondary attention">
|
<p class="secondary attention">
|
||||||
<Icon name="alert" size={14} />
|
<Icon name="alert" size={14} />
|
||||||
@@ -376,6 +456,114 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
/* Dev mode's controls. Deliberately compact and deliberately here: what Launch will do
|
||||||
|
is decided in the same place you press it, not two drawers away. */
|
||||||
|
.dev {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
padding: 0.7rem 0.75rem;
|
||||||
|
border: 1px solid color-mix(in srgb, #46d17f 32%, transparent);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: color-mix(in srgb, #46d17f 7%, transparent);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.55rem;
|
||||||
|
}
|
||||||
|
.dev-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
color: #46d17f;
|
||||||
|
}
|
||||||
|
.dev-title {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
}
|
||||||
|
.dev-from {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: var(--text-dim, #9aa3ad);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 55%;
|
||||||
|
direction: rtl; /* keep the END of a long path, which is the part that differs */
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.dev-targets {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
.dev-target {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.1rem;
|
||||||
|
padding: 0.35rem 0.25rem;
|
||||||
|
border: 1px solid var(--line, #2a3139);
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
.dev-target.on {
|
||||||
|
border-color: color-mix(in srgb, #46d17f 60%, transparent);
|
||||||
|
background: color-mix(in srgb, #46d17f 14%, transparent);
|
||||||
|
}
|
||||||
|
.dev-target:has(input:disabled) {
|
||||||
|
opacity: 0.55;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.dev-target input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.dev-target-name {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.dev-target-note {
|
||||||
|
font-size: 0.68rem;
|
||||||
|
color: var(--text-dim, #9aa3ad);
|
||||||
|
}
|
||||||
|
.dev-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
.dev-row.wrap {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
row-gap: 0.3rem;
|
||||||
|
}
|
||||||
|
.dev-path {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0.3rem 0.45rem;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-family: inherit;
|
||||||
|
color: inherit;
|
||||||
|
background: rgba(0, 0, 0, 0.25);
|
||||||
|
border: 1px solid var(--line, #2a3139);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
.dev-check {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.dev-offline,
|
||||||
|
.dev-note {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-dim, #9aa3ad);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.dev-note code {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
}
|
||||||
|
|
||||||
.action {
|
.action {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
|
|||||||
@@ -19,7 +19,8 @@
|
|||||||
| "signout"
|
| "signout"
|
||||||
| "key"
|
| "key"
|
||||||
| "globe"
|
| "globe"
|
||||||
| "market";
|
| "market"
|
||||||
|
| "code";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@@ -98,6 +99,9 @@
|
|||||||
<rect x="9" y="2.5" width="4.5" height="4.5" />
|
<rect x="9" y="2.5" width="4.5" height="4.5" />
|
||||||
<rect x="2.5" y="9" width="4.5" height="4.5" />
|
<rect x="2.5" y="9" width="4.5" height="4.5" />
|
||||||
<path d="M11.25 9v4.5M9 11.25h4.5" />
|
<path d="M11.25 9v4.5M9 11.25h4.5" />
|
||||||
|
{:else if name === "code"}
|
||||||
|
<path d="M5.75 4.5 2 8l3.75 3.5" />
|
||||||
|
<path d="M10.25 4.5 14 8l-3.75 3.5" />
|
||||||
{/if}
|
{/if}
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,19 @@
|
|||||||
// It starts where the news panel ends, so the wordmark is never half-hidden
|
// It starts where the news panel ends, so the wordmark is never half-hidden
|
||||||
// behind it and the panel's side of the window stays plain black.
|
// behind it and the panel's side of the window stays plain black.
|
||||||
import keyart from "../assets/keyart.webp";
|
import keyart from "../assets/keyart.webp";
|
||||||
|
import keyartDev from "../assets/keyart-dev.webp";
|
||||||
|
import { launcher } from "../lib/launcher.svelte";
|
||||||
|
|
||||||
|
// Dev mode gets its own picture. Not decoration: the backdrop is the one thing on screen
|
||||||
|
// you cannot miss, so a glance says which launcher you are looking at before you read a
|
||||||
|
// word of it. It follows the SWITCH, not the licence - a licensed PC with dev mode off is
|
||||||
|
// an ordinary launcher and should look like one.
|
||||||
|
const art = $derived(launcher.devOn ? keyartDev : keyart);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="keyart" aria-hidden="true">
|
<div class="keyart" aria-hidden="true">
|
||||||
<div class="frame">
|
<div class="frame">
|
||||||
<img class="art" src={keyart} alt="" draggable="false" />
|
<img class="art" src={art} alt="" draggable="false" />
|
||||||
</div>
|
</div>
|
||||||
<div class="vignette"></div>
|
<div class="vignette"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -62,7 +62,6 @@
|
|||||||
trimmed.dev_target !== current.dev_target),
|
trimmed.dev_target !== current.dev_target),
|
||||||
);
|
);
|
||||||
/** A drive's launcher needs a folder on a disk: a root or a share holds no install. */
|
/** A drive's launcher needs a folder on a disk: a root or a share holds no install. */
|
||||||
const dev = $derived(launcher.status?.dev ?? null);
|
|
||||||
const offDrive = $derived(portable && trimmed.install_dir !== "" && !/^[A-Za-z]:\\.+/.test(trimmed.install_dir));
|
const offDrive = $derived(portable && trimmed.install_dir !== "" && !/^[A-Za-z]:\\.+/.test(trimmed.install_dir));
|
||||||
// Blank is a deliberate answer for all three: no DHP, no sign-in service, no AI. It is
|
// Blank is a deliberate answer for all three: no DHP, no sign-in service, no AI. It is
|
||||||
// how a PC is put fully offline on purpose - and how an offline site is rehearsed on a
|
// how a PC is put fully offline on purpose - and how an offline site is rehearsed on a
|
||||||
@@ -294,82 +293,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
{#if dev}
|
|
||||||
<!-- Only ever rendered on a PC holding a valid signed licence. An operator's
|
|
||||||
launcher has no dev section at all, which is the point of the signature. -->
|
|
||||||
<section class="group" aria-labelledby="set-dev-title">
|
|
||||||
<h3 id="set-dev-title" class="eyebrow">Dev</h3>
|
|
||||||
<p class="text">
|
|
||||||
{#if dev.machine}This PC{:else}This install{/if} carries a dev licence, valid until
|
|
||||||
<strong>{dev.expires_at}</strong>{#if dev.note} — {dev.note}{/if}. Dev mode runs the
|
|
||||||
client from your checkout, in a console, over this install's database and tiles.
|
|
||||||
</p>
|
|
||||||
<div class="field">
|
|
||||||
<label class="field-label" for="set-dev-root">Checkout folder</label>
|
|
||||||
<div class="row">
|
|
||||||
<input
|
|
||||||
id="set-dev-root"
|
|
||||||
class="input"
|
|
||||||
type="text"
|
|
||||||
spellcheck="false"
|
|
||||||
placeholder="E:\ODIAN_Dev"
|
|
||||||
bind:value={draft.dev_root}
|
|
||||||
disabled={locked || busy}
|
|
||||||
/>
|
|
||||||
<button type="button" class="btn" disabled={locked || busy} onclick={browseDev}>Browse…</button>
|
|
||||||
</div>
|
|
||||||
<p class="hint">The folder that holds <code>ODIAN_Engine</code>.</p>
|
|
||||||
</div>
|
|
||||||
<label class="check-row">
|
|
||||||
<input type="checkbox" bind:checked={draft.dev_mode} disabled={locked || busy || !draft.dev_root.trim()} />
|
|
||||||
<span>Launch the whole stack from the checkout instead of the installed version</span>
|
|
||||||
</label>
|
|
||||||
<label class="check-row">
|
|
||||||
<input type="checkbox" bind:checked={draft.dev_build} disabled={locked || busy || !draft.dev_mode} />
|
|
||||||
<span>Build it first, every launch</span>
|
|
||||||
</label>
|
|
||||||
<div class="field">
|
|
||||||
<span class="field-label" id="set-dev-target-label">Launch starts</span>
|
|
||||||
<div class="choices" role="radiogroup" aria-labelledby="set-dev-target-label">
|
|
||||||
{#each TARGETS as option (option.key)}
|
|
||||||
<label class="choice" class:on={draft.dev_target === option.key}>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="dev-target"
|
|
||||||
value={option.key}
|
|
||||||
bind:group={draft.dev_target}
|
|
||||||
disabled={locked || busy || !draft.dev_mode}
|
|
||||||
/>
|
|
||||||
<span class="choice-name">{option.name}</span>
|
|
||||||
<span class="choice-note">{option.note}</span>
|
|
||||||
</label>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="field-label" for="set-dev-user">Run as</label>
|
|
||||||
<input
|
|
||||||
id="set-dev-user"
|
|
||||||
class="input"
|
|
||||||
type="text"
|
|
||||||
spellcheck="false"
|
|
||||||
placeholder={launcher.status?.operator ?? "the operator who signed in"}
|
|
||||||
bind:value={draft.dev_user}
|
|
||||||
disabled={locked || busy || !draft.dev_mode}
|
|
||||||
/>
|
|
||||||
<p class="hint">
|
|
||||||
{#if draft.dev_user.trim() && draft.dev_user.trim() !== launcher.status?.operator}
|
|
||||||
This PC will <strong>not sync with the DHP</strong> while it runs as someone else: work
|
|
||||||
authored as them would arrive there under your account.
|
|
||||||
{:else}
|
|
||||||
Another operator's name, to test what they see. Sync is switched off while it differs
|
|
||||||
from yours.
|
|
||||||
{/if}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<section class="group" aria-labelledby="set-drive-title">
|
<section class="group" aria-labelledby="set-drive-title">
|
||||||
<h3 id="set-drive-title" class="eyebrow">Install from drive</h3>
|
<h3 id="set-drive-title" class="eyebrow">Install from drive</h3>
|
||||||
<p class="text">
|
<p class="text">
|
||||||
@@ -447,56 +370,6 @@
|
|||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* The Dev section's own rows. Only ever drawn on a licensed PC. */
|
|
||||||
.check-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-top: 0.6rem;
|
|
||||||
line-height: 1.35;
|
|
||||||
}
|
|
||||||
.check-row input {
|
|
||||||
margin-top: 0.15rem;
|
|
||||||
flex: none;
|
|
||||||
}
|
|
||||||
.hint {
|
|
||||||
margin: 0.35rem 0 0;
|
|
||||||
font-size: 0.82rem;
|
|
||||||
line-height: 1.45;
|
|
||||||
color: var(--text-dim, #9aa3ad);
|
|
||||||
}
|
|
||||||
.choices {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.35rem;
|
|
||||||
margin-top: 0.4rem;
|
|
||||||
}
|
|
||||||
.choice {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto auto 1fr;
|
|
||||||
align-items: baseline;
|
|
||||||
gap: 0.5rem;
|
|
||||||
padding: 0.4rem 0.55rem;
|
|
||||||
border: 1px solid var(--border, #2a3139);
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.choice.on {
|
|
||||||
border-color: var(--accent, #5b8cff);
|
|
||||||
background: color-mix(in srgb, var(--accent, #5b8cff) 12%, transparent);
|
|
||||||
}
|
|
||||||
.choice:has(input:disabled) {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
.choice-name {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.choice-note {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--text-dim, #9aa3ad);
|
|
||||||
}
|
|
||||||
|
|
||||||
.drawer {
|
.drawer {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0 0 0 auto;
|
inset: 0 0 0 auto;
|
||||||
|
|||||||
@@ -16,6 +16,10 @@
|
|||||||
|
|
||||||
const status = $derived(launcher.status);
|
const status = $derived(launcher.status);
|
||||||
const checking = $derived(launcher.refreshing || status?.phase === "checking");
|
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>) {
|
function windowAction(task: () => Promise<void>) {
|
||||||
return () => {
|
return () => {
|
||||||
@@ -80,6 +84,22 @@
|
|||||||
|
|
||||||
<div class="drag" data-tauri-drag-region></div>
|
<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}>
|
<button class="icon-btn market" aria-label="Marketplace" title="Marketplace" aria-haspopup="dialog" onclick={onOpenMarket}>
|
||||||
<Icon name="market" />
|
<Icon name="market" />
|
||||||
</button>
|
</button>
|
||||||
@@ -107,6 +127,18 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<style>
|
<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 {
|
.titlebar {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
|
|||||||
@@ -402,6 +402,23 @@ class Launcher {
|
|||||||
return this.launch();
|
return this.launch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Change part of the settings and save, for the dev controls on the hub. */
|
||||||
|
setDev(change: Partial<Settings>) {
|
||||||
|
const current = this.settings;
|
||||||
|
if (!current) return Promise.resolve();
|
||||||
|
return this.saveSettings({ ...current, ...change });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Dev mode is ON: this PC has a licence and the switch is thrown. */
|
||||||
|
get devOn(): boolean {
|
||||||
|
return (this.status?.dev ?? null) !== null && (this.settings?.dev_mode ?? false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ...and a checkout has been named, so a launch will really come from it. */
|
||||||
|
get devReady(): boolean {
|
||||||
|
return this.devOn && (this.settings?.dev_root ?? "").trim() !== "";
|
||||||
|
}
|
||||||
|
|
||||||
saveSettings(next: Settings) {
|
saveSettings(next: Settings) {
|
||||||
return this.run("settings", async () => {
|
return this.run("settings", async () => {
|
||||||
this.applyStatus(await backend.setSettings(next));
|
this.applyStatus(await backend.setSettings(next));
|
||||||
|
|||||||
Reference in New Issue
Block a user