Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/build-server.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,23 @@ jobs:
cd client
npm run build

- name: Setup Node.js (V3)
uses: actions/setup-node@v4
with:
node-version: '24'
cache: 'npm'
cache-dependency-path: 'client-v3/package-lock.json'

- name: Install dependencies (V3)
run: |
cd client-v3
npm ci

- name: Build frontend (V3)
run: |
cd client-v3
npm run build

- name: Upload frontend build
uses: actions/upload-artifact@v4
with:
Expand Down
20 changes: 14 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
FROM node:24-bookworm AS node_build
FROM node:24-bookworm AS build_v2

# npm 11 bundled with Node 24, no separate install needed
RUN mkdir -p /server/static

COPY /client/package.json /client/package.json
COPY /client/package-lock.json /client/package-lock.json
COPY /client/.npmrc /client/.npmrc
Expand All @@ -12,7 +10,15 @@ COPY /client /client
COPY /docs /docs
RUN npm run build

COPY /server /server
FROM node:24-bookworm AS build_v3

RUN mkdir -p /server/static/ui-new
COPY /client-v3/package.json /client-v3/package.json
COPY /client-v3/package-lock.json /client-v3/package-lock.json
WORKDIR /client-v3
RUN npm ci
COPY /client-v3 /client-v3
RUN npm run build

FROM python:3.13-bookworm

Expand All @@ -22,8 +28,10 @@ RUN pip install -r requirements.txt
RUN apt update
RUN apt install -y nano

