Skip to content

Commit f4db9da

Browse files
authored
Merge pull request #660 from Levev/feat/config-cleanup
feat: Configuration screen code cleanup
2 parents 0c6d602 + 8c740fb commit f4db9da

5 files changed

Lines changed: 317 additions & 444 deletions

File tree

src/renderer/App.vue

Lines changed: 9 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
<template>
2-
<main class="overflow-hidden relative w-screen h-screen">
2+
<main
3+
class="overflow-hidden relative w-screen h-screen"
4+
:class="{ animationsDisabled: 'disable-animations' }"
5+
>
36
<!-- Decoration -->
47
<div
58
class="gradient-ball absolute -z-10 left-0 bottom-0 translate-x-[-50%] translate-y-[50%] w-[90vw] aspect-square opacity-15 blob-anim"
@@ -11,7 +14,6 @@
1114
<!-- Stripes for experimental -->
1215
<div
1316
v-show="wbConfig?.config.experimentalFeatures"
14-
:key="rerenderCounter"
1517
class="experimental-stripes absolute top-0 left-0 w-full h-[3rem] pointer-events-none z-[10] opacity-15 grayscale"
1618
></div>
1719

@@ -138,7 +140,7 @@
138140
{{ useRoute().name }}
139141
</h1>
140142
</div>
141-
<router-view v-slot="{ Component }" @rerender="rerenderCounter++">
143+
<router-view v-slot="{ Component }">
142144
<transition mode="out-in" name="fade">
143145
<component :is="Component" />
144146
</transition>
@@ -156,13 +158,12 @@
156158
import { RouteRecordRaw, RouterLink, useRoute, useRouter } from "vue-router";
157159
import { routes } from "./router";
158160
import { Icon } from "@iconify/vue";
159-
import { onMounted, onUnmounted, ref, useTemplateRef, watch } from "vue";
161+
import { onMounted, ref, useTemplateRef, watch, reactive, computed } from "vue";
160162
import { isInstalled } from "./lib/install";
161163
import { Winboat } from "./lib/winboat";
162164
import { openAnchorLink } from "./utils/openLink";
163165
import { WinboatConfig } from "./lib/config";
164166
import { USBManager } from "./lib/usbmanager";
165-
import { setIntervalImmediately } from "./utils/interval";
166167
import { CommonPorts, getActiveHostPort } from "./lib/containers/common";
167168
import { performAutoMigrations } from "./lib/migrate";
168169
const { BrowserWindow }: typeof import("@electron/remote") = require("@electron/remote");
@@ -179,16 +180,15 @@ let updateTimeout: NodeJS.Timeout | null = null;
179180
const manualUpdateRequired = ref(false);
180181
const MANUAL_UPDATE_TIMEOUT = 60000; // 60 seconds
181182
const updateDialog = useTemplateRef("updateDialog");
182-
// TODO: Hack for non-reactive data
183-
const rerenderCounter = ref(0);
184183
const novncURL = ref("");
185-
let animationCheckInterval: NodeJS.Timeout | null = null;
184+
185+
const animationsDisabled = computed(() => wbConfig?.config.disableAnimations);
186186
187187
onMounted(async () => {
188188
const winboatInstalled = await isInstalled();
189189
190190
if (winboatInstalled) {
191-
wbConfig = WinboatConfig.getInstance(); // Instantiate singleton class
191+
wbConfig = reactive(WinboatConfig.getInstance()); // Instantiate singleton class
192192
winboat = Winboat.getInstance(); // Instantiate singleton class
193193
USBManager.getInstance(); // Instantiate singleton class
194194
@@ -202,30 +202,6 @@ onMounted(async () => {
202202
console.log("Not installed, redirecting to setup...");
203203
$router.push("/setup");
204204
}
205-
206-
// Apply or remove disable-animations class based on config
207-
const updateAnimationClass = () => {
208-
if (wbConfig?.config.disableAnimations) {
209-
document.body.classList.add("disable-animations");
210-
console.log("Animations disabled");
211-
} else {
212-
document.body.classList.remove("disable-animations");
213-
console.log("Animations enabled");
214-
}
215-
};
216-
217-
// Poll for config changes since the Proxy doesn't trigger Vue reactivity
218-
// This is similar to how rerenderCounter is used elsewhere in the codebase
219-
// Start with undefined so that the first call will always apply the current state
220-
let lastAnimationState: boolean | undefined = undefined;
221-
animationCheckInterval = setIntervalImmediately(() => {
222-
const currentState = wbConfig?.config.disableAnimations;
223-
if (currentState !== lastAnimationState) {
224-
lastAnimationState = currentState;
225-
updateAnimationClass();
226-
rerenderCounter.value++; // Force re-render to update transitions
227-
}
228-
}, 1000); // Check every 1000ms
229205
230206
// Watch for guest server updates and show dialog
231207
watch(
@@ -250,13 +226,6 @@ onMounted(async () => {
250226
);
251227
});
252228
253-
onUnmounted(() => {
254-
// Clean up the interval when component unmounts
255-
if (animationCheckInterval) {
256-
clearInterval(animationCheckInterval);
257-
}
258-
});
259-
260229
function handleMinimize() {
261230
console.log("Minimize");
262231
window.electronAPI.minimizeWindow();
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
<template>
2+
<x-card
3+
class="flex flex-row justify-between items-center p-2 py-3 my-0 w-full backdrop-blur-xl backdrop-brightness-150 bg-neutral-800/20"
4+
>
5+
<div>
6+
<div class="flex flex-row gap-2 items-center mb-2">
7+
<Icon class="inline-flex text-violet-400 size-8" :icon="props.icon"></Icon>
8+
<h1 class="my-0 text-lg font-semibold">{{ props.title }}</h1>
9+
</div>
10+
<p class="text-neutral-400 text-[0.9rem] !pt-0 !mt-0">
11+
<slot name="desc">{{ props.desc }}</slot>
12+
</p>
13+
</div>
14+
<div class="flex flex-row gap-2 justify-center items-center">
15+
<slot v-if="props.type === 'custom'"/>
16+
<template v-else-if="props.type === 'number'">
17+
<x-button
18+
v-if="props.step"
19+
type="button"
20+
class="size-8 !p-0"
21+
@click="() => applyStep(-props.step!)"
22+
>
23+
<Icon icon="mdi:minus" class="size-4"></Icon>
24+
<x-label class="sr-only">Subtract</x-label>
25+
</x-button>
26+
<x-input
27+
class="max-w-16 text-right text-[1.1rem]"
28+
:min="props.min"
29+
:max="props.max"
30+
:value="value"
31+
v-on:keydown="(e: any) => ensureNumericInput(e)"
32+
@input="(e: any) => (value = Number(/^\d+$/.exec(e.target.value)![0] || props.min))"
33+
required
34+
/>
35+
<x-button
36+
v-if="props.step"
37+
type="button"
38+
class="size-8 !p-0"
39+
@click="() => applyStep(props.step!)"
40+
>
41+
<Icon icon="mdi:plus" class="size-4"></Icon>
42+
<x-label class="sr-only">Add</x-label>
43+
</x-button>
44+
<p class="text-neutral-100">{{ props.unit }}</p>
45+
</template>
46+
<template v-else-if="props.type === 'dropdown'">
47+
<x-select
48+
class="w-20"
49+
@change="(e: any) => (value = e.detail.newValue)"
50+
>
51+
<x-menu>
52+
<x-menuitem v-for="(opt, key) in props.options" :value="opt" :key="key" :toggled="value === opt">
53+
<x-label>{{ opt }}{{ props.unit ?? '' }}</x-label>
54+
</x-menuitem>
55+
</x-menu>
56+
</x-select>
57+
</template>
58+
<template v-else-if="props.type === 'switch'">
59+
<x-switch
60+
:toggled="value"
61+
@toggle="(_: any) => { $emit('toggle'); (value = !value) }"
62+
size="large"
63+
/>
64+
</template>
65+
</div>
66+
</x-card>
67+
</template>
68+
69+
<script setup lang="ts">
70+
import { Icon } from "@iconify/vue";
71+
72+
type PropsType = {
73+
/**
74+
* The icon displayed in the top-left corner of the card. Only accepts Iconify icon name format.
75+
* @example "fluent:folder-link-32-filled"
76+
*/
77+
icon: string;
78+
79+
/**
80+
* The title text displayed next to the icon.
81+
*/
82+
title: string;
83+
84+
/**
85+
* The description of the card. It will be displayed as a <p> tag.
86+
* In case you need more control over how the description is displayed, use the `desc` slot instead.
87+
*/
88+
desc?: string;
89+
90+
/**
91+
* Specifies the nature of the input value.
92+
* - `number`: Shows a numeric input with optional Add/Subtract buttons in case `step` is specified.
93+
* - `dropdown`: Shows a dropdown menu with values defined by the `options` prop.
94+
* - `switch`: Shows a toggle switch.
95+
* - `custom`: Shows the default slot content.
96+
*/
97+
type: "number" | "dropdown" | "switch" | "custom";
98+
99+
/**
100+
* The minimum accepted value in case the `number` type is specified.
101+
*/
102+
min?: number;
103+
104+
/**
105+
* The maximum accepted value in case the `number` type is specified.
106+
*/
107+
max?: number;
108+
109+
/**
110+
* Specifies how much the Add/Subtract buttons change the input value.
111+
* Can be omitted, in which case the buttons won't be shown.
112+
*/
113+
step?: number;
114+
115+
/**
116+
* Can be used to append some text after dropdown selections or a number input.
117+
*/
118+
unit?: string;
119+
120+
/**
121+
* Defines dropdown entries in case the `dropdown` type is specified.
122+
*/
123+
options?: any[];
124+
};
125+
126+
const props = defineProps<PropsType>();
127+
const value = defineModel("value");
128+
129+
function ensureNumericInput(e: any) {
130+
if (e.metaKey || e.ctrlKey || e.which <= 0 || e.which === 8 || e.key === "ArrowRight" || e.key === "ArrowLeft") {
131+
return;
132+
}
133+
134+
if (!/\d/.test(e.key)) {
135+
e.preventDefault();
136+
}
137+
}
138+
139+
function applyStep(step: number) {
140+
let tmp = Number.parseInt(value.value as string);
141+
142+
if (Number.isNaN(tmp)) return;
143+
144+
tmp += step;
145+
146+
if(!props.min && !props.max) {
147+
value.value = tmp;
148+
return;
149+
}
150+
151+
value.value = Math.min(Math.max(props.min ?? Number.MIN_SAFE_INTEGER, tmp), props.max ?? Number.MAX_SAFE_INTEGER);
152+
}
153+
</script>

src/renderer/lib/config.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ type WinboatVersionData = {
5050
current: WinboatVersion
5151
}
5252

53+
export enum MultiMonitorMode {
54+
None = "None",
55+
MultiMon = "MultiMon",
56+
Span = "Span"
57+
};
58+
5359
export type WinboatConfigObj = {
5460
scale: number;
5561
scaleDesktop: number;
@@ -59,7 +65,7 @@ export type WinboatConfigObj = {
5965
customApps: WinApp[];
6066
experimentalFeatures: boolean;
6167
advancedFeatures: boolean;
62-
multiMonitor: number;
68+
multiMonitor: MultiMonitorMode;
6369
rdpArgs: RdpArg[];
6470
disableAnimations: boolean;
6571
containerRuntime: ContainerRuntimes;
@@ -78,7 +84,7 @@ const defaultConfig: WinboatConfigObj = {
7884
customApps: [],
7985
experimentalFeatures: false,
8086
advancedFeatures: false,
81-
multiMonitor: 0,
87+
multiMonitor: MultiMonitorMode.None,
8288
rdpArgs: [],
8389
disableAnimations: false,
8490
// TODO: Ideally should be podman once we flesh out everything
@@ -93,15 +99,17 @@ const defaultConfig: WinboatConfigObj = {
9399
export class WinboatConfig {
94100
private static readonly configPath: string = path.join(WINBOAT_DIR, "winboat.config.json");
95101
private static instance: WinboatConfig | null = null;
96-
#configData: WinboatConfigObj = { ...defaultConfig };
102+
103+
// Due to us wrapping WinboatConfig in reactive, this can't be private
104+
configData: WinboatConfigObj = { ...defaultConfig };
97105

98106
static getInstance() {
99107
WinboatConfig.instance ??= new WinboatConfig();
100108
return WinboatConfig.instance;
101109
}
102110

103111
private constructor() {
104-
this.#configData = WinboatConfig.readConfigObject()!;
112+
this.configData = WinboatConfig.readConfigObject()!;
105113

106114
// Set correct versionData
107115
if(this.config.versionData.current.versionToken !== currentVersion.versionToken) {
@@ -111,12 +119,12 @@ export class WinboatConfig {
111119
logger.info(`Updated version data from '${this.config.versionData.previous.toString()}' to '${currentVersion.toString()}'`);
112120
}
113121

114-
console.log("Reading current config", this.#configData);
122+
console.log("Reading current config", this.configData);
115123
}
116124

117125
get config(): WinboatConfigObj {
118126
// Return a proxy to intercept property sets
119-
return new Proxy(this.#configData, {
127+
return new Proxy(this.configData, {
120128
get: (target, key) => Reflect.get(target, key),
121129
set: (target, key, value: WinboatConfigObj) => {
122130
const result = Reflect.set(target, key, value);
@@ -130,7 +138,7 @@ export class WinboatConfig {
130138
}
131139

132140
set config(newConfig: WinboatConfigObj) {
133-
this.#configData = { ...newConfig };
141+
this.configData = { ...newConfig };
134142
WinboatConfig.writeConfigObject(newConfig);
135143
console.info("Wrote modified config to disk");
136144
}

src/renderer/lib/winboat.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import YAML from "yaml";
1414
import { InternalApps } from "../data/internalapps";
1515
import { getFreeRDP } from "../utils/getFreeRDP";
1616
import { openLink } from "../utils/openLink";
17-
import { WinboatConfig } from "./config";
17+
import { MultiMonitorMode, WinboatConfig } from "./config";
1818
import { QMPManager } from "./qmp";
1919
import { assert } from "@vueuse/core";
2020
import { setIntervalImmediately } from "../utils/interval";
@@ -644,9 +644,9 @@ export class Winboat {
644644
]);
645645
} else {
646646
args = args.concat([
647-
this.#wbConfig?.config.multiMonitor == 2 ? "+span" : "",
647+
this.#wbConfig?.config.multiMonitor === MultiMonitorMode.Span ? "+span" : "",
648648
"-wallpaper",
649-
this.#wbConfig?.config.multiMonitor == 1 ? "/multimon" : "",
649+
this.#wbConfig?.config.multiMonitor === MultiMonitorMode.MultiMon ? "/multimon" : "",
650650
`/scale-desktop:${this.#wbConfig?.config.scaleDesktop ?? 100}`,
651651
`/wm-class:winboat-${cleanAppName}`,
652652
`/app:program:${app.Path},name:${cleanAppName},cmd:"${app.Args}"`,

0 commit comments

Comments
 (0)