Files
silo-server/web/src/hooks/useSettingsForm.ts
T
5afe56cfc0 feat(jellycompat): add runtime-managed Jellyfin Web compatibility (#77)
* feat(jellycompat): install web assets at runtime

* fix(jellycompat): recover stale web operation locks

* fix(jellycompat): harden web component management

* feat(admin): refine compat settings and restart status

* chore(dev): add hot-reload docker compose stack

* fix(dev): include npm in hot-reload backend

* feat(admin): refine Jellyfin compatibility settings

* feat(settings): improve jellyfin proxy summary

* feat(settings): improve jellyfin web controls

* fix(settings): update jellyfin web removal status

* fix(settings): enable jellyfin web after install

* feat(jellycompat): auto-select web ui version

* test(api): update rate limit handler setup

* feat(jellycompat): refine web ui install onboarding

* fix(jellycompat): address web ui install review issues

* fix(onboarding): mirror jellyfin api runtime status

* fix(admin): remove global restart banner

* fix(settings): gate restart required tracking

* fix(jellyfin): ignore live settings for restart status

* fix(jellyfin): avoid restart for live compat settings

* fix(subtitles): normalize AI language codes

* fix(catalog): support partial title search tokens

* feat(branding): add white-label customization

* Add push relay engineering plan

- Document relay API contracts, APNs/FCM behavior, auth, storage, and ops
- Capture implementation plan, provider references, decisions, and README

---------

Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com>
2026-06-15 09:34:08 -04:00

106 lines
3.3 KiB
TypeScript

import { useState, useEffect, useCallback, useMemo } from "react";
import type { AdminSettingsConnectionCheckRequest } from "@/api/types";
import {
useAdminServerSettings,
useUpdateServerSetting,
useAdminSensitiveStatus,
} from "@/hooks/queries/admin/settings";
interface UseSettingsFormOptions {
/** Setting keys this section manages */
keys: string[];
}
export function useSettingsForm({ keys }: UseSettingsFormOptions) {
const { data: settings, isLoading } = useAdminServerSettings();
const { data: sensitiveData } = useAdminSensitiveStatus();
const updateSetting = useUpdateServerSetting();
const [localValues, setLocalValues] = useState<Record<string, string>>({});
const [dirty, setDirty] = useState<Set<string>>(new Set());
const [restartRequired, setRestartRequired] = useState(false);
// Sync from server when settings load
useEffect(() => {
if (!settings) return;
setLocalValues((prev) => {
const next = { ...prev };
for (const key of keys) {
// Only set if not dirty (user hasn't edited)
if (!dirty.has(key)) {
next[key] = settings[key] ?? "";
}
}
return next;
});
}, [settings, keys, dirty]);
const getValue = useCallback(
(key: string) => localValues[key] ?? settings?.[key] ?? "",
[localValues, settings],
);
const setValue = useCallback((key: string, value: string) => {
setLocalValues((prev) => ({ ...prev, [key]: value }));
setDirty((prev) => new Set(prev).add(key));
}, []);
const dirtyCount = dirty.size;
const dirtyKeys = useMemo(() => Array.from(dirty), [dirty]);
const isDirty = useCallback((key: string) => dirty.has(key), [dirty]);
const buildConnectionCheckRequest = useCallback(
(selectedKeys: string[] = keys): AdminSettingsConnectionCheckRequest => ({
values: Object.fromEntries(
selectedKeys.map((key) => [key, localValues[key] ?? settings?.[key] ?? ""]),
),
dirty_keys: selectedKeys.filter((key) => dirty.has(key)),
}),
[dirty, keys, localValues, settings],
);
const save = useCallback(async () => {
const promises = Array.from(dirty).map((key) =>
updateSetting.mutateAsync({ key, value: localValues[key] ?? "" }),
);
const results = await Promise.all(promises);
setDirty(new Set());
// The backend reports per key whether a restart is needed; most settings
// hot-reload. Once a restart-required key was saved, keep the banner up
// until the server actually restarts.
if (results.some((r) => r?.restart_required)) {
setRestartRequired(true);
}
}, [dirty, localValues, updateSetting]);
const discard = useCallback(() => {
if (!settings) return;
const reset: Record<string, string> = {};
for (const key of keys) {
reset[key] = settings[key] ?? "";
}
setLocalValues((prev) => ({ ...prev, ...reset }));
setDirty(new Set());
}, [settings, keys]);
const sensitiveConfigured = sensitiveData?.configured ?? [];
const sensitiveManagedByEnv = sensitiveData?.managed_by_env ?? [];
return {
isLoading,
getValue,
setValue,
dirtyCount,
dirtyKeys,
isDirty,
save,
discard,
isSaving: updateSetting.isPending,
restartRequired,
sensitiveConfigured,
sensitiveManagedByEnv,
buildConnectionCheckRequest,
};
}