COPY --from=node_build /server /server
COPY /server /server
COPY --from=build_v2 /server/static /server/static
COPY --from=build_v3 /server/static/ui-new /server/static/ui-new
WORKDIR /server
RUN mkdir conf
EXPOSE 8080
ENTRYPOINT ["python3", "main.py"]
ENTRYPOINT ["python3", "main.py"]
10 changes: 10 additions & 0 deletions client-v3/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
</BNavItem>
</BNavbarNav>
<BNavbarNav class="ms-auto">
<BNavItem href="/?_switch=1"> Switch to Classic UI </BNavItem>
<BNavItem to="/help"> Help </BNavItem>
<BNavItem to="/about"> About </BNavItem>
<BNavItemDropdown v-if="isElectronEnv" text="Server">
Expand Down Expand Up @@ -193,6 +194,7 @@ import { useConfirm } from '@/composables/useConfirm';
import ConfirmDialog from '@/components/common/ConfirmDialog.vue';
import CreateUser from '@/components/user/CreateUser.vue';
import { useUserStore } from '@/stores/user';
import type { UserSettings } from '@/types/api/user';
import { useSystemStore } from '@/stores/system';
import { useShowStore } from '@/stores/show';
import { useWebSocketStore } from '@/stores/websocket';
Expand Down Expand Up @@ -275,6 +277,14 @@ async function awaitWSConnect(): Promise<void> {
if (userStore.authToken) {
await userStore.getCurrentUser();
await Promise.all([userStore.getCurrentRbac(), userStore.getUserSettings()]);
const switching = new URLSearchParams(window.location.search).has('_switch');
if (!switching && (userStore.userSettings as UserSettings).preferred_ui === 'old') {
window.location.href = '/?_switch=1';
return;
}
if (switching) {
await router.replace(router.currentRoute.value.path);
}
}
if (systemStore.currentShow != null) {
await showStore.getShowSessionData();
Expand Down
28 changes: 26 additions & 2 deletions client-v3/src/components/user/settings/UserSettingsConfig.vue
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,18 @@
/>
</BFormGroup>

<BFormGroup label-cols="4" label="Preferred UI Version" label-for="preferred-ui-input">
<BFormSelect
id="preferred-ui-input"
v-model="state.preferred_ui"
name="preferred-ui-input"
:options="preferredUiOptions"
/>
</BFormGroup>

<BButtonGroup size="md" style="float: right">
<BButton type="reset" variant="danger" :disabled="!v$.$anyDirty">Reset</BButton>
<BButton type="submit" variant="primary" :disabled="!v$.$anyDirty || v$.$anyError">
<BButton type="reset" variant="danger" :disabled="!formDirty">Reset</BButton>
<BButton type="submit" variant="primary" :disabled="!formDirty || v$.$anyError">
Submit
</BButton>
</BButtonGroup>
Expand All @@ -119,6 +128,7 @@

<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue';

import { useVuelidate } from '@vuelidate/core';
import { required, integer, minValue } from '@vuelidate/validators';
import log from 'loglevel';
Expand All @@ -143,6 +153,7 @@ const defaultState = (): UserSettings => ({
console_log_level: 'WARN',
character_mru_sort: false,
character_combined_dropdown: false,
preferred_ui: null,
});

const state = ref<UserSettings>(defaultState());
Expand All @@ -162,6 +173,12 @@ const consoleLogLevelOptions = [
{ value: 'SILENT', text: 'SILENT' },
];

const preferredUiOptions = [
{ value: null, text: 'Use system default' },
{ value: 'old', text: 'Classic UI' },
{ value: 'new', text: 'New UI' },
];

const rules = computed(() => ({
enable_script_auto_save: {},
script_auto_save_interval: {
Expand All @@ -176,13 +193,19 @@ const rules = computed(() => ({
console_log_level: { required },
character_mru_sort: {},
character_combined_dropdown: {},
preferred_ui: {},
}));

const v$ = useVuelidate(rules, state);
const savedSettings = ref<UserSettings>(defaultState());
const formDirty = computed(
() => JSON.stringify(state.value) !== JSON.stringify(savedSettings.value)
);

function resetForm(): void {
loaded.value = false;
const settings = userStore.userSettings as UserSettings;
savedSettings.value = { ...defaultState(), ...settings };
state.value = { ...defaultState(), ...settings };
v$.value.$reset();
loaded.value = true;
Expand All @@ -192,6 +215,7 @@ watch(() => userStore.userSettings, resetForm, { deep: true });

onMounted(() => {
const settings = userStore.userSettings as UserSettings;
savedSettings.value = { ...defaultState(), ...settings };
state.value = { ...defaultState(), ...settings };
loaded.value = true;
});
Expand Down
1 change: 1 addition & 0 deletions client-v3/src/types/api/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ export interface UserSettings {
console_log_level: string;
character_mru_sort: boolean;
character_combined_dropdown: boolean;
preferred_ui: string | null;
}
13 changes: 13 additions & 0 deletions client/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
</b-nav-item>
</b-navbar-nav>
<b-navbar-nav class="ml-auto">
<b-nav-item href="/ui-new/?_switch=1"> Switch to New UI </b-nav-item>
<b-nav-item to="/help"> Help </b-nav-item>
<b-nav-item to="/about"> About </b-nav-item>
<b-nav-item-dropdown v-if="isElectron()" text="Server">
Expand Down Expand Up @@ -323,6 +324,18 @@ export default defineComponent({
if ((this as any).AUTH_TOKEN) {
await (this as any).GET_CURRENT_USER();
await Promise.all([(this as any).GET_CURRENT_RBAC(), (this as any).GET_USER_SETTINGS()]);
const switching = new URLSearchParams(window.location.search).has('_switch');
if (!switching) {
const userPref = (this as any).USER_SETTINGS?.preferred_ui as string | null | undefined;
const systemDefault = (this as any).SETTINGS?.default_ui as string | undefined;
if (userPref === 'new' || (userPref == null && systemDefault === 'new')) {
window.location.href = '/ui-new/';
return;
}
}
if (switching) {
await (this as any).$router.replace({ path: (this as any).$route.path });
}
}

if ((this as any).SETTINGS.current_show != null) {
Expand Down
36 changes: 32 additions & 4 deletions client/src/vue_components/user/settings/Settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,21 @@
:switch="true"
/>
</b-form-group>
<b-form-group
:label-cols="true"
label="Preferred UI Version"
label-for="preferred-ui-input"
>
<b-form-select
id="preferred-ui-input"
v-model="$v.editSettings.preferred_ui.$model"
name="preferred-ui-input"
:options="preferredUiOptions"
/>
</b-form-group>
<b-button-group size="md" style="float: right">
<b-button type="reset" variant="danger" :disabled="!$v.$anyDirty"> Reset </b-button>
<b-button type="submit" variant="primary" :disabled="!$v.$anyDirty || $v.$anyError">
<b-button type="reset" variant="danger" :disabled="!formDirty"> Reset </b-button>
<b-button type="submit" variant="primary" :disabled="!formDirty || $v.$anyError">
Submit
</b-button>
</b-button-group>
Expand Down Expand Up @@ -133,6 +145,7 @@ export default defineComponent({
data() {
return {
loaded: false,
savedSettings: null as Record<string, unknown> | null,
editSettings: {
enable_script_auto_save: false,
script_auto_save_interval: 10,
Expand All @@ -141,6 +154,7 @@ export default defineComponent({
console_log_level: 'WARN',
character_mru_sort: false,
character_combined_dropdown: false,
preferred_ui: null as string | null,
},
textAlignmentOptions: [
{ value: TEXT_ALIGNMENT.LEFT, text: 'Left' },
Expand All @@ -155,11 +169,19 @@ export default defineComponent({
{ value: 'ERROR', text: 'ERROR' },
{ value: 'SILENT', text: 'SILENT' },
],
preferredUiOptions: [
{ value: null, text: 'Use system default' },
{ value: 'old', text: 'Classic UI' },
{ value: 'new', text: 'New UI' },
],
toggle: 0,
};
},
computed: {
...mapGetters(['USER_SETTINGS']),
formDirty(): boolean {
return JSON.stringify(this.editSettings) !== JSON.stringify(this.savedSettings);
},
},
watch: {
USER_SETTINGS(): void {
Expand All @@ -184,10 +206,13 @@ export default defineComponent({
console_log_level: { required },
character_mru_sort: {},
character_combined_dropdown: {},
preferred_ui: { isValidUi: (val: unknown) => val === null || val === 'old' || val === 'new' },
},
},
mounted(): void {
this.editSettings = JSON.parse(JSON.stringify((this as any).USER_SETTINGS));
const settings = JSON.parse(JSON.stringify((this as any).USER_SETTINGS));
this.savedSettings = settings;
this.editSettings = JSON.parse(JSON.stringify(settings));
this.loaded = true;
},
methods: {
Expand All @@ -208,12 +233,15 @@ export default defineComponent({
log.error('Unable to save settings');
} else {
(this as any).$toast.success('Saved settings');
this.savedSettings = JSON.parse(JSON.stringify(this.editSettings));
}
},
resetForm(): void {
this.loaded = false;
this.toggle = Number(!this.toggle);
this.editSettings = JSON.parse(JSON.stringify((this as any).USER_SETTINGS));
const settings = JSON.parse(JSON.stringify((this as any).USER_SETTINGS));
this.savedSettings = settings;
this.editSettings = JSON.parse(JSON.stringify(settings));
this.loaded = true;
},
},
Expand Down
23 changes: 21 additions & 2 deletions client/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,29 @@
import path from 'path';
import { defineConfig } from 'vite';
import fs from 'fs';
import { defineConfig, type Plugin } from 'vite';
import vue from '@vitejs/plugin-vue2';

function cleanV2StaticPlugin(): Plugin {
return {
name: 'clean-v2-static',
buildStart() {
if (process.env.BUILD_TARGET === 'electron') return;
const outDir = path.resolve(__dirname, '../server/static');
if (fs.existsSync(outDir)) {
for (const entry of fs.readdirSync(outDir)) {
if (entry !== 'ui-new') {
fs.rmSync(path.join(outDir, entry), { recursive: true, force: true });
}
}
}
},
};
}

export default defineConfig({
plugins: [
vue(),
cleanV2StaticPlugin(),
],
define: {
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
Expand All @@ -15,7 +34,7 @@ export default defineConfig({
build: {
outDir: process.env.BUILD_TARGET === 'electron' ? './dist-electron' : '../server/static/',
assetsDir: './assets',
emptyOutDir: true,
emptyOutDir: false,
minify: 'esbuild',
cssMinify: 'esbuild',
rollupOptions: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""add preferred_ui to user_settings

Revision ID: 11311df29aa4
Revises: 4fbfeaba60ae
Create Date: 2026-05-19 23:24:51.376973

"""

from typing import Sequence, Union

import sqlalchemy as sa
from alembic import op


# revision identifiers, used by Alembic.
revision: str = "11311df29aa4"
down_revision: Union[str, None] = "4fbfeaba60ae"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("user_settings", schema=None) as batch_op:
batch_op.add_column(sa.Column("preferred_ui", sa.String(), nullable=True))
batch_op.create_check_constraint(
"ck_user_settings_preferred_ui",
"preferred_ui IS NULL OR preferred_ui IN ('old', 'new')",
)

# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("user_settings", schema=None) as batch_op:
batch_op.drop_constraint("ck_user_settings_preferred_ui", type_="check")
batch_op.drop_column("preferred_ui")

# ### end Alembic commands ###
7 changes: 6 additions & 1 deletion server/controllers/controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ def import_all_controllers():


class RootController(BaseController):
def get(self, _path):
async def get(self, path):
if not path and not self.get_argument("_switch", None):
default_ui = await self.application.digi_settings.get("default_ui")
if default_ui == "new":
self.redirect("/ui-new/")
return
if is_frozen():
# In PyInstaller mode, use resource path
full_path = get_resource_path(os.path.join("static", "index.html"))
Expand Down
Loading
Loading