앱 버전표시
This commit is contained in:
@@ -10,6 +10,7 @@
|
|||||||
"core:window:allow-hide",
|
"core:window:allow-hide",
|
||||||
"core:window:allow-show",
|
"core:window:allow-show",
|
||||||
"core:window:allow-set-focus",
|
"core:window:allow-set-focus",
|
||||||
|
"core:window:allow-set-title",
|
||||||
"core:window:allow-start-dragging",
|
"core:window:allow-start-dragging",
|
||||||
"core:webview:allow-create-webview-window",
|
"core:webview:allow-create-webview-window",
|
||||||
"core:webview:allow-webview-close",
|
"core:webview:allow-webview-close",
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ pub fn run() {
|
|||||||
.plugin(tauri_plugin_process::init())
|
.plugin(tauri_plugin_process::init())
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
|
let version = app.package_info().version.to_string();
|
||||||
|
let main_title = format!("GoldenChart (v{version})");
|
||||||
|
if let Some(w) = app.get_webview_window("main") {
|
||||||
|
let _ = w.set_title(&main_title);
|
||||||
|
}
|
||||||
|
|
||||||
let show_i = MenuItemBuilder::with_id("show", "GoldenChart 열기").build(app)?;
|
let show_i = MenuItemBuilder::with_id("show", "GoldenChart 열기").build(app)?;
|
||||||
let quit_i = MenuItemBuilder::with_id("quit", "종료").build(app)?;
|
let quit_i = MenuItemBuilder::with_id("quit", "종료").build(app)?;
|
||||||
let menu = MenuBuilder::new(app)
|
let menu = MenuBuilder::new(app)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
type DesktopUpdatePhase,
|
type DesktopUpdatePhase,
|
||||||
type DesktopUpdateProgress,
|
type DesktopUpdateProgress,
|
||||||
} from './bridge/updater';
|
} from './bridge/updater';
|
||||||
|
import { scheduleMainWindowTitleSync } from './setMainWindowTitle';
|
||||||
import '@frontend/App.css';
|
import '@frontend/App.css';
|
||||||
|
|
||||||
const PHASE_LABEL: Record<DesktopUpdatePhase, string> = {
|
const PHASE_LABEL: Record<DesktopUpdatePhase, string> = {
|
||||||
@@ -143,6 +144,7 @@ export function StartupUpdateGate({ children }: { children: React.ReactNode }) {
|
|||||||
if (result.phase === 'error') {
|
if (result.phase === 'error') {
|
||||||
console.warn('[startup-update]', result.message);
|
console.warn('[startup-update]', result.message);
|
||||||
}
|
}
|
||||||
|
scheduleMainWindowTitleSync();
|
||||||
setReady(true);
|
setReady(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { openWidgetWindow, closeWidgetWindow, focusWidgetWindow } from './widgetWindows';
|
import { openWidgetWindow, closeWidgetWindow, focusWidgetWindow } from './widgetWindows';
|
||||||
import { showNativeNotification } from './notifications';
|
import { showNativeNotification } from './notifications';
|
||||||
import { checkForUpdates, getAppVersion, runDesktopUpdate } from './updater';
|
import { checkForUpdates, getAppVersion, runDesktopUpdate } from './updater';
|
||||||
|
import { syncMainWindowTitle } from '../setMainWindowTitle';
|
||||||
import type { DesktopBridge } from './types';
|
import type { DesktopBridge } from './types';
|
||||||
|
|
||||||
/** frontend App 로드 전 호출 — window.__goldenDesktopBridge 등록 */
|
/** frontend App 로드 전 호출 — window.__goldenDesktopBridge 등록 */
|
||||||
@@ -13,6 +14,7 @@ export function installDesktopBridge(): void {
|
|||||||
checkForUpdates,
|
checkForUpdates,
|
||||||
runDesktopUpdate,
|
runDesktopUpdate,
|
||||||
getAppVersion,
|
getAppVersion,
|
||||||
|
syncMainWindowTitle,
|
||||||
};
|
};
|
||||||
window.__goldenDesktopBridge = bridge;
|
window.__goldenDesktopBridge = bridge;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export interface DesktopBridge {
|
|||||||
onProgress?: (progress: DesktopUpdateProgress) => void,
|
onProgress?: (progress: DesktopUpdateProgress) => void,
|
||||||
) => Promise<DesktopUpdateResult>;
|
) => Promise<DesktopUpdateResult>;
|
||||||
getAppVersion: () => Promise<string>;
|
getAppVersion: () => Promise<string>;
|
||||||
|
syncMainWindowTitle: () => Promise<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import ReactDOM from 'react-dom/client';
|
|||||||
import { initStorage, refreshApiBaseFromStorage } from '@goldenchart/shared';
|
import { initStorage, refreshApiBaseFromStorage } from '@goldenchart/shared';
|
||||||
import { installDesktopBridge } from './bridge/installDesktopBridge';
|
import { installDesktopBridge } from './bridge/installDesktopBridge';
|
||||||
import { ensureDesktopApiBase } from './ensureDesktopApiBase';
|
import { ensureDesktopApiBase } from './ensureDesktopApiBase';
|
||||||
|
import { scheduleMainWindowTitleSync, syncMainWindowTitle } from './setMainWindowTitle';
|
||||||
import { StartupUpdateGate } from './StartupUpdateGate';
|
import { StartupUpdateGate } from './StartupUpdateGate';
|
||||||
import App from '@frontend/App';
|
import App from '@frontend/App';
|
||||||
|
|
||||||
@@ -13,6 +14,13 @@ async function bootstrap() {
|
|||||||
await initStorage();
|
await initStorage();
|
||||||
ensureDesktopApiBase();
|
ensureDesktopApiBase();
|
||||||
refreshApiBaseFromStorage();
|
refreshApiBaseFromStorage();
|
||||||
|
document.documentElement.classList.add('desktop-client');
|
||||||
|
try {
|
||||||
|
await syncMainWindowTitle();
|
||||||
|
} catch {
|
||||||
|
/* 권한·타이밍 이슈 — scheduleMainWindowTitleSync 가 재시도 */
|
||||||
|
}
|
||||||
|
scheduleMainWindowTitleSync();
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { getVersion } from '@tauri-apps/api/app';
|
||||||
|
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||||
|
|
||||||
|
const APP_NAME = 'GoldenChart';
|
||||||
|
|
||||||
|
export function formatDesktopWindowTitle(version: string): string {
|
||||||
|
const v = version.trim();
|
||||||
|
return v ? `${APP_NAME} (v${v})` : APP_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** macOS/Windows 네이티브 창 타이틀 — GoldenChart (vX.Y.Z) */
|
||||||
|
export async function syncMainWindowTitle(): Promise<string> {
|
||||||
|
try {
|
||||||
|
const version = await getVersion();
|
||||||
|
const title = formatDesktopWindowTitle(version);
|
||||||
|
await getCurrentWindow().setTitle(title);
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
document.title = title;
|
||||||
|
}
|
||||||
|
return title;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[desktop] syncMainWindowTitle failed:', e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** WebView 로드 후 macOS가 document.title로 타이틀을 덮어쓸 수 있어 재시도 */
|
||||||
|
export function scheduleMainWindowTitleSync(): void {
|
||||||
|
const delays = [0, 150, 600, 2000];
|
||||||
|
for (const ms of delays) {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
void syncMainWindowTitle().catch(() => {});
|
||||||
|
}, ms);
|
||||||
|
}
|
||||||
|
void getCurrentWindow()
|
||||||
|
.listen('tauri://focus', () => {
|
||||||
|
void syncMainWindowTitle().catch(() => {});
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
Vendored
+2
@@ -1 +1,3 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
declare const __DESKTOP_APP_VERSION__: string;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { readFileSync } from 'fs';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
@@ -9,6 +10,16 @@ const sharedRoot = path.resolve(__dirname, '../packages/shared/src');
|
|||||||
|
|
||||||
const host = process.env.TAURI_DEV_HOST;
|
const host = process.env.TAURI_DEV_HOST;
|
||||||
|
|
||||||
|
function readDesktopAppVersion(): string {
|
||||||
|
try {
|
||||||
|
const confPath = path.resolve(__dirname, 'src-tauri/tauri.conf.json');
|
||||||
|
const conf = JSON.parse(readFileSync(confPath, 'utf8')) as { version?: string };
|
||||||
|
return String(conf.version ?? '0.0.0');
|
||||||
|
} catch {
|
||||||
|
return '0.0.0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
/** Tauri 패키지 앱 — 절대 경로(/assets) 사용 시 빈 화면 */
|
/** Tauri 패키지 앱 — 절대 경로(/assets) 사용 시 빈 화면 */
|
||||||
base: './',
|
base: './',
|
||||||
@@ -18,6 +29,7 @@ export default defineConfig({
|
|||||||
/** desktop은 항상 exdev 서버 (localhost 금지) */
|
/** desktop은 항상 exdev 서버 (localhost 금지) */
|
||||||
'import.meta.env.VITE_API_BASE_URL': JSON.stringify('http://exdev.co.kr/api'),
|
'import.meta.env.VITE_API_BASE_URL': JSON.stringify('http://exdev.co.kr/api'),
|
||||||
__DESKTOP_CLIENT__: 'true',
|
__DESKTOP_CLIENT__: 'true',
|
||||||
|
__DESKTOP_APP_VERSION__: JSON.stringify(readDesktopAppVersion()),
|
||||||
},
|
},
|
||||||
envDir: __dirname,
|
envDir: __dirname,
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
|||||||
@@ -206,6 +206,11 @@ html.theme-blue {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html.desktop-client .tmb-logo-text {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
/* 구분선 */
|
/* 구분선 */
|
||||||
.tmb-divider {
|
.tmb-divider {
|
||||||
width: 1px;
|
width: 1px;
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import type { LoginResponse } from './utils/backendApi';
|
|||||||
import { invalidateAppSettingsCache } from './hooks/useAppSettings';
|
import { invalidateAppSettingsCache } from './hooks/useAppSettings';
|
||||||
import { invalidateIndicatorSettingsCache } from './hooks/useIndicatorSettings';
|
import { invalidateIndicatorSettingsCache } from './hooks/useIndicatorSettings';
|
||||||
import { isDesktop } from './utils/platform';
|
import { isDesktop } from './utils/platform';
|
||||||
|
import { syncDesktopMainWindowTitle } from './utils/desktopBridge';
|
||||||
import { writeDesktopSync } from './utils/desktopSync';
|
import { writeDesktopSync } from './utils/desktopSync';
|
||||||
|
|
||||||
const LAST_MENU_KEY = 'gc_last_menu';
|
const LAST_MENU_KEY = 'gc_last_menu';
|
||||||
@@ -513,6 +514,11 @@ function AppMainContent({
|
|||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [sessionKey, setSessionKey] = useState(0);
|
const [sessionKey, setSessionKey] = useState(0);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isDesktop()) return;
|
||||||
|
void syncDesktopMainWindowTitle();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const [appEntered, setAppEnteredState] = useState(() => isAppEntered());
|
const [appEntered, setAppEnteredState] = useState(() => isAppEntered());
|
||||||
const [authUser, setAuthUser] = useState<AuthSession | null>(() =>
|
const [authUser, setAuthUser] = useState<AuthSession | null>(() =>
|
||||||
isAppEntered() ? getAuthSession() : null,
|
isAppEntered() ? getAuthSession() : null,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import type { AuthSession } from '../utils/auth';
|
|||||||
import type { TradeAlertPopupLayout, TradeAlertPopupPosition } from '../utils/tradeAlertPopupLayout';
|
import type { TradeAlertPopupLayout, TradeAlertPopupPosition } from '../utils/tradeAlertPopupLayout';
|
||||||
import { canAccessMenu } from '../utils/permissions';
|
import { canAccessMenu } from '../utils/permissions';
|
||||||
import { isDesktop } from '../utils/platform';
|
import { isDesktop } from '../utils/platform';
|
||||||
|
import { useDesktopAppTitle, formatDesktopAppLabel } from '../hooks/useDesktopAppTitle';
|
||||||
import DesktopUpdateModal from './DesktopUpdateModal';
|
import DesktopUpdateModal from './DesktopUpdateModal';
|
||||||
import TopMenuBarNavGroups from './TopMenuBarNavGroups';
|
import TopMenuBarNavGroups from './TopMenuBarNavGroups';
|
||||||
|
|
||||||
@@ -297,6 +298,7 @@ export const TopMenuBar = memo(function TopMenuBar({
|
|||||||
const [isFullscreen, setIsFullscreen] = useState(() => !!document.fullscreenElement);
|
const [isFullscreen, setIsFullscreen] = useState(() => !!document.fullscreenElement);
|
||||||
const [desktopUpdateOpen, setDesktopUpdateOpen] = useState(false);
|
const [desktopUpdateOpen, setDesktopUpdateOpen] = useState(false);
|
||||||
const desktopClient = isDesktop();
|
const desktopClient = isDesktop();
|
||||||
|
const desktopAppVersion = useDesktopAppTitle();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sync = () => setIsFullscreen(!!document.fullscreenElement);
|
const sync = () => setIsFullscreen(!!document.fullscreenElement);
|
||||||
@@ -317,7 +319,9 @@ export const TopMenuBar = memo(function TopMenuBar({
|
|||||||
<rect width="20" height="20" rx="4" fill="#2196f3"/>
|
<rect width="20" height="20" rx="4" fill="#2196f3"/>
|
||||||
<polyline points="3,14 7,9 10,12 14,6 17,6" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none"/>
|
<polyline points="3,14 7,9 10,12 14,6 17,6" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span className="tmb-logo-text">GoldenChart</span>
|
<span className="tmb-logo-text">
|
||||||
|
{desktopClient ? formatDesktopAppLabel(desktopAppVersion) : 'GoldenChart'}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="tmb-divider" />
|
<div className="tmb-divider" />
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { isDesktop } from '../utils/platform';
|
||||||
|
import { getDesktopAppVersion, syncDesktopMainWindowTitle } from '../utils/desktopBridge';
|
||||||
|
|
||||||
|
declare const __DESKTOP_APP_VERSION__: string | undefined;
|
||||||
|
|
||||||
|
function readBakedDesktopVersion(): string | null {
|
||||||
|
if (typeof __DESKTOP_APP_VERSION__ === 'undefined') return null;
|
||||||
|
const v = __DESKTOP_APP_VERSION__.trim();
|
||||||
|
return v || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Desktop — 네이티브 창 타이틀·document.title을 GoldenChart (vX.Y.Z) 로 맞춤 */
|
||||||
|
export function useDesktopAppTitle(): string | null {
|
||||||
|
const [version, setVersion] = useState<string | null>(() =>
|
||||||
|
isDesktop() ? readBakedDesktopVersion() : null,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isDesktop()) return;
|
||||||
|
let cancelled = false;
|
||||||
|
void (async () => {
|
||||||
|
const v = (await getDesktopAppVersion()) ?? readBakedDesktopVersion();
|
||||||
|
if (cancelled || !v) return;
|
||||||
|
setVersion(v);
|
||||||
|
await syncDesktopMainWindowTitle();
|
||||||
|
})();
|
||||||
|
return () => { cancelled = true; };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDesktopAppLabel(version: string | null | undefined): string {
|
||||||
|
if (!version?.trim()) return 'GoldenChart';
|
||||||
|
return `GoldenChart (v${version.trim()})`;
|
||||||
|
}
|
||||||
@@ -63,6 +63,8 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 10px 12px 12px;
|
padding: 10px 12px 12px;
|
||||||
background: var(--se-center-bg, var(--se-bg));
|
background: var(--se-center-bg, var(--se-bg));
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 좌·중·우 패널 동일 높이 — flex 체인 */
|
/* 좌·중·우 패널 동일 높이 — flex 체인 */
|
||||||
@@ -193,6 +195,8 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export interface DesktopBridge {
|
|||||||
onProgress?: (progress: DesktopUpdateProgress) => void,
|
onProgress?: (progress: DesktopUpdateProgress) => void,
|
||||||
) => Promise<DesktopUpdateResult>;
|
) => Promise<DesktopUpdateResult>;
|
||||||
getAppVersion: () => Promise<string>;
|
getAppVersion: () => Promise<string>;
|
||||||
|
syncMainWindowTitle: () => Promise<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@@ -85,3 +86,13 @@ export async function getDesktopAppVersion(): Promise<string | null> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function syncDesktopMainWindowTitle(): Promise<string | null> {
|
||||||
|
const bridge = getDesktopBridge();
|
||||||
|
if (!bridge?.syncMainWindowTitle) return null;
|
||||||
|
try {
|
||||||
|
return await bridge.syncMainWindowTitle();
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user