feat(ui): frameless modern window frame with dark titlebar controls and sleek dark scrollbars
Build Windows Installer / build (push) Successful in 18m51s
Build Windows Installer / build (push) Successful in 18m51s
This commit is contained in:
5 files changed
+194
-3
No files matched your search
@@ -288,3 +288,28 @@ func (a *App) OpenOutputFile(path string) error {
|
||||
}
|
||||
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
@@ -124,7 +124,7 @@ function renderShell() {
|
||||
const root = document.getElementById('app')!;
|
||||
const verText = state.appInfo?.version ? `v${state.appInfo.version}` : 'v2.0';
|
||||
root.innerHTML = `
|
||||
<header class="topbar">
|
||||
<header class="topbar" id="topbar">
|
||||
<div class="brand">
|
||||
<div class="brand-icon">▶</div>
|
||||
<span class="brand-title">ConvertGUI</span>
|
||||
@@ -140,6 +140,17 @@ function renderShell() {
|
||||
<div class="topbar-right">
|
||||
<button id="ffmpegStatusBtn" class="status-pill warn">${tr.ffmpegMissing}</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>
|
||||
</header>
|
||||
|
||||
@@ -899,6 +910,7 @@ function bindGlobalEvents() {
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
fitVideoStage();
|
||||
void syncMaximizedUI();
|
||||
});
|
||||
|
||||
window.addEventListener('keydown', (e) => {
|
||||
@@ -1285,7 +1297,66 @@ function bindEvents() {
|
||||
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();
|
||||
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() {
|
||||
|
||||
+81
-1
@@ -32,22 +32,102 @@ body {
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
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 */
|
||||
.topbar {
|
||||
--wails-draggable: drag;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 10px 16px;
|
||||
padding: 6px 0 6px 14px;
|
||||
background: var(--panel);
|
||||
border-bottom: 1px solid var(--border);
|
||||
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 {
|
||||
|
||||
Vendored
+8
@@ -122,6 +122,10 @@ declare global {
|
||||
CancelConvert(): Promise<boolean>;
|
||||
OpenInExplorer(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;
|
||||
OnFileDrop?(callback: (x: number, y: number, paths: string[]) => void, useDropTarget?: boolean): void;
|
||||
OnFileDropOff?(): void;
|
||||
WindowMinimise?(): void;
|
||||
WindowToggleMaximise?(): void;
|
||||
WindowClose?(): void;
|
||||
WindowIsMaximised?(): Promise<boolean>;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,8 @@ func main() {
|
||||
AssetServer: &assetserver.Options{
|
||||
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,
|
||||
OnShutdown: app.shutdown,
|
||||
DragAndDrop: &options.DragAndDrop{
|
||||
@@ -56,7 +57,13 @@ func main() {
|
||||
Windows: &windows.Options{
|
||||
WebviewIsTransparent: false,
|
||||
WindowIsTranslucent: false,
|
||||
DisableFramelessWindowDecorations: false,
|
||||
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{}{
|
||||
app,
|
||||
|
||||
Reference in new issue
Block a user