앱 버전표시

This commit is contained in:
Macbook
2026-06-14 19:20:48 +09:00
parent b2c6650cd5
commit 5045a548a4
10 changed files with 137 additions and 31 deletions
+23 -5
View File
@@ -4,12 +4,24 @@ use tauri::{
AppHandle, Manager, RunEvent,
};
fn format_main_window_title(version: &str) -> String {
format!("GoldenChart (v{version})")
}
fn apply_main_window_title(app: &AppHandle) {
let title = format_main_window_title(&app.package_info().version.to_string());
if let Some(w) = app.get_webview_window("main") {
let _ = w.set_title(&title);
}
}
fn show_main_window(app: &AppHandle) {
if let Some(w) = app.get_webview_window("main") {
let _ = w.show();
let _ = w.unminimize();
let _ = w.set_focus();
}
apply_main_window_title(app);
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
@@ -21,11 +33,7 @@ pub fn run() {
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_opener::init())
.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);
}
apply_main_window_title(app.handle());
let show_i = MenuItemBuilder::with_id("show", "GoldenChart 열기").build(app)?;
let quit_i = MenuItemBuilder::with_id("quit", "종료").build(app)?;
@@ -63,10 +71,19 @@ pub fn run() {
show_main_window(app.handle());
Ok(())
})
.on_page_load(|webview, _payload| {
if webview.label() != "main" {
return;
}
apply_main_window_title(webview.app_handle());
})
.on_window_event(|window, event| {
if window.label() != "main" {
return;
}
if matches!(event, tauri::WindowEvent::Focused(true)) {
apply_main_window_title(window.app_handle());
}
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
api.prevent_close();
let _ = window.hide();
@@ -75,6 +92,7 @@ pub fn run() {
.build(tauri::generate_context!())
.expect("error while building tauri application")
.run(|app_handle, event| match event {
RunEvent::Ready => apply_main_window_title(&app_handle),
#[cfg(target_os = "macos")]
RunEvent::Reopen { .. } => show_main_window(&app_handle),
RunEvent::ExitRequested { api, .. } => {
+37 -9
View File
@@ -1,32 +1,60 @@
import { getVersion } from '@tauri-apps/api/app';
import { getCurrentWindow } from '@tauri-apps/api/window';
import {
formatDesktopWindowTitle,
readBakedDesktopAppVersion,
} from '@frontend/utils/desktopAppVersion';
const APP_NAME = 'GoldenChart';
export function formatDesktopWindowTitle(version: string): string {
const v = version.trim();
return v ? `${APP_NAME} (v${v})` : APP_NAME;
}
export { formatDesktopWindowTitle } from '@frontend/utils/desktopAppVersion';
/** macOS/Windows 네이티브 창 타이틀 — GoldenChart (vX.Y.Z) */
export async function syncMainWindowTitle(): Promise<string> {
try {
const version = await getVersion();
const version = (await getVersion()) || readBakedDesktopAppVersion() || '';
const title = formatDesktopWindowTitle(version);
await getCurrentWindow().setTitle(title);
if (typeof document !== 'undefined') {
if (typeof document !== 'undefined' && document.title !== title) {
document.title = title;
}
return title;
} catch (e) {
const fallback = formatDesktopWindowTitle(readBakedDesktopAppVersion());
if (typeof document !== 'undefined') {
document.title = fallback;
}
console.warn('[desktop] syncMainWindowTitle failed:', e);
throw e;
}
}
let titleGuardInstalled = false;
/** macOS WebKit — document.title 변경 시 네이티브 타이틀을 다시 맞춤 */
export function installDocumentTitleGuard(): void {
if (titleGuardInstalled || typeof document === 'undefined') return;
titleGuardInstalled = true;
const rebalance = () => {
void syncMainWindowTitle().catch(() => {});
};
const titleEl = document.querySelector('title');
if (titleEl) {
const observer = new MutationObserver(() => {
if (!document.title.includes('(v')) rebalance();
});
observer.observe(titleEl, { childList: true, characterData: true, subtree: true });
}
window.setInterval(() => {
if (!document.title.includes('(v')) rebalance();
}, 3000);
}
/** WebView 로드 후 macOS가 document.title로 타이틀을 덮어쓸 수 있어 재시도 */
export function scheduleMainWindowTitleSync(): void {
const delays = [0, 150, 600, 2000];
installDocumentTitleGuard();
const delays = [0, 50, 150, 400, 1000, 2500, 5000];
for (const ms of delays) {
window.setTimeout(() => {
void syncMainWindowTitle().catch(() => {});
+19 -1
View File
@@ -20,6 +20,24 @@ function readDesktopAppVersion(): string {
}
}
const desktopVersion = readDesktopAppVersion();
const desktopWindowTitle = `GoldenChart (v${desktopVersion})`;
function desktopHtmlTitlePlugin() {
return {
name: 'desktop-html-title',
transformIndexHtml(html: string, ctx: { filename: string }) {
if (!ctx.filename.endsWith('index.html')) return html;
return html
.replace(/<title>[^<]*<\/title>/i, `<title>${desktopWindowTitle}</title>`)
.replace(
'<script>var global = globalThis;</script>',
`<script>var global = globalThis;window.__GC_APP_VERSION__="${desktopVersion}";</script>`,
);
},
};
}
export default defineConfig({
/** Tauri 패키지 앱 — 절대 경로(/assets) 사용 시 빈 화면 */
base: './',
@@ -32,7 +50,7 @@ export default defineConfig({
__DESKTOP_APP_VERSION__: JSON.stringify(readDesktopAppVersion()),
},
envDir: __dirname,
plugins: [react()],
plugins: [react(), desktopHtmlTitlePlugin()],
resolve: {
dedupe: ['react', 'react-dom', 'lightweight-charts'],
alias: {
+8
View File
@@ -211,6 +211,14 @@ html.desktop-client .tmb-logo-text {
font-weight: 600;
}
html.desktop-client .tmb-logo-version {
font-size: 12px;
font-weight: 500;
color: var(--text3);
letter-spacing: 0;
white-space: nowrap;
}
/* 구분선 */
.tmb-divider {
width: 1px;
+5 -4
View File
@@ -10,7 +10,7 @@ import type { AuthSession } from '../utils/auth';
import type { TradeAlertPopupLayout, TradeAlertPopupPosition } from '../utils/tradeAlertPopupLayout';
import { canAccessMenu } from '../utils/permissions';
import { isDesktop } from '../utils/platform';
import { useDesktopAppTitle, formatDesktopAppLabel } from '../hooks/useDesktopAppTitle';
import { useDesktopAppTitle } from '../hooks/useDesktopAppTitle';
import DesktopUpdateModal from './DesktopUpdateModal';
import TopMenuBarNavGroups from './TopMenuBarNavGroups';
@@ -319,9 +319,10 @@ export const TopMenuBar = memo(function TopMenuBar({
<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"/>
</svg>
<span className="tmb-logo-text">
{desktopClient ? formatDesktopAppLabel(desktopAppVersion) : 'GoldenChart'}
</span>
<span className="tmb-logo-text">GoldenChart</span>
{desktopClient && desktopAppVersion ? (
<span className="tmb-logo-version">(v{desktopAppVersion})</span>
) : null}
</div>
<div className="tmb-divider" />
+3 -10
View File
@@ -1,26 +1,19 @@
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;
}
import { readBakedDesktopAppVersion } from '../utils/desktopAppVersion';
/** Desktop — 네이티브 창 타이틀·document.title을 GoldenChart (vX.Y.Z) 로 맞춤 */
export function useDesktopAppTitle(): string | null {
const [version, setVersion] = useState<string | null>(() =>
isDesktop() ? readBakedDesktopVersion() : null,
isDesktop() ? readBakedDesktopAppVersion() : null,
);
useEffect(() => {
if (!isDesktop()) return;
let cancelled = false;
void (async () => {
const v = (await getDesktopAppVersion()) ?? readBakedDesktopVersion();
const v = (await getDesktopAppVersion()) ?? readBakedDesktopAppVersion();
if (cancelled || !v) return;
setVersion(v);
await syncDesktopMainWindowTitle();
+25
View File
@@ -0,0 +1,25 @@
declare const __DESKTOP_APP_VERSION__: string | undefined;
declare global {
interface Window {
__GC_APP_VERSION__?: string;
}
}
/** Desktop Vite 빌드·index.html 인라인 스크립트에 주입된 앱 버전 */
export function readBakedDesktopAppVersion(): string | null {
if (typeof window !== 'undefined') {
const fromWindow = window.__GC_APP_VERSION__?.trim();
if (fromWindow) return fromWindow;
}
if (typeof __DESKTOP_APP_VERSION__ !== 'undefined') {
const fromDefine = __DESKTOP_APP_VERSION__.trim();
if (fromDefine) return fromDefine;
}
return null;
}
export function formatDesktopWindowTitle(version: string | null | undefined): string {
const v = version?.trim();
return v ? `GoldenChart (v${v})` : 'GoldenChart';
}
+3 -1
View File
@@ -2,7 +2,9 @@
export type RuntimePlatform = 'web' | 'desktop' | 'mobile';
export function isDesktop(): boolean {
return typeof window !== 'undefined' && '__TAURI__' in window;
if (typeof window === 'undefined') return false;
if ('__TAURI__' in window || '__TAURI_INTERNALS__' in window) return true;
return document.documentElement.classList.contains('desktop-client');
}
export function isMobileCapacitor(): boolean {
+3 -1
View File
@@ -2,7 +2,9 @@
export type RuntimePlatform = 'web' | 'desktop' | 'mobile';
export function isDesktop(): boolean {
return typeof window !== 'undefined' && '__TAURI__' in window;
if (typeof window === 'undefined') return false;
if ('__TAURI__' in window || '__TAURI_INTERNALS__' in window) return true;
return document.documentElement.classList.contains('desktop-client');
}
export function isMobileCapacitor(): boolean {
+11
View File
@@ -64,6 +64,17 @@ fi
VERSION="$(node -p "require('$CONF').version")"
echo "Version: $VERSION"
node -e "
const fs = require('fs');
const confPath = process.argv[1];
const version = process.argv[2];
const title = 'GoldenChart (v' + version + ')';
const conf = JSON.parse(fs.readFileSync(confPath, 'utf8'));
if (conf.app?.windows?.[0]) conf.app.windows[0].title = title;
fs.writeFileSync(confPath, JSON.stringify(conf, null, 2) + '\n');
console.log('window title →', title);
" "$CONF" "$VERSION"
if [[ -f "$ROOT/scripts/patch-tauri-updater-pubkey.sh" ]]; then
chmod +x "$ROOT/scripts/patch-tauri-updater-pubkey.sh"
fi