mirror of
https://github.com/nekename/OpenDeck.git
synced 2026-10-28 09:44:01 +00:00
feat: move to a navbar-based layout on the home screen for easier navigation
This commit is contained in:
11 files changed
+206
-113
No files matched your search
+47
-5
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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]}
|
||||
|
||||
@@ -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)}
|
||||
>
|
||||
|
||||
@@ -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)}
|
||||
>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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])}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,6 +2,4 @@
|
||||
import "../app.css";
|
||||
</script>
|
||||
|
||||
<div class="flex flex-row h-screen">
|
||||
<slot />
|
||||
</div>
|
||||
<slot />
|
||||
+44
-41
@@ -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>
|
||||
Reference in new issue
Block a user