Implement community forum integration in CommunityPage; update BottomNav to conditionally display community tab based on screen size. Add new audio processing function for combined PEQ magnitude calculation in peqAudio.
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
BOTTOM NAV — Luxsin X9 Controller (Mobile)
|
BOTTOM NAV — Luxsin X9 Controller (Mobile)
|
||||||
Design: iOS/Android App-style bottom tab bar
|
Design: iOS/Android App-style bottom tab bar
|
||||||
5 tabs — labels from locales/bottomNav, language = deviceState.language (同系统页)
|
5 tabs — labels from locales/bottomNav, language = deviceState.language (同系统页)
|
||||||
|
- 社区:仅桌面宽屏(lg ≥1024px)显示,手机等小屏隐藏
|
||||||
- Active tab: cyan icon + top indicator bar
|
- Active tab: cyan icon + top indicator bar
|
||||||
- Inactive: muted icon + label
|
- Inactive: muted icon + label
|
||||||
============================================================ */
|
============================================================ */
|
||||||
@@ -30,6 +31,8 @@ interface NavItem {
|
|||||||
labelKey: keyof BottomNavLocale;
|
labelKey: keyof BottomNavLocale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FORUM_URL = "https://forum.zidoo.tv/index.php";
|
||||||
|
|
||||||
const NAV_ITEMS: NavItem[] = [
|
const NAV_ITEMS: NavItem[] = [
|
||||||
{ path: "/community", icon: MessageSquare, labelKey: "community" },
|
{ path: "/community", icon: MessageSquare, labelKey: "community" },
|
||||||
{ path: "/eq", icon: Sliders, labelKey: "hpEq" },
|
{ path: "/eq", icon: Sliders, labelKey: "hpEq" },
|
||||||
@@ -51,6 +54,12 @@ export default function BottomNav() {
|
|||||||
return (pack.bottomNav ?? localeZh.bottomNav) as BottomNavLocale;
|
return (pack.bottomNav ?? localeZh.bottomNav) as BottomNavLocale;
|
||||||
}, [deviceState?.language]);
|
}, [deviceState?.language]);
|
||||||
|
|
||||||
|
/** 社区入口仅在桌面级视口显示(与 useIsLgUp / Tailwind lg 一致) */
|
||||||
|
const visibleNavItems = useMemo(
|
||||||
|
() => NAV_ITEMS.filter((item) => item.path !== "/community" || isLgUp),
|
||||||
|
[isLgUp],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav
|
<nav
|
||||||
className="fixed bottom-0 left-0 right-0 z-50 lg:left-1/2 lg:right-auto lg:w-[min(1180px,calc(100vw-3rem))] lg:-translate-x-1/2"
|
className="fixed bottom-0 left-0 right-0 z-50 lg:left-1/2 lg:right-auto lg:w-[min(1180px,calc(100vw-3rem))] lg:-translate-x-1/2"
|
||||||
@@ -64,7 +73,7 @@ export default function BottomNav() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-stretch justify-around">
|
<div className="flex items-stretch justify-around">
|
||||||
{NAV_ITEMS.map((item) => {
|
{visibleNavItems.map((item) => {
|
||||||
const isActive =
|
const isActive =
|
||||||
item.path === "/ai"
|
item.path === "/ai"
|
||||||
? location === "/ai" || (isLgUp && aiDrawerOpen)
|
? location === "/ai" || (isLgUp && aiDrawerOpen)
|
||||||
@@ -122,6 +131,19 @@ export default function BottomNav() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (item.path === "/community") {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={item.path}
|
||||||
|
type="button"
|
||||||
|
className="cursor-pointer border-0 bg-transparent p-0 text-inherit outline-none focus-visible:ring-2 focus-visible:ring-[#00FFF6]/40 focus-visible:ring-offset-2 focus-visible:ring-offset-[rgba(10,10,12,0.96)]"
|
||||||
|
onClick={() => window.open(FORUM_URL, "_blank", "noopener,noreferrer")}
|
||||||
|
>
|
||||||
|
{inner}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (item.path === "/ai") {
|
if (item.path === "/ai") {
|
||||||
if (!isLgUp) {
|
if (!isLgUp) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -359,6 +359,26 @@ export function computePeqMagnitudeDb(bands: PeqBandForResponse[], fs: number):
|
|||||||
return getFreqznList(list, fs, f);
|
return getFreqznList(list, fs, f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 级联后总幅度(dB)在某一频率上的值,与 {@link computePeqMagnitudeDb} / 黄线 EQ 曲线同源。
|
||||||
|
* 用于把 10 个拖拽点画在曲线上(曲线为各带合成响应,不能再用单带的 gain 参数当纵坐标)。
|
||||||
|
*/
|
||||||
|
export function sampleCombinedPeqMagnitudeDb(
|
||||||
|
bands: PeqBandForResponse[],
|
||||||
|
freq: number,
|
||||||
|
fs: number = 48000,
|
||||||
|
): number {
|
||||||
|
const fc = Math.max(20, Math.min(20000, freq));
|
||||||
|
const list: Coeff[] = [];
|
||||||
|
bands.forEach((b) => {
|
||||||
|
if (!b.enabled) return;
|
||||||
|
list.push(getSectionsMatrix(b.gain, b.freq, b.q, getFilterType(b.type), false, fs));
|
||||||
|
});
|
||||||
|
if (list.length === 0) return 0;
|
||||||
|
const out = getFreqznList(list, fs, [fc]);
|
||||||
|
return out[0] ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
export type PeqSvgCurveData = {
|
export type PeqSvgCurveData = {
|
||||||
points: Array<{ x: number; y: number; gainDb: number; freq: number }>;
|
points: Array<{ x: number; y: number; gainDb: number; freq: number }>;
|
||||||
pathD: string;
|
pathD: string;
|
||||||
|
|||||||
@@ -1,31 +1,22 @@
|
|||||||
import BottomNav from "@/components/BottomNav";
|
import BottomNav from "@/components/BottomNav";
|
||||||
import { MessageSquare } from "lucide-react";
|
|
||||||
|
const FORUM_URL = "https://forum.zidoo.tv";
|
||||||
|
|
||||||
export default function CommunityPage() {
|
export default function CommunityPage() {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="h-[100dvh] flex flex-col pb-[calc(env(safe-area-inset-bottom,0px)+56px)]"
|
className="h-[100dvh] flex flex-col pb-[calc(env(safe-area-inset-bottom,0px)+56px)]"
|
||||||
style={{
|
style={{
|
||||||
background:
|
background: "#04060d",
|
||||||
"radial-gradient(120% 80% at 50% 0%, rgba(12, 24, 30, 0.45) 0%, rgba(6, 9, 16, 0.92) 38%, #04060d 100%)",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<main className="relative flex-1 flex items-center justify-center px-6">
|
<iframe
|
||||||
<div className="flex flex-col items-center text-center -translate-y-8">
|
className="flex-1 w-full min-h-0 border-0 bg-white"
|
||||||
<div
|
src={FORUM_URL}
|
||||||
className="w-[96px] h-[96px] rounded-full flex items-center justify-center mb-7"
|
title="Zidoo forum"
|
||||||
style={{
|
allow="fullscreen; clipboard-read; clipboard-write"
|
||||||
background: "radial-gradient(circle at 35% 30%, rgba(0,255,246,0.18), rgba(0,255,246,0.07))",
|
referrerPolicy="strict-origin-when-cross-origin"
|
||||||
boxShadow: "0 0 34px rgba(0,255,246,0.18), inset 0 0 24px rgba(0,255,246,0.08)",
|
/>
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MessageSquare size={38} className="text-[#00FFF6]" strokeWidth={2.2} />
|
|
||||||
</div>
|
|
||||||
<p className="text-white/85 text-[42px] leading-none font-semibold tracking-wide mb-3">Coming Soon</p>
|
|
||||||
<p className="text-white/40 text-[22px] leading-none">Community features are under development</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
<BottomNav />
|
<BottomNav />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import {
|
|||||||
getFilterType,
|
getFilterType,
|
||||||
getFilterShortName,
|
getFilterShortName,
|
||||||
buildPeqSvgCurveData,
|
buildPeqSvgCurveData,
|
||||||
|
sampleCombinedPeqMagnitudeDb,
|
||||||
} from "@/lib/peqAudio";
|
} from "@/lib/peqAudio";
|
||||||
import localeZh from "@/locales/data-zh.json";
|
import localeZh from "@/locales/data-zh.json";
|
||||||
import localeZhHK from "@/locales/data-zh-HK.json";
|
import localeZhHK from "@/locales/data-zh-HK.json";
|
||||||
@@ -128,7 +129,8 @@ function FreqChart({
|
|||||||
onBandSelect: (idx: number) => void;
|
onBandSelect: (idx: number) => void;
|
||||||
eqUi: PeqEqUi;
|
eqUi: PeqEqUi;
|
||||||
}) {
|
}) {
|
||||||
const H = 140;
|
/** 频响图 SVG 高度(viewBox 与 CSS 一致) */
|
||||||
|
const H = 400;
|
||||||
const chartContainerRef = useRef<HTMLDivElement | null>(null);
|
const chartContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const svgRef = useRef<SVGSVGElement | null>(null);
|
const svgRef = useRef<SVGSVGElement | null>(null);
|
||||||
const draggingBandRef = useRef<number | null>(null);
|
const draggingBandRef = useRef<number | null>(null);
|
||||||
@@ -217,11 +219,17 @@ function FreqChart({
|
|||||||
maxFreq: 20000,
|
maxFreq: 20000,
|
||||||
paddingY: 10,
|
paddingY: 10,
|
||||||
}),
|
}),
|
||||||
[bands, W],
|
[bands, W, H],
|
||||||
);
|
);
|
||||||
const pathD = curveData.pathD;
|
const pathD = curveData.pathD;
|
||||||
const fillD = curveData.fillD;
|
const fillD = curveData.fillD;
|
||||||
|
|
||||||
|
/** 各带中心频率处级联响应 dB,与黄线同源 — 手柄纵坐标须用此值才能落在曲线上 */
|
||||||
|
const combinedMagDbAtHandles = useMemo(
|
||||||
|
() => bands.map((b) => sampleCombinedPeqMagnitudeDb(bands, b.freq, 48000)),
|
||||||
|
[bands],
|
||||||
|
);
|
||||||
|
|
||||||
const [curveVisibilityByMode, setCurveVisibilityByMode] = useState<
|
const [curveVisibilityByMode, setCurveVisibilityByMode] = useState<
|
||||||
Record<"A" | "B", { eq: boolean; raw: boolean; equalized: boolean }>
|
Record<"A" | "B", { eq: boolean; raw: boolean; equalized: boolean }>
|
||||||
>({
|
>({
|
||||||
@@ -361,7 +369,7 @@ function FreqChart({
|
|||||||
ref={svgRef}
|
ref={svgRef}
|
||||||
viewBox={`0 0 ${W} ${H}`}
|
viewBox={`0 0 ${W} ${H}`}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
style={{ height: 140, touchAction: "none" }}
|
style={{ height: H, touchAction: "none" }}
|
||||||
onPointerMove={handleSvgPointerMove}
|
onPointerMove={handleSvgPointerMove}
|
||||||
onPointerUp={stopDragging}
|
onPointerUp={stopDragging}
|
||||||
onPointerCancel={stopDragging}
|
onPointerCancel={stopDragging}
|
||||||
@@ -437,17 +445,18 @@ function FreqChart({
|
|||||||
const fillColor = isSelected ? "#FFED00" : "rgba(0,0,0,0.5)";
|
const fillColor = isSelected ? "#FFED00" : "rgba(0,0,0,0.5)";
|
||||||
const strokeColor = isSelected ? "#FFED00" : "#FFED00";
|
const strokeColor = isSelected ? "#FFED00" : "#FFED00";
|
||||||
const textColor = isSelected ? "#000000" : "#FFED00";
|
const textColor = isSelected ? "#000000" : "#FFED00";
|
||||||
|
const handleY = gainToY(combinedMagDbAtHandles[i] ?? 0);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<circle
|
<circle
|
||||||
cx={freqToX(band.freq)}
|
cx={freqToX(band.freq)}
|
||||||
cy={gainToY(band.gain)}
|
cy={handleY}
|
||||||
r="14"
|
r="14"
|
||||||
fill="transparent"
|
fill="transparent"
|
||||||
/>
|
/>
|
||||||
<circle
|
<circle
|
||||||
cx={freqToX(band.freq)}
|
cx={freqToX(band.freq)}
|
||||||
cy={gainToY(band.gain)}
|
cy={handleY}
|
||||||
r={outerR}
|
r={outerR}
|
||||||
fill={fillColor}
|
fill={fillColor}
|
||||||
stroke={strokeColor}
|
stroke={strokeColor}
|
||||||
@@ -455,7 +464,7 @@ function FreqChart({
|
|||||||
/>
|
/>
|
||||||
<text
|
<text
|
||||||
x={freqToX(band.freq)}
|
x={freqToX(band.freq)}
|
||||||
y={gainToY(band.gain) + 3.5}
|
y={handleY + 3.5}
|
||||||
textAnchor="middle"
|
textAnchor="middle"
|
||||||
fontSize="7"
|
fontSize="7"
|
||||||
fill={textColor}
|
fill={textColor}
|
||||||
|
|||||||
Reference in New Issue
Block a user