Files
desktop/src/components/white-balance/white-balance.tsx

467 lines
18 KiB
TypeScript

import { listen } from '@tauri-apps/api/event';
import { Component, createEffect, onCleanup, createSignal } from 'solid-js';
import { ColorCalibration, LedStripConfigContainer } from '../../models/led-strip-config';
import { ledStripStore, setLedStripStore } from '../../stores/led-strip.store';
import { ColorSlider } from './color-slider';
import { TestColorsBg } from './test-colors-bg';
import { invoke } from '@tauri-apps/api/core';
import { VsClose } from 'solid-icons/vs';
import { BiRegularReset } from 'solid-icons/bi';
import { BsFullscreen, BsFullscreenExit } from 'solid-icons/bs';
import { getCurrentWindow } from '@tauri-apps/api/window';
import transparentBg from '../../assets/transparent-grid-background.svg?url';
import { useLanguage } from '../../i18n/index';
const Value: Component<{ value: number }> = (props) => {
return (
<div class="badge badge-outline badge-sm font-mono">
{(props.value * 100).toFixed(0)}%
</div>
);
};
export const WhiteBalance = () => {
const { t } = useLanguage();
const [isFullscreen, setIsFullscreen] = createSignal(false);
const [panelPosition, setPanelPosition] = createSignal({ x: 0, y: 0 });
const [isDragging, setIsDragging] = createSignal(false);
const [dragOffset, setDragOffset] = createSignal({ x: 0, y: 0 });
// 自动进入全屏模式
createEffect(() => {
const autoEnterFullscreen = async () => {
try {
const window = getCurrentWindow();
const currentFullscreen = await window.isFullscreen();
if (!currentFullscreen) {
await window.setFullscreen(true);
setIsFullscreen(true);
} else {
setIsFullscreen(true);
}
} catch (error) {
// Silently handle fullscreen error
}
};
autoEnterFullscreen();
});
// 初始化面板位置到屏幕中央
createEffect(() => {
if (isFullscreen()) {
const centerX = window.innerWidth / 2 - 160; // 160是面板宽度的一半
const centerY = window.innerHeight / 2 - 200; // 200是面板高度的一半
setPanelPosition({ x: centerX, y: centerY });
}
});
// 拖拽处理函数
const handleMouseDown = (e: MouseEvent) => {
// 确保只有在标题栏区域点击时才触发拖拽
setIsDragging(true);
const panelRect = (e.currentTarget as HTMLElement).closest('.fixed')?.getBoundingClientRect();
if (panelRect) {
setDragOffset({
x: e.clientX - panelRect.left,
y: e.clientY - panelRect.top
});
}
e.preventDefault();
e.stopPropagation();
};
const handleMouseMove = (e: MouseEvent) => {
if (isDragging()) {
const newX = e.clientX - dragOffset().x;
const newY = e.clientY - dragOffset().y;
// 限制面板在屏幕范围内
const maxX = window.innerWidth - 320; // 320是面板宽度
const maxY = window.innerHeight - 400; // 400是面板高度
setPanelPosition({
x: Math.max(0, Math.min(newX, maxX)),
y: Math.max(0, Math.min(newY, maxY))
});
}
};
const handleMouseUp = () => {
setIsDragging(false);
};
// 添加全局鼠标事件监听
createEffect(() => {
if (isDragging()) {
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
} else {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
}
});
// listen to config_changed event
createEffect(() => {
const unlisten = listen('config_changed', (event) => {
const { strips, mappers, color_calibration } =
event.payload as LedStripConfigContainer;
setLedStripStore({
strips,
mappers,
colorCalibration: color_calibration,
});
});
onCleanup(async () => {
(await unlisten)();
});
});
const updateColorCalibration = (
key: keyof ColorCalibration,
value: number,
) => {
const calibration = { ...ledStripStore.colorCalibration };
calibration[key] = value;
setLedStripStore('colorCalibration', calibration);
invoke('set_color_calibration', { calibration }).catch(() => {
// Silently handle error
});
};
const toggleFullscreen = async () => {
try {
const window = getCurrentWindow();
const currentFullscreen = await window.isFullscreen();
await window.setFullscreen(!currentFullscreen);
setIsFullscreen(!currentFullscreen);
// 退出全屏时重置面板位置
if (currentFullscreen) {
setPanelPosition({ x: 0, y: 0 });
}
} catch (error) {
// Silently handle fullscreen error
}
};
const exit = () => {
// 退出时确保退出全屏模式
if (isFullscreen()) {
toggleFullscreen().then(() => {
window.history.back();
});
} else {
window.history.back();
}
};
const reset = () => {
invoke('set_color_calibration', {
calibration: new ColorCalibration(),
}).catch(() => {
// Silently handle error
});
};
return (
<>
{/* 普通模式 */}
{!isFullscreen() && (
<div class="space-y-6">
<div class="flex items-center justify-between">
<h1 class="text-2xl font-bold text-base-content">{t('whiteBalance.title')}</h1>
<div class="flex gap-2">
<button class="btn btn-outline btn-sm" onClick={toggleFullscreen} title={t('common.fullscreen')}>
<BsFullscreen size={16} />
{t('common.fullscreen')}
</button>
<button class="btn btn-outline btn-sm" onClick={reset} title={t('common.reset')}>
<BiRegularReset size={16} />
{t('common.reset')}
</button>
<button class="btn btn-primary btn-sm" onClick={exit} title={t('whiteBalance.back')}>
<VsClose size={16} />
{t('whiteBalance.back')}
</button>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* 颜色测试区域 */}
<div class="card bg-base-200 shadow-lg">
<div class="card-body p-4">
<div class="card-title text-base mb-3">
<span>{t('whiteBalance.colorTest')}</span>
<div class="badge badge-info badge-outline">{t('whiteBalance.clickToTest')}</div>
</div>
<div
class="aspect-square rounded-lg overflow-hidden border border-base-300"
style={{
'background-image': `url(${transparentBg})`,
}}
>
<TestColorsBg />
</div>
<div class="text-xs text-base-content/50 mt-2">
💡 {t('whiteBalance.colorTestTip')}
</div>
</div>
</div>
{/* 白平衡控制面板 */}
<div class="card bg-base-200 shadow-lg">
<div class="card-body p-4">
<div class="card-title text-base mb-3">
<span>{t('whiteBalance.rgbAdjustment')}</span>
<div class="badge badge-secondary badge-outline">{t('whiteBalance.realtimeAdjustment')}</div>
</div>
<div class="space-y-4">
<div class="form-control">
<label class="label">
<span class="label-text font-semibold text-red-500">{t('whiteBalance.redChannel')}</span>
<Value value={ledStripStore.colorCalibration.r} />
</label>
<ColorSlider
class="from-cyan-500 to-red-500"
value={ledStripStore.colorCalibration.r}
onInput={(ev) =>
updateColorCalibration(
'r',
(ev.target as HTMLInputElement).valueAsNumber ?? 1,
)
}
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-semibold text-green-500">{t('whiteBalance.greenChannel')}</span>
<Value value={ledStripStore.colorCalibration.g} />
</label>
<ColorSlider
class="from-pink-500 to-green-500"
value={ledStripStore.colorCalibration.g}
onInput={(ev) =>
updateColorCalibration(
'g',
(ev.target as HTMLInputElement).valueAsNumber ?? 1,
)
}
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-semibold text-blue-500">{t('whiteBalance.blueChannel')}</span>
<Value value={ledStripStore.colorCalibration.b} />
</label>
<ColorSlider
class="from-yellow-500 to-blue-500"
value={ledStripStore.colorCalibration.b}
onInput={(ev) =>
updateColorCalibration(
'b',
(ev.target as HTMLInputElement).valueAsNumber ?? 1,
)
}
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-semibold text-amber-500">{t('whiteBalance.whiteChannel')}</span>
<Value value={ledStripStore.colorCalibration.w} />
</label>
<ColorSlider
class="from-amber-100 to-amber-50"
value={ledStripStore.colorCalibration.w}
onInput={(ev) =>
updateColorCalibration(
'w',
(ev.target as HTMLInputElement).valueAsNumber ?? 1,
)
}
/>
</div>
</div>
{/* 使用说明 - 可展开 */}
<div class="collapse collapse-arrow bg-base-100 mt-4">
<input type="checkbox" />
<div class="collapse-title text-sm font-medium text-base-content/80">
💡 {t('whiteBalance.usageInstructions')}
</div>
<div class="collapse-content text-xs text-base-content/70 space-y-3">
<div class="space-y-2">
<p class="font-semibold text-primary">{t('whiteBalance.recommendedMethod')}</p>
<ol class="list-decimal list-inside space-y-1 ml-2">
<li>{t('whiteBalance.fullscreenTip')}</li>
<li>{t('whiteBalance.dragTip')}</li>
<li>{t('whiteBalance.dragPanelTip')}</li>
<li>{t('whiteBalance.compareColorsTip')}</li>
</ol>
</div>
<div class="space-y-2">
<p class="font-semibold text-secondary">{t('whiteBalance.adjustmentTips')}</p>
<ul class="list-disc list-inside space-y-1 ml-2">
<li>{t('whiteBalance.redStrong')}</li>
<li>{t('whiteBalance.greenStrong')}</li>
<li>{t('whiteBalance.blueStrong')}</li>
<li>{t('whiteBalance.whiteYellow')}</li>
<li>{t('whiteBalance.whiteBlue')}</li>
</ul>
</div>
<div class="space-y-2">
<p class="font-semibold text-accent">{t('whiteBalance.comparisonMethod')}</p>
<ul class="list-disc list-inside space-y-1 ml-2">
<li>{t('whiteBalance.whiteComparison')}</li>
<li>{t('whiteBalance.colorComparison')}</li>
<li>{t('whiteBalance.environmentTest')}</li>
<li>{t('whiteBalance.resetNote')}</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
)}
{/* 全屏模式 */}
{isFullscreen() && (
<div class="fixed inset-0 w-screen h-screen bg-black z-50">
{/* 全屏颜色测试区域 - 紧贴边缘 */}
<div class="absolute inset-0 w-full h-full">
<TestColorsBg />
</div>
{/* 可拖拽的RGB控制面板 */}
<div
class="fixed w-80 bg-base-200/95 backdrop-blur-sm rounded-lg shadow-xl z-60 select-none"
style={{
left: `${panelPosition().x}px`,
top: `${panelPosition().y}px`,
transform: 'none'
}}
>
<div class="card-body p-4">
<div
class="card-title text-base mb-3 flex justify-between items-center cursor-move"
onMouseDown={handleMouseDown}
>
<div class="flex items-center gap-2">
<span class="text-xs opacity-60"></span>
<span>{t('whiteBalance.rgbAdjustment')}</span>
<div class="badge badge-secondary badge-outline">{t('whiteBalance.draggable')}</div>
</div>
<button class="btn btn-ghost btn-xs cursor-pointer" onClick={toggleFullscreen} title={t('whiteBalance.exitFullscreen')}>
<BsFullscreenExit size={14} />
</button>
</div>
<div class="space-y-4">
<div class="form-control">
<label class="label">
<span class="label-text font-semibold text-red-500">{t('whiteBalance.redChannel')}</span>
<Value value={ledStripStore.colorCalibration.r} />
</label>
<ColorSlider
class="from-cyan-500 to-red-500"
value={ledStripStore.colorCalibration.r}
onInput={(ev) =>
updateColorCalibration(
'r',
(ev.target as HTMLInputElement).valueAsNumber ?? 1,
)
}
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-semibold text-green-500">{t('whiteBalance.greenChannel')}</span>
<Value value={ledStripStore.colorCalibration.g} />
</label>
<ColorSlider
class="from-pink-500 to-green-500"
value={ledStripStore.colorCalibration.g}
onInput={(ev) =>
updateColorCalibration(
'g',
(ev.target as HTMLInputElement).valueAsNumber ?? 1,
)
}
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-semibold text-blue-500">{t('whiteBalance.blueChannel')}</span>
<Value value={ledStripStore.colorCalibration.b} />
</label>
<ColorSlider
class="from-yellow-500 to-blue-500"
value={ledStripStore.colorCalibration.b}
onInput={(ev) =>
updateColorCalibration(
'b',
(ev.target as HTMLInputElement).valueAsNumber ?? 1,
)
}
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-semibold text-amber-500">{t('whiteBalance.whiteChannel')}</span>
<Value value={ledStripStore.colorCalibration.w} />
</label>
<ColorSlider
class="from-amber-100 to-amber-50"
value={ledStripStore.colorCalibration.w}
onInput={(ev) =>
updateColorCalibration(
'w',
(ev.target as HTMLInputElement).valueAsNumber ?? 1,
)
}
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-semibold text-base-content/70">{t('whiteBalance.whiteChannel')}</span>
<div class="badge badge-outline badge-sm">{t('whiteBalance.notEnabled')}</div>
</label>
<ColorSlider class="from-yellow-50 to-cyan-50" disabled />
</div>
</div>
<div class="text-xs text-base-content/60 mt-3 p-2 bg-base-300/50 rounded">
💡 {t('whiteBalance.fullscreenComparisonTip')}
</div>
<div class="flex gap-2 mt-4">
<button class="btn btn-outline btn-sm flex-1" onClick={reset} title={t('common.reset')}>
<BiRegularReset size={14} />
{t('common.reset')}
</button>
<button class="btn btn-primary btn-sm flex-1" onClick={exit} title={t('whiteBalance.back')}>
<VsClose size={14} />
{t('whiteBalance.back')}
</button>
</div>
</div>
</div>
</div>
)}
</>
);
};