From 6fbe16a84bb4bde4dcc3c00772ff5c12932debe8 Mon Sep 17 00:00:00 2001 From: Caleb Allen Date: Mon, 16 Feb 2026 03:48:53 -0500 Subject: [PATCH] window size saved to config, smaller font in track-list --- backend/app.go | 22 + backend/config/config.go | 14 + backend/config/window.go | 33 ++ .../src/components/track-list/track-list.ts | 393 +++++++++--------- main.go | 7 +- 5 files changed, 271 insertions(+), 198 deletions(-) create mode 100644 backend/config/window.go diff --git a/backend/app.go b/backend/app.go index 172a03c..1f75ddf 100644 --- a/backend/app.go +++ b/backend/app.go @@ -107,6 +107,11 @@ func NewYellowJacketApp( return yjApp, nil } +// WindowConfig returns the window configuration for use by the host. +func (yj *YellowJacketApp) WindowConfig() *config.WindowConfig { + return yj.appConfig.Window +} + var startupErr error // OnStartup initializes components that require the Wails runtime context. @@ -143,6 +148,23 @@ func (yj *YellowJacketApp) OnStartup(ctx context.Context) { yj.FEBindings = append(yj.FEBindings, yj.player) } +// OnBeforeClose captures window state while the window is still alive. +func (yj *YellowJacketApp) OnBeforeClose(ctx context.Context) bool { + w, h := wailsruntime.WindowGetSize(ctx) + + yj.appConfig.Window.Width = w + yj.appConfig.Window.Height = h + + if err := yj.appConfig.Save(); err != nil { + yj.logger.Error( + "Failed to save window state", + "err", err, + ) + } + + return false +} + // OnShutdown saves player state and cleans up resources before the application exits. func (yj *YellowJacketApp) OnShutdown(_ context.Context) { if yj.player != nil { diff --git a/backend/config/config.go b/backend/config/config.go index 5cf67a7..3115d62 100644 --- a/backend/config/config.go +++ b/backend/config/config.go @@ -23,6 +23,8 @@ type Config struct { serveMux *http.ServeMux filePath string // required Library *library.Config `form:"Library" schema:"library,required"` + + Window *WindowConfig `toml:"Window"` } // NewConfig creates a new config by loading it from disk. @@ -36,6 +38,7 @@ func NewConfig(logger *slog.Logger) (*Config, error) { filePath: path.Join(confDir, "config.toml"), serveMux: http.NewServeMux(), } + conf.applyDefaults() conf.logger = logger.WithGroup("config").With("config", conf) conf.serveMux.HandleFunc("/", conf.handle) @@ -99,6 +102,8 @@ func (c *Config) Load() error { return fmt.Errorf("problem parsing config file %s: %w", c.filePath, err) } + c.applyDefaults() + // validate the config if err = c.Validate(); err != nil { return fmt.Errorf("invalid config file at %s: %w", c.filePath, err) @@ -130,6 +135,15 @@ func (c *Config) Save() error { return nil } +// applyDefaults ensures all config sections have valid defaults. +func (c *Config) applyDefaults() { + if c.Window == nil { + c.Window = NewDefaultWindowConfig() + } else { + c.Window.applyDefaults() + } +} + // SetContext sets the Wails runtime context for event emission. func (c *Config) SetContext(ctx context.Context) { c.ctx = ctx diff --git a/backend/config/window.go b/backend/config/window.go new file mode 100644 index 0000000..f34faf8 --- /dev/null +++ b/backend/config/window.go @@ -0,0 +1,33 @@ +package config + +const ( + // DefaultWidth is the default window width in pixels. + DefaultWidth = 512 + // DefaultHeight is the default window height in pixels. + DefaultHeight = 384 +) + +// WindowConfig holds window size preferences. +type WindowConfig struct { + Width int `toml:"Width"` + Height int `toml:"Height"` +} + +// NewDefaultWindowConfig returns a WindowConfig with sensible defaults. +func NewDefaultWindowConfig() *WindowConfig { + return &WindowConfig{ + Width: DefaultWidth, + Height: DefaultHeight, + } +} + +// applyDefaults fills in zero-value fields with defaults. +func (w *WindowConfig) applyDefaults() { + if w.Width <= 0 { + w.Width = DefaultWidth + } + + if w.Height <= 0 { + w.Height = DefaultHeight + } +} diff --git a/frontend/src/components/track-list/track-list.ts b/frontend/src/components/track-list/track-list.ts index a82d183..b736c87 100644 --- a/frontend/src/components/track-list/track-list.ts +++ b/frontend/src/components/track-list/track-list.ts @@ -16,30 +16,30 @@ import type { PlaylistPicker } from '@components/playlist-picker/playlist-picker @customElement('track-list') export class TrackList extends LitElement { - private player = new PlayerController(this); - private queue = new QueueController(this); + private player = new PlayerController(this); + private queue = new QueueController(this); - @state() - private tracks: library.Track[] = []; + @state() + private tracks: library.Track[] = []; - @state() - private selectedTracks: Set = new Set(); + @state() + private selectedTracks: Set = new Set(); - @state() - private contextMenuOpen = false; + @state() + private contextMenuOpen = false; - @state() - private playlistSubmenuOpen = false; + @state() + private playlistSubmenuOpen = false; - @query('#context-menu') - private contextMenuPopup!: HTMLElement; + @query('#context-menu') + private contextMenuPopup!: HTMLElement; - @query('#playlist-submenu') - private playlistSubmenuPopup!: HTMLElement; + @query('#playlist-submenu') + private playlistSubmenuPopup!: HTMLElement; - private closeHandler = () => this.closeContextMenu(); + private closeHandler = () => this.closeContextMenu(); - static override styles = css` + static override styles = css` :host { display: flex; flex-direction: column; @@ -64,6 +64,7 @@ export class TrackList extends LitElement { .track-row { display: grid; grid-template-columns: 1fr 1fr 80px; + font-size: 12px; padding: 8px; border-bottom: 1px solid #333; align-items: center; @@ -152,198 +153,198 @@ export class TrackList extends LitElement { } `; - override connectedCallback() { - super.connectedCallback(); - this.loadTracks(); - document.addEventListener('click', this.closeHandler); - document.addEventListener('contextmenu', this.closeHandler); - } - - override disconnectedCallback() { - super.disconnectedCallback(); - document.removeEventListener('click', this.closeHandler); - document.removeEventListener('contextmenu', this.closeHandler); - } - - async loadTracks() { - try { - const tracks = await GetAllTracks(); - this.tracks = tracks; - this.selectedTracks = new Set(); - - if (tracks[0]) { - LogPrint(tracks[0].TrackName); - } - } catch (error) { - console.error('Error loading tracks:', error); - } - } - - private getSelectedFilePaths(): string[] { - return this.tracks - .filter((t) => this.selectedTracks.has(t.FilePath)) - .map((t) => t.FilePath); - } - - private onTrackRowClick(e: MouseEvent, track: library.Track) { - const isCtrl = e.ctrlKey || e.metaKey; - - if (isCtrl) { - const next = new Set(this.selectedTracks); - - if (next.has(track.FilePath)) { - next.delete(track.FilePath); - } else { - next.add(track.FilePath); - } - - this.selectedTracks = next; - } else { - this.selectedTracks = new Set([track.FilePath]); - } - } - - private onTrackRowDblClick(track: library.Track) { - this.selectedTracks = new Set(); - this.queue.setQueue([track.FilePath], 0); - } - - private onTrackContextMenu(e: MouseEvent, track: library.Track) { - e.preventDefault(); - e.stopPropagation(); - - if (!this.selectedTracks.has(track.FilePath)) { - this.selectedTracks = new Set([track.FilePath]); + override connectedCallback() { + super.connectedCallback(); + this.loadTracks(); + document.addEventListener('click', this.closeHandler); + document.addEventListener('contextmenu', this.closeHandler); } - this.contextMenuOpen = true; - - // Position the popup at the mouse cursor using a virtual anchor. - this.updateComplete.then(() => { - const popup = this.contextMenuPopup; - - if (popup) { - (popup as any).anchor = { - getBoundingClientRect() { - return { - width: 0, - height: 0, - x: e.clientX, - y: e.clientY, - top: e.clientY, - left: e.clientX, - right: e.clientX, - bottom: e.clientY, - }; - }, - }; - (popup as any).active = true; - } - }); - } - - private onContextMenuAction(action: string) { - const filePaths = this.getSelectedFilePaths(); - - if (filePaths.length === 0) return; - - switch (action) { - case 'play': - this.queue.setQueue(filePaths, 0); - break; - case 'add-to-queue': - this.queue.addTracksToQueue(filePaths); - break; - case 'play-next': - this.queue.playTracksNext(filePaths); - break; + override disconnectedCallback() { + super.disconnectedCallback(); + document.removeEventListener('click', this.closeHandler); + document.removeEventListener('contextmenu', this.closeHandler); } - this.closeContextMenu(true); - } + async loadTracks() { + try { + const tracks = await GetAllTracks(); + this.tracks = tracks; + this.selectedTracks = new Set(); - private closeContextMenu(clearSelection = false) { - if (!this.contextMenuOpen) return; - - this.closePlaylistSubmenu(); - this.contextMenuOpen = false; - - if (clearSelection) { - this.selectedTracks = new Set(); + if (tracks[0]) { + LogPrint(tracks[0].TrackName); + } + } catch (error) { + console.error('Error loading tracks:', error); + } } - const popup = this.contextMenuPopup; - - if (popup) { - (popup as any).active = false; - } - } - - private async showPlaylistSubmenu() { - if (this.playlistSubmenuOpen) return; - - this.playlistSubmenuOpen = true; - - await this.updateComplete; - - const submenu = this.playlistSubmenuPopup; - const trigger = this.shadowRoot?.querySelector('.submenu-item'); - - if (submenu && trigger) { - (submenu as any).anchor = trigger; - (submenu as any).active = true; + private getSelectedFilePaths(): string[] { + return this.tracks + .filter((t) => this.selectedTracks.has(t.FilePath)) + .map((t) => t.FilePath); } - const picker = this.shadowRoot?.querySelector( - 'playlist-picker', - ) as PlaylistPicker | null; + private onTrackRowClick(e: MouseEvent, track: library.Track) { + const isCtrl = e.ctrlKey || e.metaKey; - picker?.reset(); - } + if (isCtrl) { + const next = new Set(this.selectedTracks); - private closePlaylistSubmenu() { - if (!this.playlistSubmenuOpen) return; + if (next.has(track.FilePath)) { + next.delete(track.FilePath); + } else { + next.add(track.FilePath); + } - this.playlistSubmenuOpen = false; - - const submenu = this.playlistSubmenuPopup; - - if (submenu) { - (submenu as any).active = false; + this.selectedTracks = next; + } else { + this.selectedTracks = new Set([track.FilePath]); + } } - } - private onPlaylistActionComplete = () => { - this.closeContextMenu(true); - }; + private onTrackRowDblClick(track: library.Track) { + this.selectedTracks = new Set(); + this.queue.setQueue([track.FilePath], 0); + } - private isActiveTrack(track: library.Track): boolean { - const currentTrack = this.player.currentTrack; + private onTrackContextMenu(e: MouseEvent, track: library.Track) { + e.preventDefault(); + e.stopPropagation(); - if (!currentTrack) return false; + if (!this.selectedTracks.has(track.FilePath)) { + this.selectedTracks = new Set([track.FilePath]); + } - return currentTrack.filePath === track.FilePath; - } + this.contextMenuOpen = true; - private renderTrackRow = (track: library.Track): unknown => { - const active = this.isActiveTrack(track); - const selected = this.selectedTracks.has(track.FilePath); + // Position the popup at the mouse cursor using a virtual anchor. + this.updateComplete.then(() => { + const popup = this.contextMenuPopup; - const classes = [ - 'track-row', - active ? 'active' : '', - selected ? 'selected' : '', - ] - .filter(Boolean) - .join(' '); + if (popup) { + (popup as any).anchor = { + getBoundingClientRect() { + return { + width: 0, + height: 0, + x: e.clientX, + y: e.clientY, + top: e.clientY, + left: e.clientX, + right: e.clientX, + bottom: e.clientY, + }; + }, + }; + (popup as any).active = true; + } + }); + } - return html` + private onContextMenuAction(action: string) { + const filePaths = this.getSelectedFilePaths(); + + if (filePaths.length === 0) return; + + switch (action) { + case 'play': + this.queue.setQueue(filePaths, 0); + break; + case 'add-to-queue': + this.queue.addTracksToQueue(filePaths); + break; + case 'play-next': + this.queue.playTracksNext(filePaths); + break; + } + + this.closeContextMenu(true); + } + + private closeContextMenu(clearSelection = false) { + if (!this.contextMenuOpen) return; + + this.closePlaylistSubmenu(); + this.contextMenuOpen = false; + + if (clearSelection) { + this.selectedTracks = new Set(); + } + + const popup = this.contextMenuPopup; + + if (popup) { + (popup as any).active = false; + } + } + + private async showPlaylistSubmenu() { + if (this.playlistSubmenuOpen) return; + + this.playlistSubmenuOpen = true; + + await this.updateComplete; + + const submenu = this.playlistSubmenuPopup; + const trigger = this.shadowRoot?.querySelector('.submenu-item'); + + if (submenu && trigger) { + (submenu as any).anchor = trigger; + (submenu as any).active = true; + } + + const picker = this.shadowRoot?.querySelector( + 'playlist-picker', + ) as PlaylistPicker | null; + + picker?.reset(); + } + + private closePlaylistSubmenu() { + if (!this.playlistSubmenuOpen) return; + + this.playlistSubmenuOpen = false; + + const submenu = this.playlistSubmenuPopup; + + if (submenu) { + (submenu as any).active = false; + } + } + + private onPlaylistActionComplete = () => { + this.closeContextMenu(true); + }; + + private isActiveTrack(track: library.Track): boolean { + const currentTrack = this.player.currentTrack; + + if (!currentTrack) return false; + + return currentTrack.filePath === track.FilePath; + } + + private renderTrackRow = (track: library.Track): unknown => { + const active = this.isActiveTrack(track); + const selected = this.selectedTracks.has(track.FilePath); + + const classes = [ + 'track-row', + active ? 'active' : '', + selected ? 'selected' : '', + ] + .filter(Boolean) + .join(' '); + + return html`
this.onTrackRowClick(e, track)} @dblclick=${() => this.onTrackRowDblClick(track)} @contextmenu=${(e: MouseEvent) => - this.onTrackContextMenu(e, track)} + this.onTrackContextMenu(e, track)} >
${track.TrackName}
${track.ArtistName}
@@ -352,13 +353,13 @@ export class TrackList extends LitElement {
`; - }; + }; - override render() { - return html` + override render() { + return html` ${this.tracks.length === 0 - ? html`

Loading tracks...

` - : html` + ? html`

Loading tracks...

` + : html`
Track Name Artist @@ -378,7 +379,7 @@ export class TrackList extends LitElement { .active=${this.contextMenuOpen} > ${this.contextMenuOpen - ? html` + ? html`
this.onContextMenuAction('play')} @@ -402,9 +403,9 @@ export class TrackList extends LitElement { class="submenu-item" @mouseenter=${() => this.showPlaylistSubmenu()} @click=${(e: Event) => { - e.stopPropagation(); - void this.showPlaylistSubmenu(); - }} + e.stopPropagation(); + void this.showPlaylistSubmenu(); + }} > Add to Playlist @@ -412,7 +413,7 @@ export class TrackList extends LitElement {
` - : nothing} + : nothing} ${this.playlistSubmenuOpen && this.selectedTracks.size > 0 - ? html` + ? html` e.stopPropagation()} > ` - : nothing} + : nothing} `; - } + } } diff --git a/main.go b/main.go index 4919945..0a4d910 100644 --- a/main.go +++ b/main.go @@ -57,10 +57,12 @@ func main() { } // Create application with options + winCfg := yjApp.WindowConfig() + err = wails.Run(&options.App{ Title: "yellowjacket", - Width: 512, - Height: 384, + Width: winCfg.Width, + Height: winCfg.Height, Logger: logging.NewLogger( sLogger, []string{}, @@ -69,6 +71,7 @@ func main() { BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, OnStartup: yjApp.OnStartup, OnDomReady: yjApp.OnDomReady, + OnBeforeClose: yjApp.OnBeforeClose, OnShutdown: yjApp.OnShutdown, Bind: yjApp.FEBindings, MinWidth: 512,