清理项目没用的代码,新增 EQ 预设管理界面
This commit is contained in:
@@ -1,17 +0,0 @@
|
||||
export { COOKIE_NAME, ONE_YEAR_MS } from "@shared/const";
|
||||
|
||||
// Generate login URL at runtime so redirect URI reflects the current origin.
|
||||
export const getLoginUrl = () => {
|
||||
const oauthPortalUrl = import.meta.env.VITE_OAUTH_PORTAL_URL;
|
||||
const appId = import.meta.env.VITE_APP_ID;
|
||||
const redirectUri = `${window.location.origin}/api/oauth/callback`;
|
||||
const state = btoa(redirectUri);
|
||||
|
||||
const url = new URL(`${oauthPortalUrl}/app-auth`);
|
||||
url.searchParams.set("appId", appId);
|
||||
url.searchParams.set("redirectUri", redirectUri);
|
||||
url.searchParams.set("state", state);
|
||||
url.searchParams.set("type", "signIn");
|
||||
|
||||
return url.toString();
|
||||
};
|
||||
@@ -308,6 +308,12 @@
|
||||
"toastNewEqOk": "New headphone EQ saved",
|
||||
"toastTargetSelected": "Target selected: {{name}}",
|
||||
"deleteConfirm": "Delete headphone \"{{name}}\"?",
|
||||
"batchDeleteConfirm": "Delete {{count}} selected preset(s)?",
|
||||
"deletePresetConfirm": "Delete preset \"{{name}}\"?",
|
||||
"toastRenamed": "Renamed",
|
||||
"toastRenameFail": "Rename failed",
|
||||
"renameEmptyName": "Name cannot be empty",
|
||||
"renameNameExists": "Name already exists",
|
||||
"toastDeleted": "Headphone removed",
|
||||
"toastDeleteFail": "Failed to delete headphone",
|
||||
"bassBoost": "Bass boost: fc {{fc}}, q {{q}}, gain {{gain}} dB",
|
||||
|
||||
@@ -308,6 +308,12 @@
|
||||
"toastNewEqOk": "新耳機 EQ 已上報",
|
||||
"toastTargetSelected": "已選目標:{{name}}",
|
||||
"deleteConfirm": "是否刪除耳機「{{name}}」?",
|
||||
"batchDeleteConfirm": "是否刪除選中的 {{count}} 個預設?",
|
||||
"deletePresetConfirm": "是否刪除預設「{{name}}」?",
|
||||
"toastRenamed": "已重新命名",
|
||||
"toastRenameFail": "重新命名失敗",
|
||||
"renameEmptyName": "名稱不能為空",
|
||||
"renameNameExists": "名稱已存在",
|
||||
"toastDeleted": "已刪除耳機",
|
||||
"toastDeleteFail": "刪除耳機失敗",
|
||||
"bassBoost": "低頻增強:fc {{fc}},q {{q}},gain {{gain}}dB",
|
||||
|
||||
@@ -308,6 +308,12 @@
|
||||
"toastNewEqOk": "新耳机 EQ 已上报",
|
||||
"toastTargetSelected": "已选目标:{{name}}",
|
||||
"deleteConfirm": "是否删除耳机「{{name}}」?",
|
||||
"batchDeleteConfirm": "是否删除选中的 {{count}} 个预设?",
|
||||
"deletePresetConfirm": "是否删除预设「{{name}}」?",
|
||||
"toastRenamed": "已重命名",
|
||||
"toastRenameFail": "重命名失败",
|
||||
"renameEmptyName": "名称不能为空",
|
||||
"renameNameExists": "名称已存在",
|
||||
"toastDeleted": "已删除耳机",
|
||||
"toastDeleteFail": "删除耳机失败",
|
||||
"bassBoost": "低频增强:fc {{fc}},q {{q}},gain {{gain}}dB",
|
||||
|
||||
@@ -20,6 +20,7 @@ import { FeatureGate } from "@/components/FeatureGate";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
fetchLuxsinAudioCurve,
|
||||
normalizePeqFiltersForSubmit,
|
||||
type PeqFilter,
|
||||
type PeqApplyPayload,
|
||||
type PeqChangePayload,
|
||||
@@ -39,6 +40,7 @@ import { BatchEditDialog } from "./eq/components/BatchEditDialog";
|
||||
import { ShareDialog } from "./eq/components/ShareDialog";
|
||||
import { BrandDrawer } from "./eq/components/BrandDrawer";
|
||||
import { PeqOverwriteConfirmDialog } from "./eq/components/PeqOverwriteConfirmDialog";
|
||||
import { PeqPresetManageDialog } from "./eq/components/PeqPresetManageDialog";
|
||||
import { useRawCurve } from "./eq/hooks/useRawCurve";
|
||||
import {
|
||||
BAND_FREQ_MAX,
|
||||
@@ -151,6 +153,7 @@ export default function EQPage() {
|
||||
const [brandDrawerKey, setBrandDrawerKey] = useState(0);
|
||||
const [isShareDialogOpen, setIsShareDialogOpen] = useState(false);
|
||||
const [shareDialogKey, setShareDialogKey] = useState(0);
|
||||
const [isPresetManageDialogOpen, setIsPresetManageDialogOpen] = useState(false);
|
||||
const [overwritePresetDialog, setOverwritePresetDialog] = useState<{
|
||||
name: string;
|
||||
onConfirm: () => void | Promise<void>;
|
||||
@@ -297,6 +300,10 @@ export default function EQPage() {
|
||||
setIsBatchEditDialogOpen(true);
|
||||
};
|
||||
|
||||
const openPresetManageDialog = () => {
|
||||
setIsPresetManageDialogOpen(true);
|
||||
};
|
||||
|
||||
const parseBatchEditText = useCallback(
|
||||
(raw: string) => {
|
||||
const t = eqUi;
|
||||
@@ -1406,6 +1413,13 @@ export default function EQPage() {
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
className="w-9 h-9 rounded-full flex items-center justify-center text-white/60 active:text-white transition-colors"
|
||||
style={{ background: "rgba(44,44,46,0.8)", border: "1px solid rgba(255,255,255,0.1)" }}
|
||||
onClick={openPresetManageDialog}
|
||||
>
|
||||
<Edit3 size={15} />
|
||||
</button>
|
||||
<button
|
||||
className="w-9 h-9 rounded-full flex items-center justify-center text-white/60 active:text-white transition-colors"
|
||||
style={{ background: "rgba(44,44,46,0.8)", border: "1px solid rgba(255,255,255,0.1)" }}
|
||||
@@ -1812,6 +1826,82 @@ export default function EQPage() {
|
||||
}}
|
||||
/>
|
||||
|
||||
<PeqPresetManageDialog
|
||||
open={isPresetManageDialogOpen}
|
||||
items={peqItems}
|
||||
selectedIdx={headphoneIdx}
|
||||
onClose={() => setIsPresetManageDialogOpen(false)}
|
||||
eqUi={eqUi}
|
||||
onDeletePresets={async (names) => {
|
||||
try {
|
||||
if (isDemoMode || !api) {
|
||||
const nextItems = peqItems.filter(it => !names.includes(it.name));
|
||||
names.forEach(n => delete peqPresetCacheRef.current[n]);
|
||||
applyPeqStateToUI({ peq: nextItems, peqSelect: Math.min(headphoneIdx, Math.max(0, nextItems.length - 1)) });
|
||||
toast.success(eqUi.toastDeleted);
|
||||
return true;
|
||||
}
|
||||
names.forEach(n => delete peqPresetCacheRef.current[n]);
|
||||
await api.removePeq(names);
|
||||
const latest = await fetchEqSyncPeq(api, "deletePresets");
|
||||
applyPeqStateToUI(latest);
|
||||
toast.success(eqUi.toastDeleted);
|
||||
return true;
|
||||
} catch {
|
||||
toast.error(eqUi.toastDeleteFail);
|
||||
return false;
|
||||
}
|
||||
}}
|
||||
onRenamePreset={async (oldName, newName, item) => {
|
||||
try {
|
||||
const rawFilters = normalizePeqFiltersForSubmit(item.filters);
|
||||
const filters = rawFilters.map(f => ({
|
||||
...f,
|
||||
type: getFilterType(f.type),
|
||||
}));
|
||||
const payload: PeqChangePayload = {
|
||||
peqChange: {
|
||||
name: newName,
|
||||
filters,
|
||||
autoPre: item.autoPre,
|
||||
preamp: item.preamp,
|
||||
canDel: item.canDel ?? 1,
|
||||
brand: item.brand,
|
||||
model: item.model,
|
||||
target: item.target,
|
||||
form: item.form,
|
||||
},
|
||||
};
|
||||
|
||||
if (isDemoMode || !api) {
|
||||
const nextItems = peqItems.map(it =>
|
||||
it.name === oldName ? { ...it, name: newName } : it
|
||||
);
|
||||
delete peqPresetCacheRef.current[oldName];
|
||||
applyPeqStateToUI({ peq: nextItems, peqSelect: headphoneIdx });
|
||||
toast.success(eqUi.toastRenamed);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Step 1: peqChange — create new preset with new name + same EQ data
|
||||
// Use api.upgradePeqChange directly (NOT the DeviceContext wrapper)
|
||||
// to avoid applyPeqFiltersToState side-effect that overwrites current UI bands.
|
||||
await api.upgradePeqChange(payload);
|
||||
// Step 2: peqRemove — delete the old preset
|
||||
delete peqPresetCacheRef.current[oldName];
|
||||
await api.removePeq([oldName]);
|
||||
// Step 3: sync latest state from device
|
||||
const latest = await fetchEqSyncPeq(api, "renamePreset");
|
||||
applyPeqStateToUI(latest);
|
||||
toast.success(eqUi.toastRenamed);
|
||||
return true;
|
||||
} catch {
|
||||
toast.error(eqUi.toastRenameFail);
|
||||
return false;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<ShareDialog
|
||||
key={`share-dialog-${shareDialogKey}`}
|
||||
open={isShareDialogOpen}
|
||||
|
||||
@@ -0,0 +1,350 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { X, Trash2, Pencil, Check, Loader2 } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { toast } from "sonner";
|
||||
import type { PeqEqUi } from "../types";
|
||||
|
||||
/* ── Custom checkbox ── */
|
||||
function Checkbox({
|
||||
checked,
|
||||
onChange,
|
||||
className,
|
||||
}: {
|
||||
checked: boolean;
|
||||
onChange: () => void;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onChange}
|
||||
className={cn(
|
||||
"shrink-0 flex items-center justify-center w-[18px] h-[18px] rounded-[5px] border transition-all duration-200 active:scale-90 outline-none focus-visible:ring-2 focus-visible:ring-[#00FFF6]/40",
|
||||
checked
|
||||
? "border-transparent bg-gradient-to-br from-[#00FFF6] to-[#00C8C0] shadow-[0_0_8px_rgba(0,255,246,0.35)]"
|
||||
: "border-white/25 bg-white/[0.04] hover:border-white/40 hover:bg-white/[0.08]",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Check
|
||||
size={12}
|
||||
strokeWidth={3}
|
||||
className={cn(
|
||||
"text-black transition-all duration-200",
|
||||
checked ? "opacity-100 scale-100" : "opacity-0 scale-50"
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
type PeqItem = {
|
||||
name: string;
|
||||
filters?: any[] | string;
|
||||
autoPre?: number;
|
||||
preamp?: number;
|
||||
canDel?: number;
|
||||
brand?: string;
|
||||
model?: string;
|
||||
target?: string;
|
||||
form?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Preset management dialog.
|
||||
*
|
||||
* Behavior:
|
||||
* - Delete (single / batch): calls onDeletePresets callback with preset names,
|
||||
* which should call info.cgi with { peqRemove: names } then sync.
|
||||
* - Rename: calls onRenamePreset(oldName, newName, item) callback which
|
||||
* should: 1) peqChange with new name + same EQ data 2) peqRemove old name.
|
||||
*/
|
||||
export function PeqPresetManageDialog({
|
||||
open,
|
||||
items,
|
||||
selectedIdx,
|
||||
onClose,
|
||||
eqUi,
|
||||
onDeletePresets,
|
||||
onRenamePreset,
|
||||
}: {
|
||||
open: boolean;
|
||||
items: PeqItem[];
|
||||
selectedIdx: number;
|
||||
onClose: () => void;
|
||||
eqUi: PeqEqUi;
|
||||
/**
|
||||
* Called with preset names to delete. Should call api.removePeq(names) then
|
||||
* fetchEqSyncPeq + applyPeqStateToUI. Return true on success, false on failure.
|
||||
*/
|
||||
onDeletePresets: (names: string[]) => Promise<boolean>;
|
||||
/**
|
||||
* Called to rename a preset. Implementation should:
|
||||
* 1) upgradePeqChange({ peqChange: { name: newName, filters, ...rest } })
|
||||
* 2) api.removePeq([oldName])
|
||||
* 3) fetchEqSyncPeq + applyPeqStateToUI
|
||||
* Return true on success, false on failure.
|
||||
*/
|
||||
onRenamePreset: (oldName: string, newName: string, item: PeqItem) => Promise<boolean>;
|
||||
}) {
|
||||
const [checked, setChecked] = useState<Record<number, boolean>>({});
|
||||
const [editingIdx, setEditingIdx] = useState<number | null>(null);
|
||||
const [editingName, setEditingName] = useState("");
|
||||
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [renaming, setRenaming] = useState(false);
|
||||
|
||||
const checkedIdxs = useMemo(
|
||||
() =>
|
||||
Object.entries(checked)
|
||||
.filter(([, v]) => v)
|
||||
.map(([k]) => Number(k)),
|
||||
[checked]
|
||||
);
|
||||
|
||||
const allChecked = items.length > 0 && checkedIdxs.length === items.length;
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
const toggleAll = () => {
|
||||
if (allChecked) {
|
||||
setChecked({});
|
||||
return;
|
||||
}
|
||||
const next: Record<number, boolean> = {};
|
||||
for (let i = 0; i < items.length; i++) next[i] = true;
|
||||
setChecked(next);
|
||||
};
|
||||
|
||||
const toggleOne = (idx: number) => {
|
||||
setChecked(prev => ({ ...prev, [idx]: !prev[idx] }));
|
||||
};
|
||||
|
||||
const beginRename = (idx: number) => {
|
||||
const name = items[idx]?.name ?? "";
|
||||
setEditingIdx(idx);
|
||||
setEditingName(name);
|
||||
};
|
||||
|
||||
const commitRename = async () => {
|
||||
if (editingIdx === null) return;
|
||||
const nextName = editingName.trim();
|
||||
if (!nextName) {
|
||||
toast.error(eqUi.renameEmptyName);
|
||||
return;
|
||||
}
|
||||
const oldItem = items[editingIdx];
|
||||
if (!oldItem) return;
|
||||
if (nextName === oldItem.name) {
|
||||
setEditingIdx(null);
|
||||
setEditingName("");
|
||||
return;
|
||||
}
|
||||
if (items.some((it, i) => i !== editingIdx && it.name === nextName)) {
|
||||
toast.error(eqUi.renameNameExists);
|
||||
return;
|
||||
}
|
||||
|
||||
setRenaming(true);
|
||||
try {
|
||||
const ok = await onRenamePreset(oldItem.name, nextName, oldItem);
|
||||
if (ok) {
|
||||
setEditingIdx(null);
|
||||
setEditingName("");
|
||||
}
|
||||
} finally {
|
||||
setRenaming(false);
|
||||
}
|
||||
};
|
||||
|
||||
const confirmAndDelete = async (idxs: number[]) => {
|
||||
const uniq = Array.from(new Set(idxs)).filter(
|
||||
i => i >= 0 && i < items.length
|
||||
);
|
||||
if (uniq.length === 0) return;
|
||||
|
||||
const names = uniq.map(i => items[i].name);
|
||||
|
||||
// Confirmation dialog
|
||||
const confirmMsg =
|
||||
names.length === 1
|
||||
? eqUi.deletePresetConfirm.replace("{{name}}", names[0])
|
||||
: eqUi.batchDeleteConfirm.replace("{{count}}", String(names.length));
|
||||
if (!window.confirm(confirmMsg)) return;
|
||||
|
||||
setDeleting(true);
|
||||
try {
|
||||
const ok = await onDeletePresets(names);
|
||||
if (ok) {
|
||||
setChecked({});
|
||||
if (editingIdx !== null && uniq.includes(editingIdx)) {
|
||||
setEditingIdx(null);
|
||||
setEditingName("");
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
setDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[140] flex items-center justify-center bg-black/65 px-4">
|
||||
<div
|
||||
className="w-full max-w-[520px] rounded-[14px] p-5"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(180deg, rgba(34,36,40,0.98) 0%, rgba(24,26,30,0.98) 100%)",
|
||||
border: "1px solid rgba(255,255,255,0.12)",
|
||||
boxShadow: "0 18px 48px rgba(0,0,0,0.55)",
|
||||
}}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-[18px] font-semibold leading-tight text-white/90">
|
||||
管理 EQ 预设
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-full p-1.5 text-white/45 transition-colors hover:bg-white/10 hover:text-white/80"
|
||||
onClick={onClose}
|
||||
aria-label="Close"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-3 mb-3">
|
||||
<label className="flex items-center gap-2.5 text-[13px] text-white/70 select-none cursor-pointer">
|
||||
<Checkbox checked={allChecked} onChange={toggleAll} />
|
||||
全选
|
||||
</label>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
disabled={checkedIdxs.length === 0 || deleting}
|
||||
className={cn(
|
||||
"rounded-full px-3 py-1.5 text-[13px] font-semibold transition-all active:scale-[0.98] flex items-center gap-1.5",
|
||||
checkedIdxs.length === 0 || deleting
|
||||
? "bg-white/10 text-white/30 cursor-not-allowed"
|
||||
: "bg-red-500/15 text-red-300 hover:bg-red-500/20"
|
||||
)}
|
||||
onClick={() => void confirmAndDelete(checkedIdxs)}
|
||||
>
|
||||
{deleting && <Loader2 size={13} className="animate-spin" />}
|
||||
批量删除
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="rounded-[10px] overflow-hidden max-h-[46vh] overflow-y-auto"
|
||||
style={{
|
||||
background: "rgba(10,12,16,0.98)",
|
||||
border: "1px solid rgba(0,255,246,0.22)",
|
||||
boxShadow: "0 8px 20px rgba(0,0,0,0.35)",
|
||||
}}
|
||||
>
|
||||
{items.length === 0 ? (
|
||||
<div className="py-10 text-center text-[13px] text-white/30">
|
||||
暂无预设
|
||||
</div>
|
||||
) : (
|
||||
items.map((item, idx) => {
|
||||
const active = idx === selectedIdx;
|
||||
const isEditing = idx === editingIdx;
|
||||
return (
|
||||
<div
|
||||
key={`${item.name}-${idx}`}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-3 py-2.5 border-b border-white/[0.06] last:border-b-0",
|
||||
active ? "bg-white/[0.06]" : ""
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
checked={!!checked[idx]}
|
||||
onChange={() => toggleOne(idx)}
|
||||
/>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
{isEditing ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
value={editingName}
|
||||
onChange={e => setEditingName(e.target.value)}
|
||||
className="h-9 min-w-0 flex-1 rounded-[10px] border border-white/12 bg-[#15171b] px-3 text-[14px] text-white/90 outline-none placeholder:text-white/30 focus:border-[#00FFF6]/40"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
disabled={renaming}
|
||||
className={cn(
|
||||
"rounded-full px-3 py-1.5 text-[13px] font-semibold active:scale-[0.98] flex items-center gap-1.5",
|
||||
renaming
|
||||
? "bg-[#00FFF6]/40 text-black/50 cursor-not-allowed"
|
||||
: "bg-[#00FFF6] text-black"
|
||||
)}
|
||||
onClick={() => void commitRename()}
|
||||
>
|
||||
{renaming && <Loader2 size={13} className="animate-spin" />}
|
||||
保存
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={renaming}
|
||||
className={cn(
|
||||
"rounded-full px-3 py-1.5 text-[13px] font-medium bg-white/10 text-white/70 active:scale-[0.98]",
|
||||
renaming && "cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={() => {
|
||||
setEditingIdx(null);
|
||||
setEditingName("");
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="truncate text-[13px] text-white/85"
|
||||
title={item.name}
|
||||
>
|
||||
{item.name}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!isEditing && (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="w-8 h-8 rounded-[8px] flex items-center justify-center text-white/70 active:scale-95 transition-transform"
|
||||
style={{
|
||||
background: "rgba(255,255,255,0.06)",
|
||||
border: "1px solid rgba(255,255,255,0.1)",
|
||||
}}
|
||||
title="重命名"
|
||||
onClick={() => beginRename(idx)}
|
||||
>
|
||||
<Pencil size={14} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="w-8 h-8 rounded-[8px] flex items-center justify-center text-red-300/80 active:scale-95 transition-transform"
|
||||
style={{
|
||||
background: "rgba(239,68,68,0.08)",
|
||||
border: "1px solid rgba(239,68,68,0.2)",
|
||||
}}
|
||||
title="删除"
|
||||
onClick={() => void confirmAndDelete([idx])}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -12,7 +12,6 @@
|
||||
"build:test": "pnpm build:test:legacy && pnpm build:test:modern",
|
||||
"build:test:modern": "DEPLOY_ENV=test vite build",
|
||||
"build:test:legacy": "DEPLOY_ENV=test vite build --config vite.legacy.config.ts",
|
||||
"start": "NODE_ENV=production tsx server/index.ts",
|
||||
"preview": "vite preview --host",
|
||||
"check": "tsc --noEmit",
|
||||
"format": "prettier --write ."
|
||||
@@ -51,7 +50,6 @@
|
||||
"cmdk": "^1.1.1",
|
||||
"echarts": "^6.0.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"express": "^4.21.2",
|
||||
"framer-motion": "^12.23.22",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.453.0",
|
||||
@@ -75,7 +73,6 @@
|
||||
"@builder.io/vite-plugin-jsx-loc": "^0.1.1",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@tailwindcss/vite": "^4.1.3",
|
||||
"@types/express": "4.17.21",
|
||||
"@types/google.maps": "^3.58.1",
|
||||
"@types/node": "^24.7.0",
|
||||
"@types/react": "^19.2.1",
|
||||
@@ -90,7 +87,6 @@
|
||||
"tailwindcss": "^4.1.14",
|
||||
"tailwindcss3": "npm:tailwindcss@^3.4.17",
|
||||
"terser": "^5.46.1",
|
||||
"tsx": "^4.19.1",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "5.6.3",
|
||||
"vite": "^7.1.7",
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
diff --git a/esm/index.js b/esm/index.js
|
||||
index c83bc63a2c10431fb62e25b7d490656a3796f301..bcae513cc20a4be6c38dc116e0b8d9bacda62b5b 100644
|
||||
--- a/esm/index.js
|
||||
+++ b/esm/index.js
|
||||
@@ -338,6 +338,23 @@ const Switch = ({ children, location }) => {
|
||||
const router = useRouter();
|
||||
const [originalLocation] = useLocationFromRouter(router);
|
||||
|
||||
+ // Collect all route paths to window object
|
||||
+ if (typeof window !== 'undefined') {
|
||||
+ if (!window.__WOUTER_ROUTES__) {
|
||||
+ window.__WOUTER_ROUTES__ = [];
|
||||
+ }
|
||||
+
|
||||
+ const allChildren = flattenChildren(children);
|
||||
+ allChildren.forEach((element) => {
|
||||
+ if (isValidElement(element) && element.props.path) {
|
||||
+ const path = element.props.path;
|
||||
+ if (!window.__WOUTER_ROUTES__.includes(path)) {
|
||||
+ window.__WOUTER_ROUTES__.push(path);
|
||||
+ }
|
||||
+ }
|
||||
+ });
|
||||
+ }
|
||||
+
|
||||
for (const element of flattenChildren(children)) {
|
||||
let match = 0;
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import express from "express";
|
||||
import { createServer } from "http";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
async function startServer() {
|
||||
const app = express();
|
||||
const server = createServer(app);
|
||||
|
||||
const modernPath = path.resolve(__dirname, "..", "dist-modern");
|
||||
const legacyPath = path.resolve(__dirname, "..", "dist-legacy");
|
||||
|
||||
app.use("/modern", express.static(modernPath));
|
||||
app.use("/legacy", express.static(legacyPath));
|
||||
|
||||
function shouldUseLegacy(req: express.Request) {
|
||||
// Prefer capability detection on client, but keep a UA escape hatch for very old WebViews.
|
||||
const ua = req.headers["user-agent"] || "";
|
||||
// Chrome 91 WebView frequently reports like: Chrome/91.0.4472.114
|
||||
const m = String(ua).match(/Chrome\/(\d+)\./);
|
||||
if (m) return Number(m[1]) <= 91;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Bootstrap entry: redirect to /legacy or /modern while preserving hash routes.
|
||||
app.get(["/", "/index.html", "/i.html"], (req, res) => {
|
||||
const defaultToLegacy = shouldUseLegacy(req) ? "true" : "false";
|
||||
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
||||
res.end(`<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1" />
|
||||
<title>Luxsin X8 Controller</title>
|
||||
<script>
|
||||
(function () {
|
||||
function supportsModernCss() {
|
||||
try {
|
||||
return (
|
||||
typeof CSS !== "undefined" &&
|
||||
CSS.supports("color", "color-mix(in srgb, red 50%, blue)") &&
|
||||
CSS.supports("height", "100dvh")
|
||||
);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
var useLegacy = ${defaultToLegacy} || !supportsModernCss();
|
||||
var prefix = useLegacy ? "/x8/v2/legacy/" : "/x8/v2/modern/";
|
||||
// Hash router: keep location.hash
|
||||
location.replace(prefix + (location.hash || "#/"));
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>`);
|
||||
});
|
||||
|
||||
// Hash routing: any other path should serve the bootstrap as well.
|
||||
app.get("*", (_req, res) => {
|
||||
res.redirect(302, "/x8/v2/");
|
||||
});
|
||||
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
server.listen(port, () => {
|
||||
console.log(`Server running on http://localhost:${port}/`);
|
||||
});
|
||||
}
|
||||
|
||||
startServer().catch(console.error);
|
||||
@@ -1,2 +0,0 @@
|
||||
export const COOKIE_NAME = "app_session_id";
|
||||
export const ONE_YEAR_MS = 1000 * 60 * 60 * 24 * 365;
|
||||
+2
-3
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"include": ["client/src/**/*", "shared/**/*", "server/**/*"],
|
||||
"include": ["client/src/**/*"],
|
||||
"exclude": ["node_modules", "build", "dist", "**/*.test.ts"],
|
||||
"compilerOptions": {
|
||||
"incremental": true,
|
||||
@@ -16,8 +16,7 @@
|
||||
"baseUrl": ".",
|
||||
"types": ["node", "vite/client"],
|
||||
"paths": {
|
||||
"@/*": ["./client/src/*"],
|
||||
"@shared/*": ["./shared/*"]
|
||||
"@/*": ["./client/src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,6 @@ export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(import.meta.dirname, "client", "src"),
|
||||
"@shared": path.resolve(import.meta.dirname, "shared"),
|
||||
"@assets": path.resolve(import.meta.dirname, "attached_assets"),
|
||||
},
|
||||
dedupe: ["react", "react-dom"],
|
||||
|
||||
@@ -59,7 +59,6 @@ export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(PROJECT_ROOT, "client", "src"),
|
||||
"@shared": path.resolve(PROJECT_ROOT, "shared"),
|
||||
"@assets": path.resolve(PROJECT_ROOT, "attached_assets"),
|
||||
},
|
||||
dedupe: ["react", "react-dom"],
|
||||
|
||||
Reference in New Issue
Block a user