feat(ui): frameless modern window frame with dark titlebar controls and sleek dark scrollbars
Build Windows Installer / build (push) Successful in 18m51s

This commit is contained in:
m4xx1m committed 2026-11-12 20:40:36 +03:00
1 parent 375cff78cb
commit 99d8e97f83
5 files changed
+194 -3

No files matched your search

+25
View File
@@ -288,3 +288,28 @@ func (a *App) OpenOutputFile(path string) error {
} }
return exec.Command("xdg-open", path).Start() return exec.Command("xdg-open", path).Start()
} }
func (a *App) WindowMinimise() {
if a.ctx != nil {
runtime.WindowMinimise(a.ctx)
}
}
func (a *App) WindowToggleMaximise() {
if a.ctx != nil {
runtime.WindowToggleMaximise(a.ctx)
}
}
func (a *App) WindowClose() {
if a.ctx != nil {
runtime.Quit(a.ctx)
}
}
func (a *App) WindowIsMaximised() bool {
if a.ctx != nil {
return runtime.WindowIsMaximised(a.ctx)
}
return false
}
+72 -1
View File
@@ -124,7 +124,7 @@ function renderShell() {
const root = document.getElementById('app')!; const root = document.getElementById('app')!;
const verText = state.appInfo?.version ? `v${state.appInfo.version}` : 'v2.0'; const verText = state.appInfo?.version ? `v${state.appInfo.version}` : 'v2.0';
root.innerHTML = ` root.innerHTML = `
<header class="topbar"> <header class="topbar" id="topbar">
<div class="brand"> <div class="brand">
<div class="brand-icon">▶</div> <div class="brand-icon">▶</div>
<span class="brand-title">ConvertGUI</span> <span class="brand-title">ConvertGUI</span>
@@ -140,6 +140,17 @@ function renderShell() {
<div class="topbar-right"> <div class="topbar-right">
<button id="ffmpegStatusBtn" class="status-pill warn">${tr.ffmpegMissing}</button> <button id="ffmpegStatusBtn" class="status-pill warn">${tr.ffmpegMissing}</button>
<button id="langBtn">${state.lang.toUpperCase()}</button> <button id="langBtn">${state.lang.toUpperCase()}</button>
<div class="win-controls" id="winControls">
<button id="winMinBtn" class="win-btn" title="Свернуть">
<svg width="10" height="1" viewBox="0 0 10 1"><rect width="10" height="1" fill="currentColor"/></svg>
</button>
<button id="winMaxBtn" class="win-btn" title="Развернуть">
<svg id="winMaxIcon" width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.2"><rect x="0.6" y="0.6" width="8.8" height="8.8"/></svg>
</button>
<button id="winCloseBtn" class="win-btn close" title="Закрыть">
<svg width="10" height="10" viewBox="0 0 10 10" stroke="currentColor" stroke-width="1.2"><line x1="0.5" y1="0.5" x2="9.5" y2="9.5"/><line x1="9.5" y1="0.5" x2="0.5" y2="9.5"/></svg>
</button>
</div>
</div> </div>
</header> </header>
@@ -899,6 +910,7 @@ function bindGlobalEvents() {
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
fitVideoStage(); fitVideoStage();
void syncMaximizedUI();
}); });
window.addEventListener('keydown', (e) => { window.addEventListener('keydown', (e) => {
@@ -1285,7 +1297,66 @@ function bindEvents() {
if (state.donePath) void api?.OpenOutputFile(state.donePath); if (state.donePath) void api?.OpenOutputFile(state.donePath);
}); });
// Window titlebar controls (frameless mode)
el<'button'>('winMinBtn')?.addEventListener('click', async () => {
if (window.runtime?.WindowMinimise) {
window.runtime.WindowMinimise();
} else {
await api?.WindowMinimise();
}
});
const toggleMaximize = async () => {
if (window.runtime?.WindowToggleMaximise) {
window.runtime.WindowToggleMaximise();
} else {
await api?.WindowToggleMaximise();
}
await syncMaximizedUI();
};
el<'button'>('winMaxBtn')?.addEventListener('click', () => {
void toggleMaximize();
});
el<'button'>('winCloseBtn')?.addEventListener('click', async () => {
if (window.runtime?.WindowClose) {
window.runtime.WindowClose();
} else {
await api?.WindowClose();
}
});
el<'header'>('topbar')?.addEventListener('dblclick', (e) => {
const target = e.target as HTMLElement;
if (target.closest('button, input, select, .file-pill')) return;
void toggleMaximize();
});
bindWheelControls(); bindWheelControls();
void syncMaximizedUI();
}
async function syncMaximizedUI() {
const api = appApi();
let isMax = false;
try {
if (window.runtime?.WindowIsMaximised) {
isMax = await window.runtime.WindowIsMaximised();
} else if (api?.WindowIsMaximised) {
isMax = await api.WindowIsMaximised();
}
} catch {
// ignore
}
document.body.classList.toggle('maximized', isMax);
const maxBtn = el<'button'>('winMaxBtn');
if (maxBtn) {
maxBtn.innerHTML = isMax
? `<svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.1"><rect x="2.5" y="0.5" width="7" height="7"/><polyline points="2.5,2.5 0.5,2.5 0.5,9.5 7.5,9.5 7.5,7.5"/></svg>`
: `<svg id="winMaxIcon" width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.2"><rect x="0.6" y="0.6" width="8.8" height="8.8"/></svg>`;
maxBtn.title = isMax ? 'Восстановить' : 'Развернуть';
}
} }
function setupWailsEvents() { function setupWailsEvents() {
+81 -1
View File
@@ -32,22 +32,102 @@ body {
overflow: hidden; overflow: hidden;
} }
/* Custom sleek dark scrollbars (eliminates default white Windows scrollbars) */
::-webkit-scrollbar {
width: 7px;
height: 7px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.15);
border-radius: 999px;
border: 1px solid transparent;
background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
background-clip: padding-box;
}
* {
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.18) transparent;
}
#app { #app {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100vh; height: 100vh;
box-sizing: border-box;
background: var(--bg);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
transition: border-radius 0.15s ease, border 0.15s ease;
}
body.maximized #app {
border: none;
border-radius: 0;
box-shadow: none;
} }
/* Top Bar */ /* Top Bar */
.topbar { .topbar {
--wails-draggable: drag;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 12px; gap: 12px;
padding: 10px 16px; padding: 6px 0 6px 14px;
background: var(--panel); background: var(--panel);
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
flex-shrink: 0; flex-shrink: 0;
user-select: none;
}
.topbar button,
.topbar input,
.topbar select,
.topbar .file-pill,
.win-controls {
--wails-draggable: no-drag;
}
/* Window Control Buttons (Minimize, Maximize, Close) */
.win-controls {
display: flex;
align-items: stretch;
height: 36px;
margin-left: 4px;
--wails-draggable: no-drag;
}
.win-btn {
width: 44px;
height: 100%;
border: none !important;
border-radius: 0 !important;
background: transparent !important;
color: var(--muted) !important;
display: flex !important;
align-items: center;
justify-content: center;
cursor: pointer;
padding: 0 !important;
transition: background 0.12s ease, color 0.12s ease;
}
.win-btn:hover {
background: rgba(255, 255, 255, 0.08) !important;
color: var(--text) !important;
}
.win-btn.close:hover {
background: #e81123 !important;
color: #fff !important;
} }
.brand { .brand {
+8
View File
@@ -122,6 +122,10 @@ declare global {
CancelConvert(): Promise<boolean>; CancelConvert(): Promise<boolean>;
OpenInExplorer(path: string): Promise<void>; OpenInExplorer(path: string): Promise<void>;
OpenOutputFile(path: string): Promise<void>; OpenOutputFile(path: string): Promise<void>;
WindowMinimise(): Promise<void>;
WindowToggleMaximise(): Promise<void>;
WindowClose(): Promise<void>;
WindowIsMaximised(): Promise<boolean>;
}; };
}; };
}; };
@@ -129,6 +133,10 @@ declare global {
EventsOn(eventName: string, callback: (...args: any[]) => void): () => void; EventsOn(eventName: string, callback: (...args: any[]) => void): () => void;
OnFileDrop?(callback: (x: number, y: number, paths: string[]) => void, useDropTarget?: boolean): void; OnFileDrop?(callback: (x: number, y: number, paths: string[]) => void, useDropTarget?: boolean): void;
OnFileDropOff?(): void; OnFileDropOff?(): void;
WindowMinimise?(): void;
WindowToggleMaximise?(): void;
WindowClose?(): void;
WindowIsMaximised?(): Promise<boolean>;
}; };
} }
} }
+8 -1
View File
@@ -46,7 +46,8 @@ func main() {
AssetServer: &assetserver.Options{ AssetServer: &assetserver.Options{
Assets: assets, Assets: assets,
}, },
BackgroundColour: &options.RGBA{R: 13, G: 17, B: 23, A: 1}, BackgroundColour: &options.RGBA{R: 11, G: 15, B: 23, A: 1},
Frameless: true,
OnStartup: app.startup, OnStartup: app.startup,
OnShutdown: app.shutdown, OnShutdown: app.shutdown,
DragAndDrop: &options.DragAndDrop{ DragAndDrop: &options.DragAndDrop{
@@ -56,7 +57,13 @@ func main() {
Windows: &windows.Options{ Windows: &windows.Options{
WebviewIsTransparent: false, WebviewIsTransparent: false,
WindowIsTranslucent: false, WindowIsTranslucent: false,
DisableFramelessWindowDecorations: false,
Theme: windows.Dark, Theme: windows.Dark,
CustomTheme: &windows.ThemeSettings{
DarkModeTitleBar: windows.RGB(17, 24, 39),
DarkModeTitleText: windows.RGB(241, 245, 249),
DarkModeBorder: windows.RGB(36, 48, 68),
},
}, },
Bind: []interface{}{ Bind: []interface{}{
app, app,