위젯 추가 팝업 수정

This commit is contained in:
Macbook
2026-06-15 17:17:06 +09:00
parent 271392bc49
commit 51fdcf40e5
16 changed files with 435 additions and 238 deletions
+55 -57
View File
@@ -1,6 +1,8 @@
import React, { useEffect, useState } from 'react';
import DesktopUpdateModal from '@frontend/components/DesktopUpdateModal';
import {
runStartupAutoUpdate,
checkForUpdates,
getAppVersion,
type DesktopUpdatePhase,
type DesktopUpdateProgress,
} from './bridge/updater';
@@ -27,9 +29,8 @@ function formatBytes(n?: number): string {
function StartupUpdateSplash({ progress }: { progress: DesktopUpdateProgress | null }) {
const phase = progress?.phase ?? 'checking';
const isActive = phase === 'checking' || phase === 'downloading' || phase === 'installing' || phase === 'relaunching';
const showBar = phase === 'available' || phase === 'downloading' || phase === 'installing';
const barPct = phase === 'installing' ? 100 : progress?.downloadPercent;
const isActive = phase === 'checking';
const showBar = false;
const panelClass = phase === 'error'
? 'desktop-update-progress--failed'
@@ -54,9 +55,6 @@ function StartupUpdateSplash({ progress }: { progress: DesktopUpdateProgress | n
<h1 className="desktop-startup-update-title">GoldenChart</h1>
<p className="desktop-startup-update-sub">
{progress?.currentVersion ? `현재 v${progress.currentVersion}` : '시작 중…'}
{progress?.newVersion && progress.newVersion !== progress.currentVersion && (
<> v{progress.newVersion}</>
)}
</p>
<div className={`desktop-update-progress ${panelClass}`.trim()}>
@@ -65,27 +63,11 @@ function StartupUpdateSplash({ progress }: { progress: DesktopUpdateProgress | n
{isActive && <span className="desktop-update-spinner" aria-hidden />}
{PHASE_LABEL[phase]}
</span>
{phase === 'downloading' && progress?.downloadPercent != null && (
<span className="desktop-update-pct">{progress.downloadPercent}%</span>
)}
</div>
{showBar && (
<div
className="desktop-update-bar"
role="progressbar"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={barPct ?? undefined}
>
{barPct != null ? (
<div
className="desktop-update-bar-fill desktop-update-bar-fill--determinate"
style={{ width: `${barPct}%` }}
/>
) : (
<div className="desktop-update-bar-fill desktop-update-bar-fill--indeterminate" />
)}
<div className="desktop-update-bar" role="progressbar" aria-valuemin={0} aria-valuemax={100}>
<div className="desktop-update-bar-fill desktop-update-bar-fill--indeterminate" />
</div>
)}
@@ -94,10 +76,7 @@ function StartupUpdateSplash({ progress }: { progress: DesktopUpdateProgress | n
<span className="desktop-update-step-dot" aria-hidden />
</li>
<li className={`desktop-update-step${
phase === 'available' || phase === 'downloading' ? ' active'
: ['installing', 'relaunching', 'uptodate'].includes(phase) ? ' done' : ''
}`}>
<li className="desktop-update-step">
<span className="desktop-update-step-dot" aria-hidden />
{progress?.downloadedBytes && progress.totalBytes ? (
@@ -106,9 +85,7 @@ function StartupUpdateSplash({ progress }: { progress: DesktopUpdateProgress | n
</span>
) : null}
</li>
<li className={`desktop-update-step${
phase === 'installing' ? ' active' : phase === 'relaunching' || phase === 'uptodate' ? ' done' : ''
}`}>
<li className="desktop-update-step">
<span className="desktop-update-step-dot" aria-hidden />
</li>
@@ -125,44 +102,56 @@ function StartupUpdateSplash({ progress }: { progress: DesktopUpdateProgress | n
);
}
/** StrictMode 이중 실행 방지 — 시작 업데이트는 한 번만 */
let startupUpdateTask: ReturnType<typeof runStartupAutoUpdate> | null = null;
const startupProgressListeners = new Set<(p: DesktopUpdateProgress) => void>();
function emitStartupProgress(p: DesktopUpdateProgress) {
for (const listener of startupProgressListeners) listener(p);
}
function getStartupUpdateTask() {
if (!startupUpdateTask) {
startupUpdateTask = runStartupAutoUpdate(emitStartupProgress);
}
return startupUpdateTask;
}
/** 시작 시 자동 업데이트 후 children(메인 앱) 렌더 */
/** 시작 시 버전 확인 후 children(메인 앱) 렌더 — 새 버전 있으면 업데이트 모달 표시 */
export function StartupUpdateGate({ children }: { children: React.ReactNode }) {
const [ready, setReady] = useState(false);
const [startupUpdateOpen, setStartupUpdateOpen] = useState(false);
const [progress, setProgress] = useState<DesktopUpdateProgress | null>({
phase: 'checking',
message: '서버에서 새 버전을 확인하는 중…',
});
useEffect(() => {
const listener = (p: DesktopUpdateProgress) => setProgress(p);
startupProgressListeners.add(listener);
let cancelled = false;
void getStartupUpdateTask().then(({ shouldContinue, result }) => {
if (!shouldContinue) return;
if (result.phase === 'error') {
console.warn('[startup-update]', result.message);
void (async () => {
let currentVersion = '—';
try {
currentVersion = await getAppVersion();
} catch {
/* ignore */
}
if (cancelled) return;
setProgress({
phase: 'checking',
message: '서버에서 새 버전을 확인하는 중…',
currentVersion,
});
const result = await checkForUpdates();
if (cancelled) return;
if (result.available) {
setProgress({
phase: 'available',
message: result.message ?? `새 버전 v${result.version}을(를) 사용할 수 있습니다.`,
currentVersion,
newVersion: result.version,
});
} else if (result.message?.includes('실패') || result.message?.includes('오류')) {
setProgress({ phase: 'error', message: result.message, currentVersion });
}
scheduleMainWindowTitleSync();
setReady(true);
});
if (result.available) {
setStartupUpdateOpen(true);
}
})();
return () => {
startupProgressListeners.delete(listener);
cancelled = true;
};
}, []);
@@ -170,5 +159,14 @@ export function StartupUpdateGate({ children }: { children: React.ReactNode }) {
return <StartupUpdateSplash progress={progress} />;
}
return <>{children}</>;
return (
<>
{children}
<DesktopUpdateModal
open={startupUpdateOpen}
onClose={() => setStartupUpdateOpen(false)}
autoStart
/>
</>
);
}
+87
View File
@@ -0,0 +1,87 @@
import { WebviewWindow } from '@tauri-apps/api/webviewWindow';
import { LogicalSize } from '@tauri-apps/api/dpi';
import { NATIVE_WIDGET_PICKER_WINDOW } from '@frontend/types/floatingWidget';
const pickerWindows = new Map<string, WebviewWindow>();
function pickerLabel(instanceId: string): string {
return `widget-picker-${instanceId}`;
}
function pickerUrl(instanceId: string, slotId: string): string {
const params = new URLSearchParams({ instanceId, slotId });
return `picker.html?${params.toString()}`;
}
/** 서버 frontend AppPopup(720×860)과 동일한 전용 OS 창 */
export async function openWidgetPickerWindow(params: {
instanceId: string;
slotId: string;
}): Promise<void> {
const label = pickerLabel(params.instanceId);
const existing = pickerWindows.get(params.instanceId)
?? (await WebviewWindow.getByLabel(label));
if (existing) {
await existing.setFocus();
pickerWindows.set(params.instanceId, existing);
return;
}
const width = NATIVE_WIDGET_PICKER_WINDOW.width;
const height = NATIVE_WIDGET_PICKER_WINDOW.height;
const win = new WebviewWindow(label, {
url: pickerUrl(params.instanceId, params.slotId),
title: 'GoldenChart — 위젯 선택 (Widget)',
width,
height,
minWidth: Math.min(width, 520),
minHeight: Math.min(height, 560),
resizable: true,
center: true,
decorations: true,
alwaysOnTop: true,
backgroundColor: '#1a1b26',
});
pickerWindows.set(params.instanceId, win);
void win.once('tauri://destroyed', () => {
pickerWindows.delete(params.instanceId);
});
}
export async function closeWidgetPickerWindow(instanceId: string): Promise<void> {
const label = pickerLabel(instanceId);
const win = pickerWindows.get(instanceId) ?? (await WebviewWindow.getByLabel(label));
if (win) {
await win.close();
pickerWindows.delete(instanceId);
}
}
/** 화면이 작을 때 picker 창 논리 크기 (생성 직후 보정) */
export function computePickerWindowLogicalSize(): { width: number; height: number } {
const marginW = 32;
const marginH = 40;
const screenW = typeof window !== 'undefined' ? (window.screen?.availWidth ?? 1280) : 1280;
const screenH = typeof window !== 'undefined' ? (window.screen?.availHeight ?? 900) : 900;
return {
width: Math.min(
NATIVE_WIDGET_PICKER_WINDOW.width,
Math.max(520, screenW - marginW),
),
height: Math.min(
NATIVE_WIDGET_PICKER_WINDOW.height,
Math.max(560, screenH - marginH),
),
};
}
export async function fitPickerWindowToScreen(): Promise<void> {
const { getCurrentWindow } = await import('@tauri-apps/api/window');
const win = getCurrentWindow();
const target = computePickerWindowLogicalSize();
await win.setSize(new LogicalSize(target.width, target.height));
await win.center();
}
+3 -2
View File
@@ -2,9 +2,9 @@
* 데스크톱(Tauri) 브릿지 타입 — frontend에서 window.__goldenDesktopBridge 로 호출
*/
import type { FloatingWidgetInstance } from '@frontend/types/floatingWidget';
import type { DesktopUpdateProgress, DesktopUpdateResult } from './updater';
import type { DesktopUpdateProgress, DesktopUpdateResult, DesktopUpdateOptions } from './updater';
export type { DesktopUpdateProgress, DesktopUpdateResult, DesktopUpdatePhase } from './updater';
export type { DesktopUpdateProgress, DesktopUpdateResult, DesktopUpdatePhase, DesktopUpdateOptions } from './updater';
export interface DesktopBridge {
openWidgetWindow: (instance: FloatingWidgetInstance) => Promise<void>;
@@ -14,6 +14,7 @@ export interface DesktopBridge {
checkForUpdates: () => Promise<{ available: boolean; version?: string; message?: string }>;
runDesktopUpdate: (
onProgress?: (progress: DesktopUpdateProgress) => void,
options?: DesktopUpdateOptions,
) => Promise<DesktopUpdateResult>;
getAppVersion: () => Promise<string>;
syncMainWindowTitle: () => Promise<string>;
+38 -3
View File
@@ -41,7 +41,7 @@ function writeUpdateLoopGuard(guard: UpdateLoopGuard) {
}
}
function clearUpdateLoopGuard() {
export function clearUpdateLoopGuard() {
try {
localStorage.removeItem(UPDATE_LOOP_GUARD_KEY);
} catch {
@@ -49,6 +49,27 @@ function clearUpdateLoopGuard() {
}
}
export interface DesktopUpdateOptions {
/** 수동 업데이트·재시도 시 반복 실패 가드 무시 */
bypassLoopGuard?: boolean;
}
const RELAUNCH_TIMEOUT_MS = 12_000;
async function relaunchWithTimeout(): Promise<boolean> {
try {
await Promise.race([
relaunch(),
new Promise<never>((_, reject) => {
window.setTimeout(() => reject(new Error('relaunch timeout')), RELAUNCH_TIMEOUT_MS);
}),
]);
return true;
} catch {
return false;
}
}
function shouldBlockUpdateLoop(currentVersion: string, targetVersion: string): boolean {
const guard = readUpdateLoopGuard();
if (!guard) return false;
@@ -231,6 +252,7 @@ export async function runStartupAutoUpdate(
export async function runDesktopUpdate(
onProgress?: (p: DesktopUpdateProgress) => void,
options?: DesktopUpdateOptions,
): Promise<DesktopUpdateResult> {
let currentVersion = '';
try {
@@ -261,7 +283,7 @@ export async function runDesktopUpdate(
return { ok: true, phase: 'uptodate', message, currentVersion };
}
if (shouldBlockUpdateLoop(currentVersion, update.version)) {
if (!options?.bypassLoopGuard && shouldBlockUpdateLoop(currentVersion, update.version)) {
const message =
`v${update.version} 업데이트가 반복 실패했습니다. 서버 패키지와 버전이 맞지 않을 수 있습니다. ` +
'PC 프로그램 탭에서 설치 파일을 받아 다시 설치해 주세요.';
@@ -269,6 +291,10 @@ export async function runDesktopUpdate(
return { ok: false, phase: 'error', message, currentVersion, newVersion: update.version };
}
if (options?.bypassLoopGuard) {
clearUpdateLoopGuard();
}
recordUpdateLoopAttempt(currentVersion, update.version);
emit(onProgress, {
@@ -323,7 +349,16 @@ export async function runDesktopUpdate(
currentVersion,
newVersion: update.version,
});
await relaunch();
const relaunched = await relaunchWithTimeout();
if (!relaunched) {
clearUpdateLoopGuard();
const message =
'업데이트는 설치됐지만 앱을 자동으로 다시 시작하지 못했습니다. GoldenChart를 직접 종료한 뒤 다시 실행해 주세요.';
emit(onProgress, { phase: 'error', message, currentVersion, newVersion: update.version });
return { ok: false, phase: 'error', message, currentVersion, newVersion: update.version };
}
return {
ok: true,
phase: 'relaunching',
+11
View File
@@ -0,0 +1,11 @@
import { getCurrentWindow, type Window } from '@tauri-apps/api/window';
import { type PhysicalSize } from '@tauri-apps/api/dpi';
export type LogicalWindowSize = { width: number; height: number };
export async function readLogicalInnerSize(win: Window = getCurrentWindow()): Promise<LogicalWindowSize> {
const physical: PhysicalSize = await win.innerSize();
const factor = await win.scaleFactor();
const logical = physical.toLogical(factor);
return { width: logical.width, height: logical.height };
}
-79
View File
@@ -1,79 +0,0 @@
import { getCurrentWindow, type Window } from '@tauri-apps/api/window';
import { LogicalSize, type PhysicalSize } from '@tauri-apps/api/dpi';
/** 서버 frontend AppPopup 위젯 선택 팝업과 동일 (720×860) */
export const WEB_WIDGET_PICKER_WINDOW = { width: 720, height: 860 } as const;
export type LogicalWindowSize = { width: number; height: number };
const WIDGET_MIN = { width: 320, height: 240 } as const;
export async function readLogicalInnerSize(win: Window = getCurrentWindow()): Promise<LogicalWindowSize> {
const physical: PhysicalSize = await win.innerSize();
const factor = await win.scaleFactor();
const logical = physical.toLogical(factor);
return { width: logical.width, height: logical.height };
}
/** OS 창 목표 크기 — 현재 창 크기(innerWidth)가 아닌 화면 기준 */
export function computeNativePickerLogicalSize(): LogicalWindowSize {
const marginW = 32;
const marginH = 40;
const screenW = window.screen?.availWidth ?? 1280;
const screenH = window.screen?.availHeight ?? 900;
return {
width: Math.min(WEB_WIDGET_PICKER_WINDOW.width, Math.max(520, screenW - marginW)),
height: Math.min(WEB_WIDGET_PICKER_WINDOW.height, Math.max(560, screenH - marginH)),
};
}
async function waitForLogicalSize(
win: Window,
target: LogicalWindowSize,
timeoutMs = 1500,
): Promise<void> {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const current = await readLogicalInnerSize(win);
if (
Math.abs(current.width - target.width) < 16
&& Math.abs(current.height - target.height) < 16
) {
return;
}
await new Promise<void>(resolve => { window.setTimeout(resolve, 50); });
}
}
export async function expandNativeWidgetWindowForPicker(
win: Window = getCurrentWindow(),
): Promise<LogicalWindowSize> {
const before = await readLogicalInnerSize(win);
const target = computeNativePickerLogicalSize();
await win.setMinSize(new LogicalSize(
Math.min(target.width, WEB_WIDGET_PICKER_WINDOW.width),
Math.min(target.height, 520),
));
await win.setSize(new LogicalSize(target.width, target.height));
await win.center();
await win.setFocus();
await waitForLogicalSize(win, target);
const after = await readLogicalInnerSize(win);
if (after.width < target.width - 24 || after.height < target.height - 24) {
await win.setSize(new LogicalSize(target.width, target.height));
await waitForLogicalSize(win, target, 800);
}
return before;
}
export async function restoreNativeWidgetWindowAfterPicker(
before: LogicalWindowSize,
win: Window = getCurrentWindow(),
): Promise<void> {
await win.setMinSize(new LogicalSize(WIDGET_MIN.width, WIDGET_MIN.height));
await win.setSize(new LogicalSize(before.width, before.height));
await waitForLogicalSize(win, before, 800);
}
+101
View File
@@ -0,0 +1,101 @@
import './forceServerApi';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import ReactDOM from 'react-dom/client';
import { emit } from '@tauri-apps/api/event';
import { getCurrentWindow } from '@tauri-apps/api/window';
import { initStorage, refreshApiBaseFromStorage } from '@goldenchart/shared';
import { ensureDesktopApiBase } from './ensureDesktopApiBase';
import { fitPickerWindowToScreen } from './bridge/pickerWindow';
import WidgetPickerModal from '@frontend/components/widgetDashboard/WidgetPickerModal';
import {
WIDGET_PICKER_SELECT_EVENT,
type WidgetPickerSelectPayload,
} from './widgetPickerEvents';
import { readDesktopSync, subscribeDesktopSync } from '@frontend/utils/desktopSync';
import { syncDocumentTheme } from '@frontend/utils/documentTheme';
import type { Theme } from '@frontend/types';
import '@frontend/App.css';
import '@frontend/styles/appPopup.css';
import '@frontend/styles/widgetDashboard.css';
document.documentElement.classList.add('fw-native-picker-window');
function parseParams(): { instanceId: string; slotId: string } {
const params = new URLSearchParams(window.location.search);
return {
instanceId: params.get('instanceId') ?? '',
slotId: params.get('slotId') ?? '',
};
}
const PickerApp: React.FC = () => {
const { instanceId, slotId } = useMemo(() => parseParams(), []);
const sync = readDesktopSync();
const [theme, setTheme] = useState<Theme>((sync?.theme as Theme | undefined) ?? 'dark');
const [ready, setReady] = useState(false);
useEffect(() => {
syncDocumentTheme(theme);
}, [theme]);
useEffect(() => {
return subscribeDesktopSync(state => {
if (state.theme) setTheme(state.theme as Theme);
});
}, []);
useEffect(() => {
void fitPickerWindowToScreen()
.catch(err => console.warn('[picker-main] window fit failed', err))
.finally(() => setReady(true));
}, []);
const handleClose = useCallback(() => {
void getCurrentWindow().close();
}, []);
const handleSelect = useCallback(async (widgetType: string) => {
if (!instanceId || !slotId) {
await getCurrentWindow().close();
return;
}
const payload: WidgetPickerSelectPayload = { instanceId, slotId, widgetType };
await emit(WIDGET_PICKER_SELECT_EVENT, payload);
await getCurrentWindow().close();
}, [instanceId, slotId]);
if (!instanceId || !slotId) {
return <p className="wd-widget-empty"> .</p>;
}
if (!ready) {
return (
<div className="wd-picker-native-loading" aria-live="polite">
</div>
);
}
return (
<WidgetPickerModal
open
category="all"
onClose={handleClose}
onSelect={widgetType => { void handleSelect(widgetType); }}
/>
);
};
async function bootstrap() {
await initStorage();
ensureDesktopApiBase();
refreshApiBaseFromStorage();
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<PickerApp />
</React.StrictMode>,
);
}
void bootstrap();
+34 -90
View File
@@ -1,22 +1,24 @@
import './forceServerApi';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import ReactDOM from 'react-dom/client';
import { listen } from '@tauri-apps/api/event';
import { getCurrentWindow } from '@tauri-apps/api/window';
import { LogicalSize } from '@tauri-apps/api/dpi';
import { initStorage, refreshApiBaseFromStorage } from '@goldenchart/shared';
import { ensureDesktopApiBase } from './ensureDesktopApiBase';
import { openWidgetPickerWindow } from './bridge/pickerWindow';
import FloatingWidgetWindow from '@frontend/components/floatingWidgets/FloatingWidgetWindow';
import WidgetPickerModal from '@frontend/components/widgetDashboard/WidgetPickerModal';
import {
defaultGridFr,
type FloatingWidgetInstance,
} from '@frontend/types/floatingWidget';
import {
expandNativeWidgetWindowForPicker,
readLogicalInnerSize,
restoreNativeWidgetWindowAfterPicker,
type LogicalWindowSize,
} from './nativeWidgetPickerWindow';
} from './bridge/widgetWindowSize';
import {
WIDGET_PICKER_SELECT_EVENT,
type WidgetPickerSelectPayload,
} from './widgetPickerEvents';
import type { Theme } from '@frontend/types';
import type { WidgetSlot } from '@frontend/types/widgetDashboard';
import { useMarketTicker } from '@frontend/hooks/useMarketTicker';
@@ -33,7 +35,6 @@ import { WidgetDashboardProvider } from '@frontend/widgets/WidgetDashboardContex
import { readDesktopSync, subscribeDesktopSync } from '@frontend/utils/desktopSync';
import { syncDocumentTheme } from '@frontend/utils/documentTheme';
import '@frontend/App.css';
import '@frontend/styles/appPopup.css';
import '@frontend/styles/paperDashboard.css';
import '@frontend/styles/widgetDashboard.css';
import '@frontend/styles/floatingWidget.css';
@@ -117,18 +118,6 @@ const WidgetApp: React.FC = () => {
syncDocumentTheme(theme);
}, [theme]);
const [pickSlotId, setPickSlotId] = useState<string | null>(null);
const [pickerReady, setPickerReady] = useState(false);
const sizeBeforePickerRef = useRef<LogicalWindowSize | null>(null);
useEffect(() => {
if (!instance) return;
const title = pickSlotId
? 'GoldenChart — 위젯 선택'
: `GoldenChart — 위젯 ${instance.rows}×${instance.cols}`;
void getCurrentWindow().setTitle(title);
}, [instance?.rows, instance?.cols, instance, pickSlotId]);
useEffect(() => {
if (!instance) return;
let unlisten: (() => void) | undefined;
@@ -144,71 +133,41 @@ const WidgetApp: React.FC = () => {
}, [instance?.id]);
useEffect(() => {
if (pickSlotId) {
document.documentElement.classList.add('fw-native-picker-mode');
} else {
document.documentElement.classList.remove('fw-native-picker-mode');
}
return () => {
document.documentElement.classList.remove('fw-native-picker-mode');
};
}, [pickSlotId]);
useEffect(() => {
if (!pickSlotId) {
setPickerReady(false);
const prev = sizeBeforePickerRef.current;
if (prev) {
sizeBeforePickerRef.current = null;
void restoreNativeWidgetWindowAfterPicker(prev).catch(err => {
console.warn('[widget-main] picker restore failed', err);
});
}
return;
}
let cancelled = false;
setPickerReady(false);
if (!instance) return;
let unlisten: (() => void) | undefined;
void (async () => {
try {
sizeBeforePickerRef.current = await expandNativeWidgetWindowForPicker();
if (!cancelled) setPickerReady(true);
} catch (err) {
console.warn('[widget-main] picker expand failed', err);
if (!cancelled) setPickerReady(true);
}
unlisten = await listen<WidgetPickerSelectPayload>(
WIDGET_PICKER_SELECT_EVENT,
event => {
const payload = event.payload;
if (payload.instanceId !== instance.id) return;
setInstance(prev => {
if (!prev) return prev;
return {
...prev,
slots: prev.slots.map(s => (
s.id === payload.slotId
? { ...s, widgetType: payload.widgetType, config: {} }
: s
)),
};
});
},
);
})();
return () => {
cancelled = true;
};
}, [pickSlotId]);
return () => { unlisten?.(); };
}, [instance?.id]);
const handleClose = useCallback(() => {
void getCurrentWindow().close();
}, []);
const handleNativePickRequest = useCallback((slotId: string) => {
setPickSlotId(slotId);
}, []);
const handleNativePickClose = useCallback(() => {
setPickSlotId(null);
}, []);
const handleNativePickSelect = useCallback((widgetType: string) => {
if (!pickSlotId) return;
setInstance(prev => {
if (!prev) return prev;
return {
...prev,
slots: prev.slots.map(s => (
s.id === pickSlotId ? { ...s, widgetType, config: {} } : s
)),
};
if (!instance) return;
void openWidgetPickerWindow({ instanceId: instance.id, slotId }).catch(err => {
console.warn('[widget-main] open picker window failed', err);
});
setPickSlotId(null);
}, [pickSlotId]);
}, [instance]);
if (!instance) {
return <p className="wd-widget-empty"> .</p>;
@@ -245,21 +204,6 @@ const WidgetApp: React.FC = () => {
}}
onUpdateGridFr={(rowFr, colFr) => setInstance(prev => (prev ? { ...prev, rowFr, colFr } : prev))}
/>
{pickSlotId != null && !pickerReady && (
<div className="wd-picker-native-loading" aria-live="polite">
</div>
)}
{pickSlotId != null && pickerReady && (
<WidgetPickerModal
open
category="all"
onClose={handleNativePickClose}
onSelect={handleNativePickSelect}
/>
)}
</div>
</WidgetDashboardProvider>
);
+8
View File
@@ -0,0 +1,8 @@
/** Tauri 위젯 ↔ 위젯 선택 창 이벤트 */
export const WIDGET_PICKER_SELECT_EVENT = 'gc-widget-picker-select';
export interface WidgetPickerSelectPayload {
instanceId: string;
slotId: string;
widgetType: string;
}