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()
|
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 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
@@ -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 {
|
||||||
|
|||||||
Vendored
+8
@@ -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>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
Reference in new issue
Block a user