feat: move to a navbar-based layout on the home screen for easier navigation

This commit is contained in:
nekename committed 2026-02-19 15:52:22 +00:00
1 parent e6fa25689c
commit f3aaa1b567
11 files changed
+206 -113

No files matched your search

+47 -5
View File
@@ -14,7 +14,11 @@ button, input[type="radio"], input[type="checkbox"], input[type="range"], input[
@apply cursor-pointer;
}
select {
.select-wrapper {
@apply relative;
}
.select-wrapper select {
@apply appearance-none;
@apply px-2 py-1;
@apply text-sm text-neutral-700 dark:text-neutral-300;
@@ -23,10 +27,6 @@ select {
@apply cursor-pointer;
}
.select-wrapper {
@apply relative;
}
.select-wrapper::after {
@apply absolute right-4 top-[47.5%] -translate-y-1/2;
@apply w-2 h-2 rotate-45;
@@ -34,6 +34,48 @@ select {
@apply pointer-events-none content-["_"];
}
.select-device-wrapper {
@apply relative;
}
.select-device-wrapper select {
@apply appearance-none;
@apply font-semibold text-xl text-neutral-800 dark:text-neutral-200;
@apply outline-hidden;
@apply cursor-pointer;
}
.select-device-wrapper::after {
@apply absolute right-0.5 top-[47.5%] -translate-y-1/2;
@apply w-2 h-2 rotate-45;
@apply border-r-2 border-b-2 border-neutral-800;
@apply pointer-events-none content-["_"];
}
:is(.dark, .dark *) .select-device-wrapper::after {
@apply border-neutral-100;
}
.select-profile-wrapper {
@apply relative;
}
.select-profile-wrapper select {
@apply appearance-none;
@apply text-neutral-500 dark:text-neutral-400;
@apply outline-hidden;
@apply cursor-pointer;
}
.select-profile-wrapper::after {
@apply absolute right-0.5 top-[47.5%] -translate-y-1/2;
@apply w-1.5 h-1.5 rotate-45;
@apply border-r-2 border-b-2 border-neutral-500;
@apply pointer-events-none content-["_"];
}
:is(.dark, .dark *) .select-profile-wrapper::after {
@apply border-neutral-400;
}
.plugin-readme h1, .plugin-readme h2, .plugin-readme h3, .plugin-readme h4, .plugin-readme h5, .plugin-readme h6 {
@apply font-medium dark:text-neutral-200;
}
+47 -45
View File
@@ -33,50 +33,52 @@
}
</script>
<div class="flex flex-row items-center bg-neutral-100 dark:bg-neutral-700 border-2 dark:border-neutral-900 rounded-md">
<MagnifyingGlass size="13" class="ml-2 mr-1 text-neutral-500 dark:text-neutral-300" />
<input
bind:value={query}
class="w-full p-1 text-sm text-neutral-700 dark:text-neutral-300 outline-hidden"
placeholder="Search actions"
type="search"
spellcheck="false"
/>
</div>
<div class="flex flex-col w-[18rem] h-full p-2 border-l dark:border-neutral-700">
<div class="flex flex-row items-center bg-neutral-100 dark:bg-neutral-700 border-2 dark:border-neutral-900 rounded-md">
<MagnifyingGlass size="13" class="ml-2 mr-1 text-neutral-500 dark:text-neutral-300" />
<input
bind:value={query}
class="w-full p-1 text-sm text-neutral-700 dark:text-neutral-300 outline-hidden"
placeholder="Search actions"
type="search"
spellcheck="false"
/>
</div>
<div class="grow mt-1 overflow-auto select-none">
{#each filteredCategories as [name, { icon, actions }]}
<details open class="mb-2">
<summary class="text-xl font-semibold dark:text-neutral-300">
{#if icon || (actions[0] && plugins.find((x) => x.id == actions[0].plugin) && categories[name].actions.every((x) => x.plugin == actions[0].plugin))}
<img
src={icon ? (!icon.startsWith("opendeck/") ? getWebserverUrl(icon) : icon.replace("opendeck", "")) : getWebserverUrl(plugins.find((x) => x.id == actions[0].plugin).icon)}
alt={name}
class="w-5 h-5 rounded-xs ml-1 -mt-1 inline"
/>
{/if}
<span class="ml-1">{name}</span>
</summary>
{#each actions as action}
<div
class="flex flex-row items-center my-2 space-x-2"
role="group"
draggable="true"
title={$localisations?.[action.plugin]?.[action.uuid]?.Tooltip ?? action.tooltip}
on:dragstart={(event) => {
if (!event.dataTransfer) return;
event.dataTransfer.effectAllowed = "copy";
event.dataTransfer.setData("action", JSON.stringify(action));
}}
>
<img
src={!action.icon.startsWith("opendeck/") ? getWebserverUrl(action.icon) : action.icon.replace("opendeck", "")}
alt={$localisations?.[action.plugin]?.[action.uuid]?.Tooltip ?? action.tooltip}
class="w-12 h-12 rounded-xs pointer-events-none"
/>
<span class="dark:text-neutral-400">{$localisations?.[action.plugin]?.[action.uuid]?.Name ?? action.name}</span>
</div>
{/each}
</details>
{/each}
<div class="grow mt-1 overflow-auto select-none">
{#each filteredCategories as [name, { icon, actions }]}
<details open class="mb-2">
<summary class="text-xl font-semibold dark:text-neutral-300">
{#if icon || (actions[0] && plugins.find((x) => x.id == actions[0].plugin) && categories[name].actions.every((x) => x.plugin == actions[0].plugin))}
<img
src={icon ? (!icon.startsWith("opendeck/") ? getWebserverUrl(icon) : icon.replace("opendeck", "")) : getWebserverUrl(plugins.find((x) => x.id == actions[0].plugin).icon)}
alt={name}
class="w-5 h-5 rounded-xs ml-1 -mt-1 inline"
/>
{/if}
<span class="ml-1">{name}</span>
</summary>
{#each actions as action}
<div
class="flex flex-row items-center my-2 space-x-2"
role="group"
draggable="true"
title={$localisations?.[action.plugin]?.[action.uuid]?.Tooltip ?? action.tooltip}
on:dragstart={(event) => {
if (!event.dataTransfer) return;
event.dataTransfer.effectAllowed = "copy";
event.dataTransfer.setData("action", JSON.stringify(action));
}}
>
<img
src={!action.icon.startsWith("opendeck/") ? getWebserverUrl(action.icon) : action.icon.replace("opendeck", "")}
alt={$localisations?.[action.plugin]?.[action.uuid]?.Tooltip ?? action.tooltip}
class="w-12 h-12 rounded-xs pointer-events-none"
/>
<span class="dark:text-neutral-400">{$localisations?.[action.plugin]?.[action.uuid]?.Name ?? action.name}</span>
</div>
{/each}
</details>
{/each}
</div>
</div>
+30 -12
View File
@@ -6,7 +6,7 @@
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { getCurrentWindow, LogicalSize } from "@tauri-apps/api/window";
import { currentMonitor, getCurrentWindow, LogicalSize } from "@tauri-apps/api/window";
export let devices: { [id: string]: DeviceInfo } = {};
export let value: string;
@@ -43,24 +43,42 @@
(async () => devices = await invoke("get_devices"))();
listen("devices", ({ payload }: { payload: { [id: string]: DeviceInfo } }) => devices = payload);
let buildInfo: string;
(async () => buildInfo = await invoke("get_build_info"))();
const window = getCurrentWindow();
$: {
if (devices[value]) {
const width = (Math.max(devices[value].columns, devices[value].encoders, devices[value].touchpoints) * 132) + 288;
const height = ((devices[value].rows + Math.min(devices[value].encoders, 1) + Math.min(devices[value].touchpoints, 1)) * 132) + 288;
const window = getCurrentWindow();
window.setMinSize(new LogicalSize(width, height)).then(async () => {
const innerSize = await window.innerSize();
if (innerSize.width < width || innerSize.height < height) {
await window.setSize(new LogicalSize(width, height));
}
});
const effectiveCols = Math.min(Math.max(devices[value].columns, devices[value].encoders, devices[value].touchpoints), 8);
const effectiveRows = Math.min(devices[value].rows + Math.min(devices[value].encoders, 1) + Math.min(devices[value].touchpoints, 1), 4);
const idealWidth = (effectiveCols * 132) + 416;
const idealHeight = (effectiveRows * 132) + 384 + (buildInfo?.includes("darwin") ? 28 : 0);
(async () => {
const monitor = await currentMonitor();
const scaleFactor = monitor?.scaleFactor ?? 1;
// Convert the monitor's work area to logical pixels
const maxWidth = (monitor?.workArea.size.width ?? Infinity) / scaleFactor;
const maxHeight = (monitor?.workArea.size.height ?? Infinity) / scaleFactor;
const width = Math.min(idealWidth, maxWidth);
const height = Math.min(idealHeight, maxHeight);
await window.setMinSize(new LogicalSize(width, height));
await window.setSize(new LogicalSize(width, height));
})();
}
}
let measure: HTMLSpanElement;
let selectWidth = 0;
$: if (value && measure && devices[value]) {
measure.textContent = devices[value].name;
selectWidth = measure.offsetWidth + 20;
}
</script>
{#if Object.keys(devices).length > 0}
<div class="select-wrapper">
<select bind:value class="w-full">
<div class="select-device-wrapper">
<span bind:this={measure} class="invisible fixed whitespace-pre pointer-events-none text-xl font-semibold" aria-hidden="true"></span>
<select bind:value style:width="{selectWidth}px">
<option value="" disabled selected>Choose a device...</option>
{#each Object.entries(devices).sort() as [id, device]}
+23 -1
View File
@@ -62,13 +62,35 @@
profile = profile;
}
}
$: overflowsX = Math.max(device.columns, device.encoders, device.touchpoints) > 8;
$: overflowsY = (device.rows + Math.min(device.encoders, 1) + Math.min(device.touchpoints, 1)) > 4;
</script>
<style>
.device-fade-x {
mask-image: linear-gradient(to right, transparent, black 7.5rem, black calc(100% - 7.5rem), transparent);
}
.device-fade-y {
mask-image: linear-gradient(to bottom, transparent, black 7.5rem, black calc(100% - 7.5rem), transparent);
}
.device-fade-xy {
mask-image:
linear-gradient(to right, transparent, black 7.5rem, black calc(100% - 7.5rem), transparent),
linear-gradient(to bottom, transparent, black 7.5rem, black calc(100% - 7.5rem), transparent);
mask-composite: intersect;
}
</style>
{#key device}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="flex flex-col items-center"
class="flex flex-col justify-center grow px-16 py-6 overflow-auto"
class:items-center={device.columns <= 8}
class:hidden={$inspectedParentAction || selectedDevice != device.id}
class:device-fade-x={overflowsX && !overflowsY}
class:device-fade-y={overflowsY && !overflowsX}
class:device-fade-xy={overflowsX && overflowsY}
on:click={() => inspectedInstance.set(null)}
on:keyup={() => inspectedInstance.set(null)}
>
+1 -1
View File
@@ -58,7 +58,7 @@
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="flex flex-col h-80 overflow-auto"
class="flex flex-col h-128 overflow-auto"
on:click={() => inspectedInstance.set(null)}
on:keyup={() => inspectedInstance.set(null)}
>
+1 -1
View File
@@ -196,7 +196,7 @@
</script>
<button
class="p-1 w-1/2 text-sm text-neutral-700 dark:text-neutral-300 bg-neutral-100 dark:bg-neutral-700 border dark:border-neutral-600 rounded-lg outline-hidden"
class="px-3 py-1 text-sm text-neutral-700 dark:text-neutral-300 bg-neutral-100 dark:bg-neutral-700 border dark:border-neutral-600 rounded-lg outline-hidden"
on:click={() => showPopup = true}
>
Plugins
+10 -2
View File
@@ -161,10 +161,18 @@
invoke("set_application_profiles", { value: applicationProfiles });
}
}
let measure: HTMLSpanElement;
let selectWidth = 0;
$: if (value && measure) {
measure.textContent = value.includes("/") ? value.split("/")[1] : value;
selectWidth = measure.offsetWidth + 18;
}
</script>
<div class="select-wrapper">
<select bind:value class="my-1 w-full">
<div class="select-profile-wrapper">
<span bind:this={measure} class="invisible fixed whitespace-pre pointer-events-none" aria-hidden="true"></span>
<select bind:value style:width="{selectWidth}px">
{#each Object.entries(folders).sort() as [id, profiles]}
{#if id && profiles.length}
<optgroup label={id}>
+1 -1
View File
@@ -175,7 +175,7 @@
}}
/>
<div class="grow overflow-auto bg-white dark:bg-neutral-900 border-t dark:border-neutral-700" bind:this={iframeContainer}>
<div class="grow min-h-64 max-h-96 overflow-auto bg-white dark:bg-neutral-900 border-t dark:border-neutral-700" bind:this={iframeContainer}>
<button
bind:this={iframeClosePopup}
on:click={() => closePopup(iframePopupsOpen[iframePopupsOpen.length - 1])}
+1 -1
View File
@@ -45,7 +45,7 @@
</script>
<button
class="p-1 w-1/2 text-sm text-neutral-700 dark:text-neutral-300 bg-neutral-100 dark:bg-neutral-700 border dark:border-neutral-600 rounded-lg outline-hidden"
class="px-3 py-1 text-sm text-neutral-700 dark:text-neutral-300 bg-neutral-100 dark:bg-neutral-700 border dark:border-neutral-600 rounded-lg outline-hidden"
on:click={() => showPopup = true}
>
Settings
+1 -3
View File
@@ -2,6 +2,4 @@
import "../app.css";
</script>
<div class="flex flex-row h-screen">
<slot />
</div>
<slot />
+44 -41
View File
@@ -25,48 +25,51 @@
<svelte:window on:dragover={(event) => event.preventDefault()} on:drop={(event) => event.preventDefault()} />
<div class="flex flex-col grow">
{#if Object.keys(devices).length > 0 && selectedProfiles}
{#if $inspectedParentAction}
<ParentActionView bind:profile={selectedProfiles[selectedDevice]} />
{/if}
{#each Object.entries(devices) as [id, device]}
{#if device && selectedProfiles[id]}
<DeviceView bind:device bind:profile={selectedProfiles[id]} bind:selectedDevice />
{/if}
{/each}
{:else}
<NoDevicesDetected />
{/if}
{#if selectedProfiles[selectedDevice]}
<PropertyInspectorView bind:device={devices[selectedDevice]} bind:profile={selectedProfiles[selectedDevice]} />
{/if}
</div>
<div class="flex flex-col p-2 w-[18rem] h-full border-l dark:border-neutral-700">
<div class="flex flex-row space-x-2">
<PluginManager />
<SettingsView />
</div>
<hr class="my-2 border dark:border-neutral-700" />
{#if !$inspectedParentAction}
<DeviceSelector
bind:devices
bind:value={selectedDevice}
bind:selectedProfiles
bind:this={$deviceSelector}
/>
{#key selectedDevice}
{#if selectedDevice && devices[selectedDevice]}
<ProfileManager
bind:device={devices[selectedDevice]}
bind:profile={selectedProfiles[selectedDevice]}
bind:this={$profileManager}
<div class="flex flex-row h-screen">
<div class="flex flex-col grow min-w-0">
<nav class="flex flex-row justify-between items-center p-3" class:hidden={$inspectedParentAction}>
<div class="flex flex-col items-start space-y-1">
<DeviceSelector
bind:devices
bind:value={selectedDevice}
bind:selectedProfiles
bind:this={$deviceSelector}
/>
{#key selectedDevice}
{#if selectedDevice && devices[selectedDevice]}
<ProfileManager
bind:device={devices[selectedDevice]}
bind:profile={selectedProfiles[selectedDevice]}
bind:this={$profileManager}
/>
{/if}
{/key}
</div>
<div class="flex flex-row items-center space-x-2">
<PluginManager />
<SettingsView />
</div>
</nav>
{#if Object.keys(devices).length > 0 && selectedProfiles}
{#if $inspectedParentAction}
<ParentActionView bind:profile={selectedProfiles[selectedDevice]} />
{/if}
{/key}
{/if}
{#each Object.entries(devices) as [id, device]}
{#if device && selectedProfiles[id]}
<DeviceView bind:device bind:profile={selectedProfiles[id]} bind:selectedDevice />
{/if}
{/each}
{#if selectedProfiles[selectedDevice]}
<PropertyInspectorView bind:device={devices[selectedDevice]} bind:profile={selectedProfiles[selectedDevice]} />
{/if}
{:else}
<NoDevicesDetected />
{/if}
</div>
<ActionList bind:this={$actionList} />
</div>