diff --git a/backend/app.go b/backend/app.go index a4dd325..fd3da81 100644 --- a/backend/app.go +++ b/backend/app.go @@ -103,6 +103,7 @@ func NewYellowJacketApp( yjApp.playlist = playlist.NewService( yjApp.logger, yjApp.database, yjApp.appConfig, ) + yjApp.playlist.SetFavoritesConfig(yjApp.appConfig) // create queue (before wails.Run so it can be bound) yjApp.queue = queue.NewQueue(yjApp.logger, yjApp.database) @@ -145,6 +146,7 @@ func (yj *YellowJacketApp) OnStartup(ctx context.Context) { yj.FrontendUtil.SetContext(ctx) yj.library.SetContext(ctx) yj.playlist.SetContext(ctx) + yj.playlist.EnsureDefaultPlaylist() // Initialize speaker hardware (player struct created in // NewYellowJacketApp for Wails binding registration). diff --git a/backend/config/config.go b/backend/config/config.go index 0e4c176..b57a484 100644 --- a/backend/config/config.go +++ b/backend/config/config.go @@ -13,6 +13,7 @@ import ( "github.com/wailsapp/wails/v2/pkg/runtime" "yellowjacket/backend/events" + "yellowjacket/backend/favorites" "yellowjacket/backend/library" "yellowjacket/backend/system" "yellowjacket/backend/theme" @@ -28,6 +29,7 @@ type Config struct { Theme *theme.Config `toml:"Theme"` Window *WindowConfig `toml:"Window"` TrackList *tracklist.Config `toml:"TrackList"` + Favorites *favorites.Config `toml:"Favorites"` } // NewConfig creates a new config by loading it from disk. @@ -78,6 +80,12 @@ func (c *Config) Validate() error { } } + if c.Favorites != nil { + if err := c.Favorites.Validate(); err != nil { + configErrs = errors.Join(configErrs, err) + } + } + if configErrs != nil { return fmt.Errorf( "one or more config parts are invalid: %w", @@ -174,6 +182,12 @@ func (c *Config) applyDefaults() { } c.TrackList.ApplyDefaults() + + if c.Favorites == nil { + c.Favorites = &favorites.Config{} + } + + c.Favorites.ApplyDefaults() } // SetContext sets the Wails runtime context for event emission. @@ -436,3 +450,96 @@ func (c *Config) emitTrackListChanged() { }, ) } + +// GetFavoritesPlaylistID returns the configured default playlist ID. +func (c *Config) GetFavoritesPlaylistID() int64 { + if c.Favorites == nil { + return 0 + } + + return c.Favorites.PlaylistID +} + +// SetFavoritesPlaylistID saves a new default playlist ID. +func (c *Config) SetFavoritesPlaylistID(id int64) error { + if c.Favorites == nil { + c.Favorites = &favorites.Config{} + c.Favorites.ApplyDefaults() + } + + c.Favorites.PlaylistID = id + + if err := c.Save(); err != nil { + return fmt.Errorf( + "could not save config: %w", err, + ) + } + + c.emitFavoritesChanged() + + c.logger.Info( + "favorites playlist ID updated", + "playlistId", id, + ) + + return nil +} + +// GetFavoritesIconStyle returns the configured icon style. +func (c *Config) GetFavoritesIconStyle() string { + if c.Favorites == nil { + return string(favorites.DefaultIconStyle) + } + + return string(c.Favorites.IconStyle) +} + +// SetFavoritesIconStyle validates and saves a new icon style. +func (c *Config) SetFavoritesIconStyle( + style string, +) error { + if c.Favorites == nil { + c.Favorites = &favorites.Config{} + c.Favorites.ApplyDefaults() + } + + c.Favorites.IconStyle = favorites.IconStyle(style) + + if err := c.Favorites.Validate(); err != nil { + return fmt.Errorf( + "invalid favorites icon style: %w", err, + ) + } + + if err := c.Save(); err != nil { + return fmt.Errorf( + "could not save config: %w", err, + ) + } + + c.emitFavoritesChanged() + + c.logger.Info( + "favorites icon style updated", + "style", style, + ) + + return nil +} + +// emitFavoritesChanged sends the FavoritesConfigChanged event +// to the frontend. +func (c *Config) emitFavoritesChanged() { + if c.ctx == nil || c.Favorites == nil { + return + } + + runtime.EventsEmit( + c.ctx, + events.FavoritesConfigChanged, + map[string]any{ + "PlaylistID": c.Favorites.PlaylistID, + "IconStyle": string(c.Favorites.IconStyle), + }, + ) +} diff --git a/backend/database/sql/queries/playlists.sql b/backend/database/sql/queries/playlists.sql index 870b51c..351697e 100644 --- a/backend/database/sql/queries/playlists.sql +++ b/backend/database/sql/queries/playlists.sql @@ -88,3 +88,23 @@ DELETE FROM playlist_tracks; -- name: GetNextPlaylistTrackPosition :one SELECT COALESCE(MAX(position), -1) + 1 AS next_position FROM playlist_tracks WHERE playlist_id = ?; + +-- name: GetPlaylistTrackFilePaths :many +SELECT af.file_path +FROM playlist_tracks pt +JOIN audio_files af ON pt.audio_file_id = af.id +WHERE pt.playlist_id = ? +ORDER BY pt.position; + +-- name: IsTrackInPlaylist :one +SELECT EXISTS( + SELECT 1 FROM playlist_tracks pt + JOIN audio_files af ON pt.audio_file_id = af.id + WHERE pt.playlist_id = ? AND af.file_path = ? +) AS in_playlist; + +-- name: RemovePlaylistTrackByPath :exec +DELETE FROM playlist_tracks +WHERE playlist_id = ? AND audio_file_id = ( + SELECT id FROM audio_files WHERE file_path = ? +); diff --git a/backend/database/sql/sqlcgen/playlists.sql.go b/backend/database/sql/sqlcgen/playlists.sql.go index 83bcba5..13200dd 100644 --- a/backend/database/sql/sqlcgen/playlists.sql.go +++ b/backend/database/sql/sqlcgen/playlists.sql.go @@ -209,6 +209,37 @@ func (q *Queries) GetPlaylist(ctx context.Context, id int64) (Playlist, error) { return i, err } +const getPlaylistTrackFilePaths = `-- name: GetPlaylistTrackFilePaths :many +SELECT af.file_path +FROM playlist_tracks pt +JOIN audio_files af ON pt.audio_file_id = af.id +WHERE pt.playlist_id = ? +ORDER BY pt.position +` + +func (q *Queries) GetPlaylistTrackFilePaths(ctx context.Context, playlistID int64) ([]string, error) { + rows, err := q.db.QueryContext(ctx, getPlaylistTrackFilePaths, playlistID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []string + for rows.Next() { + var file_path string + if err := rows.Scan(&file_path); err != nil { + return nil, err + } + items = append(items, file_path) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getPlaylistTracks = `-- name: GetPlaylistTracks :many SELECT pt.id, pt.playlist_id, pt.audio_file_id, pt.position, af.file_path FROM playlist_tracks pt @@ -328,6 +359,26 @@ func (q *Queries) GetPlaylistTracksWithMetadata(ctx context.Context, playlistID return items, nil } +const isTrackInPlaylist = `-- name: IsTrackInPlaylist :one +SELECT EXISTS( + SELECT 1 FROM playlist_tracks pt + JOIN audio_files af ON pt.audio_file_id = af.id + WHERE pt.playlist_id = ? AND af.file_path = ? +) AS in_playlist +` + +type IsTrackInPlaylistParams struct { + PlaylistID int64 + FilePath string +} + +func (q *Queries) IsTrackInPlaylist(ctx context.Context, arg IsTrackInPlaylistParams) (int64, error) { + row := q.db.QueryRowContext(ctx, isTrackInPlaylist, arg.PlaylistID, arg.FilePath) + var in_playlist int64 + err := row.Scan(&in_playlist) + return in_playlist, err +} + const removePlaylistTrack = `-- name: RemovePlaylistTrack :exec DELETE FROM playlist_tracks WHERE id = ? ` @@ -337,6 +388,23 @@ func (q *Queries) RemovePlaylistTrack(ctx context.Context, id int64) error { return err } +const removePlaylistTrackByPath = `-- name: RemovePlaylistTrackByPath :exec +DELETE FROM playlist_tracks +WHERE playlist_id = ? AND audio_file_id = ( + SELECT id FROM audio_files WHERE file_path = ? +) +` + +type RemovePlaylistTrackByPathParams struct { + PlaylistID int64 + FilePath string +} + +func (q *Queries) RemovePlaylistTrackByPath(ctx context.Context, arg RemovePlaylistTrackByPathParams) error { + _, err := q.db.ExecContext(ctx, removePlaylistTrackByPath, arg.PlaylistID, arg.FilePath) + return err +} + const updatePlaylistName = `-- name: UpdatePlaylistName :exec UPDATE playlists SET name = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? ` diff --git a/backend/events/events.go b/backend/events/events.go index df48c83..9b2c263 100644 --- a/backend/events/events.go +++ b/backend/events/events.go @@ -25,15 +25,17 @@ const ( LibraryConfigChanged = "LibraryConfigChanged" ThemeConfigChanged = "ThemeConfigChanged" TrackListConfigChanged = "TrackListConfigChanged" + FavoritesConfigChanged = "FavoritesConfigChanged" ) // Playlist events. const ( - PlaylistCreated = "PlaylistCreated" - PlaylistDeleted = "PlaylistDeleted" - PlaylistRenamed = "PlaylistRenamed" - PlaylistTracksChanged = "PlaylistTracksChanged" - PlaylistsRestored = "PlaylistsRestored" + PlaylistCreated = "PlaylistCreated" + PlaylistDeleted = "PlaylistDeleted" + PlaylistRenamed = "PlaylistRenamed" + PlaylistTracksChanged = "PlaylistTracksChanged" + PlaylistsRestored = "PlaylistsRestored" + DefaultPlaylistChanged = "DefaultPlaylistChanged" ) // Library events. diff --git a/backend/favorites/config.go b/backend/favorites/config.go new file mode 100644 index 0000000..658a5c1 --- /dev/null +++ b/backend/favorites/config.go @@ -0,0 +1,61 @@ +// Package favorites manages the default playlist configuration. +package favorites + +import ( + "errors" + "fmt" +) + +var errUnknownIconStyle = errors.New( + "unknown favorites icon style", +) + +// IconStyle controls the icon used to indicate favourited tracks. +type IconStyle string + +// Valid IconStyle values. +const ( + // IconHeart uses a heart icon. + IconHeart IconStyle = "heart" + + // IconStar uses a star icon. + IconStar IconStyle = "star" +) + +// DefaultIconStyle is applied when no value has been set. +const DefaultIconStyle = IconHeart + +// DefaultPlaylistName is the name given to the auto-created +// default playlist. +const DefaultPlaylistName = "Favorites" + +// Config holds favourites preferences. +type Config struct { + PlaylistID int64 `toml:"PlaylistID"` + IconStyle IconStyle `toml:"IconStyle"` +} + +// ApplyDefaults fills zero-value fields with sensible defaults. +func (c *Config) ApplyDefaults() { + if c.IconStyle == "" { + c.IconStyle = DefaultIconStyle + } +} + +// Validate checks that all values are well-formed. +func (c *Config) Validate() error { + c.ApplyDefaults() + + switch c.IconStyle { + case IconHeart, IconStar: + // Valid. + default: + return fmt.Errorf( + "%w: %q", + errUnknownIconStyle, + c.IconStyle, + ) + } + + return nil +} diff --git a/backend/playlist/favorites.go b/backend/playlist/favorites.go new file mode 100644 index 0000000..05cd51f --- /dev/null +++ b/backend/playlist/favorites.go @@ -0,0 +1,348 @@ +package playlist + +import ( + "errors" + "fmt" + + "yellowjacket/backend/database/sql/sqlcgen" + "yellowjacket/backend/events" + "yellowjacket/backend/favorites" +) + +var errNoDefaultPlaylist = errors.New( + "no default playlist configured", +) + +// FavoritesConfigProvider is a narrow interface for reading and +// writing the default-playlist configuration. +type FavoritesConfigProvider interface { + GetFavoritesPlaylistID() int64 + SetFavoritesPlaylistID(id int64) error + GetFavoritesIconStyle() string +} + +// EnsureDefaultPlaylist verifies the configured default playlist +// exists in the database. If the playlist is missing or no ID +// has been configured yet, a new playlist named "Favorites" is +// created and the config is updated. +func (s *Service) EnsureDefaultPlaylist() { + if s.favoritesConf == nil { + s.logger.Warn( + "No favorites config provider, skipping", + ) + + return + } + + id := s.favoritesConf.GetFavoritesPlaylistID() + + // Check whether the playlist still exists. + if id > 0 { + _, err := s.db.Queries.GetPlaylist( + s.db.Ctx, id, + ) + if err == nil { + return // Playlist exists, nothing to do. + } + + s.logger.Warn( + "Default playlist not found, recreating", + "configuredId", id, + ) + } + + // Create a fresh default playlist. + created, err := s.db.Queries.CreatePlaylist( + s.db.Ctx, favorites.DefaultPlaylistName, + ) + if err != nil { + s.logger.Error( + "Failed to create default playlist", + "err", err, + ) + + return + } + + s.savePlaylistFile(created.ID, created.Name) + + if setErr := s.favoritesConf.SetFavoritesPlaylistID( + created.ID, + ); setErr != nil { + s.logger.Error( + "Failed to save default playlist ID", + "err", setErr, + ) + } + + s.logger.Info( + "Default playlist created", + "id", created.ID, + "name", created.Name, + ) + + s.emitEvent(events.PlaylistCreated, Summary{ + ID: created.ID, Name: created.Name, + }) +} + +// GetDefaultPlaylistTrackPaths returns the file paths of all +// tracks in the default playlist. +func (s *Service) GetDefaultPlaylistTrackPaths() ( + []string, + error, +) { + id := s.defaultPlaylistID() + if id == 0 { + return []string{}, nil + } + + paths, err := s.db.Queries.GetPlaylistTrackFilePaths( + s.db.Ctx, id, + ) + if err != nil { + s.logger.Error( + "Failed to get default playlist paths", + "playlistId", id, + "err", err, + ) + + return nil, fmt.Errorf( + "failed to get default playlist paths: %w", + err, + ) + } + + if paths == nil { + paths = []string{} + } + + return paths, nil +} + +// GetDefaultPlaylistInfo returns the ID and name of the default +// playlist for display in the frontend. +func (s *Service) GetDefaultPlaylistInfo() ( + Summary, + error, +) { + id := s.defaultPlaylistID() + if id == 0 { + return Summary{}, nil + } + + pl, err := s.db.Queries.GetPlaylist(s.db.Ctx, id) + if err != nil { + return Summary{}, fmt.Errorf( + "failed to get default playlist: %w", err, + ) + } + + return Summary{ID: pl.ID, Name: pl.Name}, nil +} + +// ToggleDefaultPlaylistTrack adds or removes a single track +// from the default playlist. Returns true if the track is now +// in the playlist (was added), false if it was removed. +func (s *Service) ToggleDefaultPlaylistTrack( + filePath string, +) (bool, error) { + id := s.defaultPlaylistID() + if id == 0 { + return false, errNoDefaultPlaylist + } + + inPlaylist, err := s.db.Queries.IsTrackInPlaylist( + s.db.Ctx, + sqlcgen.IsTrackInPlaylistParams{ + PlaylistID: id, + FilePath: filePath, + }, + ) + if err != nil { + return false, fmt.Errorf( + "failed to check playlist membership: %w", + err, + ) + } + + if inPlaylist != 0 { + // Remove. + if rmErr := s.db.Queries.RemovePlaylistTrackByPath( + s.db.Ctx, + sqlcgen.RemovePlaylistTrackByPathParams{ + PlaylistID: id, + FilePath: filePath, + }, + ); rmErr != nil { + return false, fmt.Errorf( + "failed to remove track: %w", rmErr, + ) + } + + s.savePlaylistFileByID(id) + s.emitEvent( + events.DefaultPlaylistChanged, + map[string]any{ + "filePath": filePath, + "added": false, + }, + ) + s.emitEvent(events.PlaylistTracksChanged, id) + + return false, nil + } + + // Add. + nextPos, posErr := s.db.Queries.GetNextPlaylistTrackPosition( + s.db.Ctx, id, + ) + if posErr != nil { + return false, fmt.Errorf( + "failed to get next position: %w", posErr, + ) + } + + if addErr := s.addSingleTrack( + id, filePath, nextPos, + ); addErr != nil { + return false, fmt.Errorf( + "failed to add track: %w", addErr, + ) + } + + s.savePlaylistFileByID(id) + s.emitEvent( + events.DefaultPlaylistChanged, + map[string]any{ + "filePath": filePath, + "added": true, + }, + ) + s.emitEvent(events.PlaylistTracksChanged, id) + + return true, nil +} + +// AddToDefaultPlaylist adds multiple tracks to the default +// playlist, skipping any that are already present. +func (s *Service) AddToDefaultPlaylist( + filePaths []string, +) error { + id := s.defaultPlaylistID() + if id == 0 { + return errNoDefaultPlaylist + } + + nextPos, err := s.db.Queries.GetNextPlaylistTrackPosition( + s.db.Ctx, id, + ) + if err != nil { + return fmt.Errorf( + "failed to get next position: %w", err, + ) + } + + var added int + + for _, fp := range filePaths { + inPlaylist, chkErr := s.db.Queries.IsTrackInPlaylist( + s.db.Ctx, + sqlcgen.IsTrackInPlaylistParams{ + PlaylistID: id, + FilePath: fp, + }, + ) + if chkErr != nil { + s.logger.Warn( + "Could not check playlist membership", + "filePath", fp, + "err", chkErr, + ) + + continue + } + + if inPlaylist != 0 { + continue + } + + if addErr := s.addSingleTrack( + id, fp, nextPos+int64(added), + ); addErr != nil { + s.logger.Warn( + "Could not add track to default playlist", + "filePath", fp, + "err", addErr, + ) + + continue + } + + added++ + } + + if added > 0 { + s.savePlaylistFileByID(id) + s.emitEvent( + events.DefaultPlaylistChanged, nil, + ) + s.emitEvent(events.PlaylistTracksChanged, id) + } + + return nil +} + +// RemoveFromDefaultPlaylist removes multiple tracks from the +// default playlist. +func (s *Service) RemoveFromDefaultPlaylist( + filePaths []string, +) error { + id := s.defaultPlaylistID() + if id == 0 { + return errNoDefaultPlaylist + } + + var removed int + + for _, fp := range filePaths { + rmErr := s.db.Queries.RemovePlaylistTrackByPath( + s.db.Ctx, + sqlcgen.RemovePlaylistTrackByPathParams{ + PlaylistID: id, + FilePath: fp, + }, + ) + if rmErr != nil { + s.logger.Warn( + "Could not remove track from default playlist", + "filePath", fp, + "err", rmErr, + ) + + continue + } + + removed++ + } + + if removed > 0 { + s.savePlaylistFileByID(id) + s.emitEvent( + events.DefaultPlaylistChanged, nil, + ) + s.emitEvent(events.PlaylistTracksChanged, id) + } + + return nil +} + +// defaultPlaylistID returns the configured default playlist ID, +// or 0 if not configured. +func (s *Service) defaultPlaylistID() int64 { + if s.favoritesConf == nil { + return 0 + } + + return s.favoritesConf.GetFavoritesPlaylistID() +} diff --git a/backend/playlist/playlist.go b/backend/playlist/playlist.go index 35a4811..e175438 100644 --- a/backend/playlist/playlist.go +++ b/backend/playlist/playlist.go @@ -96,10 +96,11 @@ type PhantomSearchResult struct { // Service manages playlist operations. type Service struct { - ctx context.Context - logger *slog.Logger - db *database.DB - libraryDir LibraryDirProvider + ctx context.Context + logger *slog.Logger + db *database.DB + libraryDir LibraryDirProvider + favoritesConf FavoritesConfigProvider } // NewService creates a new playlist service. @@ -115,6 +116,14 @@ func NewService( } } +// SetFavoritesConfig sets the provider used to read and write +// the default-playlist configuration. +func (s *Service) SetFavoritesConfig( + provider FavoritesConfigProvider, +) { + s.favoritesConf = provider +} + // SetContext sets the Wails runtime context and runs the // one-time startup migration to bootstrap M3U8 files for // existing playlists. @@ -583,6 +592,8 @@ func (s *Service) RemoveTracksFromPlaylist( } // DeletePlaylist deletes a playlist and its M3U8 file. +// If the deleted playlist was the default, a new default +// playlist is automatically created. func (s *Service) DeletePlaylist(playlistID int64) error { if err := s.db.Queries.DeletePlaylist( s.db.Ctx, playlistID, @@ -606,6 +617,11 @@ func (s *Service) DeletePlaylist(playlistID int64) error { s.emitEvent(events.PlaylistDeleted, playlistID) + // Recreate the default playlist if we just deleted it. + if s.defaultPlaylistID() == playlistID { + s.EnsureDefaultPlaylist() + } + return nil } diff --git a/frontend/src/components/artists-view/artists-view.ts b/frontend/src/components/artists-view/artists-view.ts index cbfb776..89c1b53 100644 --- a/frontend/src/components/artists-view/artists-view.ts +++ b/frontend/src/components/artists-view/artists-view.ts @@ -23,6 +23,7 @@ import { contextMenuStyles, } from '@utils/context-menu-controller.js'; import type { ContextMenuHost } from '@utils/context-menu-controller.js'; +import { FavoritesController } from '@store/controllers/favorites-controller'; import '@awesome.me/webawesome/dist/components/icon/icon.js'; import '@awesome.me/webawesome/dist/components/popup/popup.js'; @@ -60,6 +61,7 @@ export class ArtistsView private libraryCtrl = new LibraryController(this); private searchCtrl = new SearchController(this); private ctxMenu = new ContextMenuController(this); + private favCtrl = new FavoritesController(this); private wheelListenerAttached = false; private lastSearchTerm = ''; @@ -889,6 +891,25 @@ export class ArtistsView void this.ctxMenu.showPlaylistSubmenu(paths); } + private async onContextMenuFavoriteToggle() { + const filePaths = + await this.getContextMenuArtistFilePaths(); + + if (filePaths.length === 0) return; + + if (this.favCtrl.allFavorited(filePaths)) { + void this.favCtrl.removeFromFavorites( + filePaths, + ); + } else { + void this.favCtrl.addToFavorites( + filePaths, + ); + } + + this.ctxMenu.close(); + } + /* ================================================================ * File path resolution * ================================================================ */ @@ -1102,6 +1123,18 @@ export class ArtistsView >▶ + + this.onContextMenuFavoriteToggle()} + @mouseenter=${() => + this.ctxMenu.closePlaylistSubmenu()} + > + + ${this.favCtrl.allFavorited(this.ctxMenu.playlistFilePaths) ? `Remove from ${this.favCtrl.playlistName}` : `Add to ${this.favCtrl.playlistName}`} + ` : nothing} diff --git a/frontend/src/components/config-page/config-page.ts b/frontend/src/components/config-page/config-page.ts index ddb5d71..69683fb 100644 --- a/frontend/src/components/config-page/config-page.ts +++ b/frontend/src/components/config-page/config-page.ts @@ -12,9 +12,13 @@ import { import { DirectoryPicker } from '@go/frontendutil/FrontendUtil'; import { ThemeController } from '@store/controllers/theme-controller'; import { TrackListController } from '@store/controllers/tracklist-controller'; +import { FavoritesController } from '@store/controllers/favorites-controller'; +import { GetAllPlaylists } from '@go/playlist/Service'; +import type { playlist } from '@go/models'; import { Events } from '../../events'; import type { ConfigFieldChangeEvent } from './config-field'; import type { BackgroundShade } from '@store/theme-store'; +import type { IconStyle } from '@store/favorites-store'; import { COLUMN_DEFS, ALL_COLUMN_IDS, @@ -185,6 +189,12 @@ export class ConfigPage extends LitElement { // --- Track-list column config controller --- private trackListCtrl = new TrackListController(this); + // --- Favorites controller --- + private favCtrl = new FavoritesController(this); + + // --- Favorites state --- + @state() private playlists: playlist.Summary[] = []; + // --- Library state --- @state() private libraryDirectory = ''; @state() private selectedDirectory = ''; @@ -552,6 +562,7 @@ export class ConfigPage extends LitElement { override connectedCallback(): void { super.connectedCallback(); this.loadLibraryConfig(); + void this.loadPlaylists(); this.cancelScanStarted = EventsOn( Events.LibraryScanStarted, @@ -758,6 +769,56 @@ export class ConfigPage extends LitElement { }); }; + // =================================================================== + // FAVORITES HANDLERS + // =================================================================== + + private async loadPlaylists(): Promise { + try { + this.playlists = + await GetAllPlaylists(); + } catch (err) { + console.error( + 'Failed to load playlists:', + err, + ); + } + } + + private handleFavIconStyleChange = ( + e: CustomEvent, + ): void => { + const style = String( + e.detail.value, + ) as IconStyle; + + this.favCtrl + .setIconStyle(style) + .catch((err: unknown) => { + console.error( + 'Failed to set icon style:', + err, + ); + }); + }; + + private handleFavPlaylistChange = ( + e: CustomEvent, + ): void => { + const id = Number(e.detail.value); + + if (Number.isNaN(id)) return; + + this.favCtrl + .setDefaultPlaylist(id) + .catch((err: unknown) => { + console.error( + 'Failed to set default playlist:', + err, + ); + }); + }; + // =================================================================== // TRACK LIST COLUMN HANDLERS // =================================================================== @@ -877,6 +938,7 @@ export class ConfigPage extends LitElement {

Settings

${this.renderThemeSection()} + ${this.renderFavoritesSection()} ${this.renderTrackListSection()} ${this.renderLibrarySection()} `; @@ -969,6 +1031,61 @@ export class ConfigPage extends LitElement { ); } + // --- Favorites section --- + + private renderFavoritesSection() { + const playlistOptions = + this.playlists.map((p) => ({ + value: String(p.ID), + label: p.Name, + })); + + return html` + + + + + + `; + } + // --- Track list section --- private renderTrackListSection() { diff --git a/frontend/src/components/cover-grid/cover-grid.ts b/frontend/src/components/cover-grid/cover-grid.ts index 7751f35..43fa794 100644 --- a/frontend/src/components/cover-grid/cover-grid.ts +++ b/frontend/src/components/cover-grid/cover-grid.ts @@ -41,6 +41,7 @@ import { import type { DragPayload } from '@utils/drag-controller'; import { ContextMenuController } from '@utils/context-menu-controller.js'; import type { ContextMenuHost } from '@utils/context-menu-controller.js'; +import { FavoritesController } from '@store/controllers/favorites-controller'; import { createAlbumArtDragImage, createDragImage, @@ -88,6 +89,7 @@ export class CoverGrid private static readonly CARD_PADDING = 5; private ctxMenu = new ContextMenuController(this); + private favCtrl = new FavoritesController(this); private selMgr = new AlbumSelectionManager(); private scrollMgr = new ScrollManager(this, { GRID_GAP: CoverGrid.GRID_GAP, @@ -1508,6 +1510,26 @@ export class CoverGrid this.ctxMenu.close(); } + private async onContextMenuFavoriteToggle() { + const filePaths = + await this.getPlaylistSubmenuFilePaths(); + + if (filePaths.length === 0) return; + + if (this.favCtrl.allFavorited(filePaths)) { + void this.favCtrl.removeFromFavorites( + filePaths, + ); + } else { + void this.favCtrl.addToFavorites( + filePaths, + ); + } + + this.clearContextMenuSelection(); + this.ctxMenu.close(); + } + /** Clear the selection that was active for the context menu. */ private clearContextMenuSelection() { if (this.contextMenuTarget.kind === 'track') { @@ -1990,6 +2012,18 @@ export class CoverGrid >▶ + + this.onContextMenuFavoriteToggle()} + @mouseenter=${() => + ctxMenu.closePlaylistSubmenu()} + > + + ${this.favCtrl.allFavorited(this.ctxMenu.playlistFilePaths) ? `Remove from ${this.favCtrl.playlistName}` : `Add to ${this.favCtrl.playlistName}`} + ${this.contextMenuTarget .kind === 'track' && diff --git a/frontend/src/components/genres-view/genres-view.ts b/frontend/src/components/genres-view/genres-view.ts index 5467e52..7248385 100644 --- a/frontend/src/components/genres-view/genres-view.ts +++ b/frontend/src/components/genres-view/genres-view.ts @@ -20,6 +20,7 @@ import { contextMenuStyles, } from '@utils/context-menu-controller.js'; import type { ContextMenuHost } from '@utils/context-menu-controller.js'; +import { FavoritesController } from '@store/controllers/favorites-controller'; import '@awesome.me/webawesome/dist/components/icon/icon.js'; import '@awesome.me/webawesome/dist/components/popup/popup.js'; @@ -61,6 +62,7 @@ export class GenresView private libraryCtrl = new LibraryController(this); private searchCtrl = new SearchController(this); private ctxMenu = new ContextMenuController(this); + private favCtrl = new FavoritesController(this); private wheelListenerAttached = false; private lastSearchTerm = ''; @@ -873,6 +875,25 @@ export class GenresView this.ctxMenu.close(); } + private async onContextMenuFavoriteToggle() { + const filePaths = + await this.getContextMenuGenreFilePaths(); + + if (filePaths.length === 0) return; + + if (this.favCtrl.allFavorited(filePaths)) { + void this.favCtrl.removeFromFavorites( + filePaths, + ); + } else { + void this.favCtrl.addToFavorites( + filePaths, + ); + } + + this.ctxMenu.close(); + } + /* ================================================================ * Helpers * ================================================================ */ @@ -1054,6 +1075,18 @@ export class GenresView >▶ + + this.onContextMenuFavoriteToggle()} + @mouseenter=${() => + this.ctxMenu.closePlaylistSubmenu()} + > + + ${this.favCtrl.allFavorited(this.ctxMenu.playlistFilePaths) ? `Remove from ${this.favCtrl.playlistName}` : `Add to ${this.favCtrl.playlistName}`} + ` : nothing} diff --git a/frontend/src/components/now-playing/now-playing.ts b/frontend/src/components/now-playing/now-playing.ts index 8731403..f247ad4 100644 --- a/frontend/src/components/now-playing/now-playing.ts +++ b/frontend/src/components/now-playing/now-playing.ts @@ -4,6 +4,7 @@ import '@awesome.me/webawesome/dist/components/icon/icon.js'; import '@awesome.me/webawesome/dist/components/popup/popup.js'; import type WaPopup from '@awesome.me/webawesome/dist/components/popup/popup.js'; import { PlayerController } from '@store/controllers/player-controller'; +import { FavoritesController } from '@store/controllers/favorites-controller'; const MIN_WIDTH = 120; const MAX_WIDTH = 350; @@ -12,6 +13,7 @@ const DEFAULT_WIDTH = 200; @customElement('now-playing') export class NowPlaying extends LitElement { private player = new PlayerController(this); + private favCtrl = new FavoritesController(this); @state() private isDragging = false; @@ -83,6 +85,14 @@ export class NowPlaying extends LitElement { object-fit: cover; } + .track-info-wrapper { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; + flex: 1; + } + .track-info { display: flex; flex-direction: column; @@ -90,6 +100,33 @@ export class NowPlaying extends LitElement { min-width: 0; } + .fav-btn { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + cursor: pointer; + color: var(--yj-text-tertiary, #666); + font-size: 14px; + transition: color 0.1s ease; + background: none; + border: none; + padding: 0; + } + + .fav-btn:hover { + color: var(--yj-text-primary, #fff); + } + + .fav-btn.favorited { + color: var(--yj-accent, #ffd43b); + } + + .fav-btn.favorited:hover { + color: var(--yj-accent, #ffd43b); + opacity: 0.8; + } + .track-title { font-size: 14px; font-weight: 500; @@ -154,6 +191,11 @@ export class NowPlaying extends LitElement { `; } + const isFav = track.filePath + ? this.favCtrl.isFavorited(track.filePath) + : false; + const favVariant = isFav ? 'solid' : 'regular'; + return html`
@@ -199,11 +241,32 @@ export class NowPlaying extends LitElement { : nothing}
-
- ${track.title} - - ${track.artist || 'Unknown Artist'} - +
+
+ + ${track.title} + + + ${track.artist || 'Unknown Artist'} + +
+ ${track.filePath + ? html` + + ` + : nothing}
{ if (this.activePlaylistIndex < 0) return; @@ -2134,6 +2156,18 @@ export class PlaylistView ▶ + + this.onContextMenuFavoriteToggle()} + @mouseenter=${() => + this.ctxMenu.closePlaylistSubmenu()} + > + + ${this.favCtrl.allFavorited(this.getSelectedFilePaths()) ? `Remove from ${this.favCtrl.playlistName}` : `Add to ${this.favCtrl.playlistName}`} + ${this.selection .selectionCount === 1 diff --git a/frontend/src/components/queue-panel/queue-panel.ts b/frontend/src/components/queue-panel/queue-panel.ts index 0dfa09f..c079ad9 100644 --- a/frontend/src/components/queue-panel/queue-panel.ts +++ b/frontend/src/components/queue-panel/queue-panel.ts @@ -23,6 +23,7 @@ import { contextMenuStyles, } from '@utils/context-menu-controller.js'; import type { ContextMenuHost } from '@utils/context-menu-controller.js'; +import { FavoritesController } from '@store/controllers/favorites-controller'; import { hasTrackPayload, getDragPayload, @@ -52,6 +53,7 @@ export class QueuePanel private queue = new QueueController(this); private selection = new SelectionController(this); private ctxMenu = new ContextMenuController(this); + private favCtrl = new FavoritesController(this); @property({ type: Boolean, reflect: true }) open = false; @@ -622,6 +624,26 @@ export class QueuePanel this.ctxMenu.close(); } + private onContextMenuFavoriteToggle() { + const filePaths = + this.getSelectedFilePaths(); + + if (filePaths.length === 0) return; + + if (this.favCtrl.allFavorited(filePaths)) { + void this.favCtrl.removeFromFavorites( + filePaths, + ); + } else { + void this.favCtrl.addToFavorites( + filePaths, + ); + } + + this.selection.clear(); + this.ctxMenu.close(); + } + private openTrackDetails(index: number) { const queueTrack = this.queue.tracks[index]; @@ -1330,6 +1352,18 @@ export class QueuePanel ▶ + + this.onContextMenuFavoriteToggle()} + @mouseenter=${() => + this.ctxMenu.closePlaylistSubmenu()} + > + + ${this.favCtrl.allFavorited(this.getSelectedFilePaths()) ? `Remove from ${this.favCtrl.playlistName}` : `Add to ${this.favCtrl.playlistName}`} + ${this.selection .selectionCount === 1 ? html` diff --git a/frontend/src/components/track-list/track-list.ts b/frontend/src/components/track-list/track-list.ts index c498644..60c343e 100644 --- a/frontend/src/components/track-list/track-list.ts +++ b/frontend/src/components/track-list/track-list.ts @@ -17,6 +17,7 @@ import type { ContextMenuHost } from '@utils/context-menu-controller.js'; import { PlayerController } from '@store/controllers/player-controller'; import { SearchController } from '@store/controllers/search-controller'; import { TrackListController } from '@store/controllers/tracklist-controller'; +import { FavoritesController } from '@store/controllers/favorites-controller'; import { queueStore } from '@store/queue-store'; import { LibraryController } from '@store/controllers/library-controller'; import { @@ -74,6 +75,7 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH private libraryCtrl = new LibraryController(this); private searchCtrl = new SearchController(this); private trackListCtrl = new TrackListController(this); + private favCtrl = new FavoritesController(this); private selection = new SelectionController(this); private ctxMenu = new ContextMenuController(this); private lastSearchTerm = ''; @@ -312,24 +314,34 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH private get gridTemplateColumns(): string { const cols = this.activeColumns; + const favCol = '24px'; if (this.columnWidths.length === 0) { - return cols - .map((c) => c.defaultWidth) - .join(' '); + return ( + favCol + + ' ' + + cols + .map((c) => c.defaultWidth) + .join(' ') + ); } - return this.columnWidths - .map((w) => `${w}px`) - .join(' '); + return ( + favCol + + ' ' + + this.columnWidths + .map((w) => `${w}px`) + .join(' ') + ); } private get colBoundaryPositions(): number[] { if (this.columnWidths.length === 0) return []; const padding = 8; + const favColWidth = 24; const positions: number[] = []; - let cumulative = padding; + let cumulative = padding + favColWidth; for ( let i = 0; @@ -930,7 +942,7 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH min-width: 0; } - .header-cell + .header-cell, + .header-row > :not(:first-child), .track-row > :not(:first-child) { padding-left: 6px; } @@ -971,6 +983,31 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH text-align: center; } + .fav-icon { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + flex-shrink: 0; + cursor: pointer; + color: var(--yj-text-tertiary, #666); + font-size: 12px; + transition: color 0.1s ease; + } + + .fav-icon:hover { + color: var(--yj-text-primary, #fff); + } + + .fav-icon.favorited { + color: var(--yj-accent, #ffd43b); + } + + .fav-icon.favorited:hover { + color: var(--yj-accent, #ffd43b); + opacity: 0.8; + } + .search-match { background-color: rgba(255, 212, 59, 0.15); border-radius: 2px; @@ -1258,6 +1295,26 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH this.ctxMenu.close(); } + private onContextMenuFavoriteToggle() { + const filePaths = + this.selection.getSelectedKeysOrdered(); + + if (filePaths.length === 0) return; + + if (this.favCtrl.allFavorited(filePaths)) { + void this.favCtrl.removeFromFavorites( + filePaths, + ); + } else { + void this.favCtrl.addToFavorites( + filePaths, + ); + } + + this.selection.clear(); + this.ctxMenu.close(); + } + private openTrackDetails(filePath: string) { const track = this.tracks.find( (t) => t.FilePath === filePath, @@ -1479,6 +1536,13 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH const cols = this.activeColumns; + const isFav = this.favCtrl.isFavorited( + track.FilePath, + ); + const favVariant = isFav + ? 'solid' + : 'regular'; + return html`
+
{ + e.stopPropagation(); + void this.favCtrl.toggleFavorite( + track.FilePath, + ); + }} + > + +
${cols.map((col) => { const val = col.accessor(track); const centered = val === '\u2014'; @@ -1628,6 +1706,7 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH ${this.renderSortToolbar()}
+
${cols.map( (col) => html`
▶ + + this.onContextMenuFavoriteToggle()} + @mouseenter=${() => + this.ctxMenu.closePlaylistSubmenu()} + > + + ${this.favCtrl.allFavorited(this.selection.getSelectedKeysOrdered()) ? `Remove from ${this.favCtrl.playlistName}` : `Add to ${this.favCtrl.playlistName}`} + ${this.selection.selectionCount === 1 ? html` void; + + constructor(host: ReactiveControllerHost) { + this.host = host; + host.addController(this); + } + + // =============================================================== + // LIFECYCLE HOOKS + // =============================================================== + + hostConnected(): void { + this.unsubscribe = + favoritesStore.subscribe(() => { + this.host.requestUpdate(); + }); + } + + hostDisconnected(): void { + this.unsubscribe?.(); + } + + // =============================================================== + // DATA ACCESS + // =============================================================== + + isFavorited(filePath: string): boolean { + return favoritesStore.isFavorited(filePath); + } + + allFavorited(filePaths: string[]): boolean { + return favoritesStore.allFavorited(filePaths); + } + + get iconStyle(): IconStyle { + return favoritesStore.getIconStyle(); + } + + get playlistName(): string { + return favoritesStore.getPlaylistName(); + } + + get playlistId(): number { + return favoritesStore.getPlaylistId(); + } + + /** + * Returns the icon name for the current icon style. + */ + get iconName(): string { + return this.iconStyle === 'star' + ? 'star' + : 'heart'; + } + + // =============================================================== + // ACTIONS + // =============================================================== + + async toggleFavorite( + filePath: string, + ): Promise { + await favoritesStore.toggleFavorite(filePath); + } + + async addToFavorites( + filePaths: string[], + ): Promise { + await favoritesStore.addToFavorites(filePaths); + } + + async removeFromFavorites( + filePaths: string[], + ): Promise { + await favoritesStore.removeFromFavorites( + filePaths, + ); + } + + async setIconStyle( + style: IconStyle, + ): Promise { + await favoritesStore.setIconStyle(style); + } + + async setDefaultPlaylist( + id: number, + ): Promise { + await favoritesStore.setDefaultPlaylist(id); + } +} diff --git a/frontend/src/store/favorites-store.ts b/frontend/src/store/favorites-store.ts new file mode 100644 index 0000000..db9b507 --- /dev/null +++ b/frontend/src/store/favorites-store.ts @@ -0,0 +1,278 @@ +import { EventsOn } from '@runtime/runtime'; +import { + GetDefaultPlaylistTrackPaths, + GetDefaultPlaylistInfo, + ToggleDefaultPlaylistTrack, + AddToDefaultPlaylist, + RemoveFromDefaultPlaylist, +} from '@go/playlist/Service'; +import { + GetFavoritesIconStyle, + GetFavoritesPlaylistID, + SetFavoritesIconStyle, + SetFavoritesPlaylistID, +} from '@go/config/Config'; +import { Events } from '../events'; + +export type IconStyle = 'heart' | 'star'; + +export interface FavoritesState { + playlistId: number; + playlistName: string; + iconStyle: IconStyle; + favoritedPaths: Set; +} + +type Subscriber = () => void; + +class FavoritesStore { + private playlistId = 0; + private playlistName = 'Favorites'; + private iconStyle: IconStyle = 'heart'; + private favoritedPaths = new Set(); + private subscribers = new Set(); + private loading = false; + + constructor() { + // Load initial state. + void this.loadConfig(); + void this.loadPaths(); + + // React to changes from the backend. + EventsOn( + Events.FavoritesConfigChanged, + (data: { + PlaylistID: number; + IconStyle: string; + }) => { + this.playlistId = data.PlaylistID; + this.iconStyle = + data.IconStyle as IconStyle; + void this.loadPlaylistName(); + void this.loadPaths(); + }, + ); + + EventsOn( + Events.DefaultPlaylistChanged, + () => { + void this.loadPaths(); + }, + ); + + // When a playlist's tracks change, check if it's + // our default playlist and reload if so. + EventsOn( + Events.PlaylistTracksChanged, + (playlistId: number) => { + if (playlistId === this.playlistId) { + void this.loadPaths(); + } + }, + ); + + // When a playlist is deleted and recreated, + // reload everything. + EventsOn(Events.PlaylistDeleted, () => { + void this.loadConfig(); + void this.loadPaths(); + }); + + EventsOn(Events.PlaylistRenamed, () => { + void this.loadPlaylistName(); + }); + + EventsOn(Events.PlaylistsRestored, () => { + void this.loadPaths(); + }); + } + + // =============================================================== + // DATA ACCESS + // =============================================================== + + isFavorited(filePath: string): boolean { + return this.favoritedPaths.has(filePath); + } + + /** + * Check if all given file paths are in the default + * playlist. + */ + allFavorited(filePaths: string[]): boolean { + if (filePaths.length === 0) return false; + + return filePaths.every((fp) => + this.favoritedPaths.has(fp), + ); + } + + getIconStyle(): IconStyle { + return this.iconStyle; + } + + getPlaylistName(): string { + return this.playlistName; + } + + getPlaylistId(): number { + return this.playlistId; + } + + isLoading(): boolean { + return this.loading; + } + + // =============================================================== + // ACTIONS + // =============================================================== + + async toggleFavorite(filePath: string): Promise { + // Optimistic update. + const wasIn = this.favoritedPaths.has(filePath); + + if (wasIn) { + this.favoritedPaths.delete(filePath); + } else { + this.favoritedPaths.add(filePath); + } + + this.notify(); + + try { + await ToggleDefaultPlaylistTrack(filePath); + } catch { + // Revert optimistic update. + if (wasIn) { + this.favoritedPaths.add(filePath); + } else { + this.favoritedPaths.delete(filePath); + } + + this.notify(); + } + } + + async addToFavorites( + filePaths: string[], + ): Promise { + for (const fp of filePaths) { + this.favoritedPaths.add(fp); + } + + this.notify(); + + try { + await AddToDefaultPlaylist(filePaths); + } catch { + void this.loadPaths(); + } + } + + async removeFromFavorites( + filePaths: string[], + ): Promise { + for (const fp of filePaths) { + this.favoritedPaths.delete(fp); + } + + this.notify(); + + try { + await RemoveFromDefaultPlaylist(filePaths); + } catch { + void this.loadPaths(); + } + } + + async setIconStyle( + style: IconStyle, + ): Promise { + this.iconStyle = style; + this.notify(); + await SetFavoritesIconStyle(style); + } + + async setDefaultPlaylist( + id: number, + ): Promise { + this.playlistId = id; + this.notify(); + await SetFavoritesPlaylistID(id); + await this.loadPlaylistName(); + await this.loadPaths(); + } + + // =============================================================== + // SUBSCRIPTION SYSTEM + // =============================================================== + + subscribe(callback: Subscriber): () => void { + this.subscribers.add(callback); + + return () => this.subscribers.delete(callback); + } + + private notify(): void { + this.subscribers.forEach((cb) => cb()); + } + + // =============================================================== + // LOADING HELPERS + // =============================================================== + + private async loadConfig(): Promise { + try { + const [id, style] = await Promise.all([ + GetFavoritesPlaylistID(), + GetFavoritesIconStyle(), + ]); + + this.playlistId = id; + this.iconStyle = style as IconStyle; + await this.loadPlaylistName(); + this.notify(); + } catch { + // Defaults are already set. + } + } + + private async loadPlaylistName(): Promise { + if (this.playlistId === 0) { + this.playlistName = 'Favorites'; + this.notify(); + + return; + } + + try { + const info = + await GetDefaultPlaylistInfo(); + + if (info?.Name) { + this.playlistName = info.Name; + this.notify(); + } + } catch { + // Keep current name. + } + } + + private async loadPaths(): Promise { + this.loading = true; + + try { + const paths = + await GetDefaultPlaylistTrackPaths(); + this.favoritedPaths = new Set(paths ?? []); + } catch { + // Keep current set. + } finally { + this.loading = false; + this.notify(); + } + } +} + +// Singleton instance. +export const favoritesStore = new FavoritesStore(); diff --git a/frontend/wailsjs/go/config/Config.d.ts b/frontend/wailsjs/go/config/Config.d.ts index 7e8ce6b..a6cfdcc 100755 --- a/frontend/wailsjs/go/config/Config.d.ts +++ b/frontend/wailsjs/go/config/Config.d.ts @@ -3,6 +3,10 @@ import {tracklist} from '../models'; import {context} from '../models'; +export function GetFavoritesIconStyle():Promise; + +export function GetFavoritesPlaylistID():Promise; + export function GetLibraryDirectory():Promise; export function GetScanConcurrency():Promise; @@ -19,6 +23,10 @@ export function Save():Promise; export function SetContext(arg1:context.Context):Promise; +export function SetFavoritesIconStyle(arg1:string):Promise; + +export function SetFavoritesPlaylistID(arg1:number):Promise; + export function SetLibraryDirectory(arg1:string):Promise; export function SetScanConcurrency(arg1:string):Promise; diff --git a/frontend/wailsjs/go/config/Config.js b/frontend/wailsjs/go/config/Config.js index dfd2283..1b206ec 100755 --- a/frontend/wailsjs/go/config/Config.js +++ b/frontend/wailsjs/go/config/Config.js @@ -2,6 +2,14 @@ // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT +export function GetFavoritesIconStyle() { + return window['go']['config']['Config']['GetFavoritesIconStyle'](); +} + +export function GetFavoritesPlaylistID() { + return window['go']['config']['Config']['GetFavoritesPlaylistID'](); +} + export function GetLibraryDirectory() { return window['go']['config']['Config']['GetLibraryDirectory'](); } @@ -34,6 +42,14 @@ export function SetContext(arg1) { return window['go']['config']['Config']['SetContext'](arg1); } +export function SetFavoritesIconStyle(arg1) { + return window['go']['config']['Config']['SetFavoritesIconStyle'](arg1); +} + +export function SetFavoritesPlaylistID(arg1) { + return window['go']['config']['Config']['SetFavoritesPlaylistID'](arg1); +} + export function SetLibraryDirectory(arg1) { return window['go']['config']['Config']['SetLibraryDirectory'](arg1); } diff --git a/frontend/wailsjs/go/playlist/Service.d.ts b/frontend/wailsjs/go/playlist/Service.d.ts index 3ce94c9..277dc4e 100755 --- a/frontend/wailsjs/go/playlist/Service.d.ts +++ b/frontend/wailsjs/go/playlist/Service.d.ts @@ -3,6 +3,8 @@ import {playlist} from '../models'; import {context} from '../models'; +export function AddToDefaultPlaylist(arg1:Array):Promise; + export function AddTracksToPlaylist(arg1:number,arg2:Array):Promise; export function CreatePlaylist(arg1:string):Promise; @@ -11,18 +13,26 @@ export function CreatePlaylistWithTracks(arg1:string,arg2:Array):Promise export function DeletePlaylist(arg1:number):Promise; +export function EnsureDefaultPlaylist():Promise; + export function FindPhantomMatches(arg1:number,arg2:Array):Promise; export function GetAllPlaylists():Promise>; export function GetAllPlaylistsWithTracks():Promise>; +export function GetDefaultPlaylistInfo():Promise; + +export function GetDefaultPlaylistTrackPaths():Promise>; + export function GetPhantomCandidates(arg1:number,arg2:string):Promise>; export function GetPlaylistTracks(arg1:number):Promise>; export function ImportPlaylist(arg1:string):Promise; +export function RemoveFromDefaultPlaylist(arg1:Array):Promise; + export function RemovePhantomTracks(arg1:number,arg2:Array):Promise; export function RemoveTracksFromPlaylist(arg1:number,arg2:Array):Promise; @@ -36,3 +46,7 @@ export function RestoreAllPlaylists():Promise; export function SearchLibrary(arg1:string):Promise>; export function SetContext(arg1:context.Context):Promise; + +export function SetFavoritesConfig(arg1:playlist.FavoritesConfigProvider):Promise; + +export function ToggleDefaultPlaylistTrack(arg1:string):Promise; diff --git a/frontend/wailsjs/go/playlist/Service.js b/frontend/wailsjs/go/playlist/Service.js index 7d0c805..5c5d2d0 100755 --- a/frontend/wailsjs/go/playlist/Service.js +++ b/frontend/wailsjs/go/playlist/Service.js @@ -2,6 +2,10 @@ // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT +export function AddToDefaultPlaylist(arg1) { + return window['go']['playlist']['Service']['AddToDefaultPlaylist'](arg1); +} + export function AddTracksToPlaylist(arg1, arg2) { return window['go']['playlist']['Service']['AddTracksToPlaylist'](arg1, arg2); } @@ -18,6 +22,10 @@ export function DeletePlaylist(arg1) { return window['go']['playlist']['Service']['DeletePlaylist'](arg1); } +export function EnsureDefaultPlaylist() { + return window['go']['playlist']['Service']['EnsureDefaultPlaylist'](); +} + export function FindPhantomMatches(arg1, arg2) { return window['go']['playlist']['Service']['FindPhantomMatches'](arg1, arg2); } @@ -30,6 +38,14 @@ export function GetAllPlaylistsWithTracks() { return window['go']['playlist']['Service']['GetAllPlaylistsWithTracks'](); } +export function GetDefaultPlaylistInfo() { + return window['go']['playlist']['Service']['GetDefaultPlaylistInfo'](); +} + +export function GetDefaultPlaylistTrackPaths() { + return window['go']['playlist']['Service']['GetDefaultPlaylistTrackPaths'](); +} + export function GetPhantomCandidates(arg1, arg2) { return window['go']['playlist']['Service']['GetPhantomCandidates'](arg1, arg2); } @@ -42,6 +58,10 @@ export function ImportPlaylist(arg1) { return window['go']['playlist']['Service']['ImportPlaylist'](arg1); } +export function RemoveFromDefaultPlaylist(arg1) { + return window['go']['playlist']['Service']['RemoveFromDefaultPlaylist'](arg1); +} + export function RemovePhantomTracks(arg1, arg2) { return window['go']['playlist']['Service']['RemovePhantomTracks'](arg1, arg2); } @@ -69,3 +89,11 @@ export function SearchLibrary(arg1) { export function SetContext(arg1) { return window['go']['playlist']['Service']['SetContext'](arg1); } + +export function SetFavoritesConfig(arg1) { + return window['go']['playlist']['Service']['SetFavoritesConfig'](arg1); +} + +export function ToggleDefaultPlaylistTrack(arg1) { + return window['go']['playlist']['Service']['ToggleDefaultPlaylistTrack'](arg1); +}