Files
goldenChart/desktop/src/widget-main.tsx
T
2026-06-14 10:33:53 +09:00

189 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import './forceServerApi';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import ReactDOM from 'react-dom/client';
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 FloatingWidgetWindow from '@frontend/components/floatingWidgets/FloatingWidgetWindow';
import {
defaultGridFr,
type FloatingWidgetInstance,
} from '@frontend/types/floatingWidget';
import type { Theme } from '@frontend/types';
import type { WidgetSlot } from '@frontend/types/widgetDashboard';
import { useMarketTicker } from '@frontend/hooks/useMarketTicker';
import {
loadPaperSummary,
loadPaperTrades,
loadStrategies,
type PaperSummaryDto,
type PaperTradeDto,
type StrategyDto,
} from '@frontend/utils/backendApi';
import { PAPER_TRADES_CHANGED_EVENT } from '@frontend/utils/paperTradeEvents';
import { WidgetDashboardProvider } from '@frontend/widgets/WidgetDashboardContext';
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';
document.documentElement.classList.add('fw-native-widget');
function parseInstanceFromUrl(): FloatingWidgetInstance | null {
const params = new URLSearchParams(window.location.search);
const id = params.get('id');
if (!id) return null;
const rows = Number(params.get('rows') ?? 1);
const cols = Number(params.get('cols') ?? 1);
const width = Number(params.get('width') ?? 640);
const height = Number(params.get('height') ?? 480);
let slots: WidgetSlot[] = [];
try {
slots = JSON.parse(params.get('slots') ?? '[]') as WidgetSlot[];
} catch {
slots = [];
}
const rowFrRaw = params.get('rowFr');
const colFrRaw = params.get('colFr');
const rowFr = rowFrRaw ? rowFrRaw.split(',').map(Number) : defaultGridFr(rows);
const colFr = colFrRaw ? colFrRaw.split(',').map(Number) : defaultGridFr(cols);
return {
id,
rows,
cols,
slots,
rowFr,
colFr,
position: { x: 0, y: 0 },
width,
height,
zIndex: 1,
};
}
const WidgetApp: React.FC = () => {
const initial = useMemo(() => parseInstanceFromUrl(), []);
const [instance, setInstance] = useState<FloatingWidgetInstance | null>(initial);
const sync = readDesktopSync();
const [theme, setTheme] = useState<Theme>((sync?.theme as Theme | undefined) ?? 'dark');
const [defaultMarket, setDefaultMarket] = useState(sync?.selectedMarket ?? 'KRW-BTC');
const { tickers, marketInfos, loading: marketLoading, usdRate } = useMarketTicker(
useMemo(() => ({ enabled: true, loadFull: true }), []),
);
const [strategies, setStrategies] = useState<StrategyDto[]>([]);
const [summary, setSummary] = useState<PaperSummaryDto | null>(null);
const [trades, setTrades] = useState<PaperTradeDto[]>([]);
const refreshPaperData = useCallback(async () => {
try {
const [sum, tr] = await Promise.all([loadPaperSummary(), loadPaperTrades()]);
setSummary(sum);
setTrades(tr ?? []);
} catch { /* ignore */ }
}, []);
useEffect(() => {
void loadStrategies().then(list => setStrategies(list ?? []));
void refreshPaperData();
}, [refreshPaperData]);
useEffect(() => {
const onChanged = () => { void refreshPaperData(); };
window.addEventListener(PAPER_TRADES_CHANGED_EVENT, onChanged);
return () => window.removeEventListener(PAPER_TRADES_CHANGED_EVENT, onChanged);
}, [refreshPaperData]);
useEffect(() => {
return subscribeDesktopSync(state => {
if (state.theme) setTheme(state.theme as Theme);
if (state.selectedMarket) setDefaultMarket(state.selectedMarket);
});
}, []);
useEffect(() => {
syncDocumentTheme(theme);
}, [theme]);
useEffect(() => {
if (!instance) return;
const title = `GoldenChart — 위젯 ${instance.rows}×${instance.cols}`;
void getCurrentWindow().setTitle(title);
}, [instance?.rows, instance?.cols, instance]);
useEffect(() => {
if (!instance) return;
let unlisten: (() => void) | undefined;
void (async () => {
unlisten = await getCurrentWindow().onResized(async () => {
const size = await getCurrentWindow().innerSize();
setInstance(prev => (
prev ? { ...prev, width: size.width, height: size.height } : prev
));
});
})();
return () => { unlisten?.(); };
}, [instance?.id]);
const handleClose = useCallback(() => {
void getCurrentWindow().close();
}, []);
if (!instance) {
return <p className="wd-widget-empty">위젯 정보를 불러올 없습니다.</p>;
}
return (
<WidgetDashboardProvider
theme={theme}
tickers={tickers}
marketInfos={marketInfos}
marketLoading={marketLoading}
usdRate={usdRate}
defaultMarket={defaultMarket}
strategies={strategies}
summary={summary}
trades={trades}
refreshPaperData={() => { void refreshPaperData(); }}
paperTradingEnabled
paperAutoTradeEnabled={false}
chartRealtimeSource="BACKEND_STOMP"
>
<div className="fw-widget-native-root">
<FloatingWidgetWindow
instance={instance}
focused
nativeShell
onClose={handleClose}
onFocus={() => {}}
onUpdateSlots={slots => setInstance(prev => (prev ? { ...prev, slots } : prev))}
onUpdateSize={(width, height) => {
setInstance(prev => (prev ? { ...prev, width, height } : prev));
void getCurrentWindow().setSize(new LogicalSize(width, height));
}}
onUpdateGridFr={(rowFr, colFr) => setInstance(prev => (prev ? { ...prev, rowFr, colFr } : prev))}
/>
</div>
</WidgetDashboardProvider>
);
};
async function bootstrap() {
await initStorage();
ensureDesktopApiBase();
refreshApiBaseFromStorage();
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<WidgetApp />
</React.StrictMode>,
);
}
void bootstrap();