added "default playlist"/ favorites system
This commit is contained in:
@@ -103,6 +103,7 @@ func NewYellowJacketApp(
|
|||||||
yjApp.playlist = playlist.NewService(
|
yjApp.playlist = playlist.NewService(
|
||||||
yjApp.logger, yjApp.database, yjApp.appConfig,
|
yjApp.logger, yjApp.database, yjApp.appConfig,
|
||||||
)
|
)
|
||||||
|
yjApp.playlist.SetFavoritesConfig(yjApp.appConfig)
|
||||||
|
|
||||||
// create queue (before wails.Run so it can be bound)
|
// create queue (before wails.Run so it can be bound)
|
||||||
yjApp.queue = queue.NewQueue(yjApp.logger, yjApp.database)
|
yjApp.queue = queue.NewQueue(yjApp.logger, yjApp.database)
|
||||||
@@ -145,6 +146,7 @@ func (yj *YellowJacketApp) OnStartup(ctx context.Context) {
|
|||||||
yj.FrontendUtil.SetContext(ctx)
|
yj.FrontendUtil.SetContext(ctx)
|
||||||
yj.library.SetContext(ctx)
|
yj.library.SetContext(ctx)
|
||||||
yj.playlist.SetContext(ctx)
|
yj.playlist.SetContext(ctx)
|
||||||
|
yj.playlist.EnsureDefaultPlaylist()
|
||||||
|
|
||||||
// Initialize speaker hardware (player struct created in
|
// Initialize speaker hardware (player struct created in
|
||||||
// NewYellowJacketApp for Wails binding registration).
|
// NewYellowJacketApp for Wails binding registration).
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
|
|
||||||
"yellowjacket/backend/events"
|
"yellowjacket/backend/events"
|
||||||
|
"yellowjacket/backend/favorites"
|
||||||
"yellowjacket/backend/library"
|
"yellowjacket/backend/library"
|
||||||
"yellowjacket/backend/system"
|
"yellowjacket/backend/system"
|
||||||
"yellowjacket/backend/theme"
|
"yellowjacket/backend/theme"
|
||||||
@@ -28,6 +29,7 @@ type Config struct {
|
|||||||
Theme *theme.Config `toml:"Theme"`
|
Theme *theme.Config `toml:"Theme"`
|
||||||
Window *WindowConfig `toml:"Window"`
|
Window *WindowConfig `toml:"Window"`
|
||||||
TrackList *tracklist.Config `toml:"TrackList"`
|
TrackList *tracklist.Config `toml:"TrackList"`
|
||||||
|
Favorites *favorites.Config `toml:"Favorites"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewConfig creates a new config by loading it from disk.
|
// 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 {
|
if configErrs != nil {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"one or more config parts are invalid: %w",
|
"one or more config parts are invalid: %w",
|
||||||
@@ -174,6 +182,12 @@ func (c *Config) applyDefaults() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.TrackList.ApplyDefaults()
|
c.TrackList.ApplyDefaults()
|
||||||
|
|
||||||
|
if c.Favorites == nil {
|
||||||
|
c.Favorites = &favorites.Config{}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Favorites.ApplyDefaults()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetContext sets the Wails runtime context for event emission.
|
// 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),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -88,3 +88,23 @@ DELETE FROM playlist_tracks;
|
|||||||
-- name: GetNextPlaylistTrackPosition :one
|
-- name: GetNextPlaylistTrackPosition :one
|
||||||
SELECT COALESCE(MAX(position), -1) + 1 AS next_position
|
SELECT COALESCE(MAX(position), -1) + 1 AS next_position
|
||||||
FROM playlist_tracks WHERE playlist_id = ?;
|
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 = ?
|
||||||
|
);
|
||||||
|
|||||||
@@ -209,6 +209,37 @@ func (q *Queries) GetPlaylist(ctx context.Context, id int64) (Playlist, error) {
|
|||||||
return i, err
|
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
|
const getPlaylistTracks = `-- name: GetPlaylistTracks :many
|
||||||
SELECT pt.id, pt.playlist_id, pt.audio_file_id, pt.position, af.file_path
|
SELECT pt.id, pt.playlist_id, pt.audio_file_id, pt.position, af.file_path
|
||||||
FROM playlist_tracks pt
|
FROM playlist_tracks pt
|
||||||
@@ -328,6 +359,26 @@ func (q *Queries) GetPlaylistTracksWithMetadata(ctx context.Context, playlistID
|
|||||||
return items, nil
|
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
|
const removePlaylistTrack = `-- name: RemovePlaylistTrack :exec
|
||||||
DELETE FROM playlist_tracks WHERE id = ?
|
DELETE FROM playlist_tracks WHERE id = ?
|
||||||
`
|
`
|
||||||
@@ -337,6 +388,23 @@ func (q *Queries) RemovePlaylistTrack(ctx context.Context, id int64) error {
|
|||||||
return err
|
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
|
const updatePlaylistName = `-- name: UpdatePlaylistName :exec
|
||||||
UPDATE playlists SET name = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?
|
UPDATE playlists SET name = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -25,15 +25,17 @@ const (
|
|||||||
LibraryConfigChanged = "LibraryConfigChanged"
|
LibraryConfigChanged = "LibraryConfigChanged"
|
||||||
ThemeConfigChanged = "ThemeConfigChanged"
|
ThemeConfigChanged = "ThemeConfigChanged"
|
||||||
TrackListConfigChanged = "TrackListConfigChanged"
|
TrackListConfigChanged = "TrackListConfigChanged"
|
||||||
|
FavoritesConfigChanged = "FavoritesConfigChanged"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Playlist events.
|
// Playlist events.
|
||||||
const (
|
const (
|
||||||
PlaylistCreated = "PlaylistCreated"
|
PlaylistCreated = "PlaylistCreated"
|
||||||
PlaylistDeleted = "PlaylistDeleted"
|
PlaylistDeleted = "PlaylistDeleted"
|
||||||
PlaylistRenamed = "PlaylistRenamed"
|
PlaylistRenamed = "PlaylistRenamed"
|
||||||
PlaylistTracksChanged = "PlaylistTracksChanged"
|
PlaylistTracksChanged = "PlaylistTracksChanged"
|
||||||
PlaylistsRestored = "PlaylistsRestored"
|
PlaylistsRestored = "PlaylistsRestored"
|
||||||
|
DefaultPlaylistChanged = "DefaultPlaylistChanged"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Library events.
|
// Library events.
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -96,10 +96,11 @@ type PhantomSearchResult struct {
|
|||||||
|
|
||||||
// Service manages playlist operations.
|
// Service manages playlist operations.
|
||||||
type Service struct {
|
type Service struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
db *database.DB
|
db *database.DB
|
||||||
libraryDir LibraryDirProvider
|
libraryDir LibraryDirProvider
|
||||||
|
favoritesConf FavoritesConfigProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewService creates a new playlist service.
|
// 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
|
// SetContext sets the Wails runtime context and runs the
|
||||||
// one-time startup migration to bootstrap M3U8 files for
|
// one-time startup migration to bootstrap M3U8 files for
|
||||||
// existing playlists.
|
// existing playlists.
|
||||||
@@ -583,6 +592,8 @@ func (s *Service) RemoveTracksFromPlaylist(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DeletePlaylist deletes a playlist and its M3U8 file.
|
// 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 {
|
func (s *Service) DeletePlaylist(playlistID int64) error {
|
||||||
if err := s.db.Queries.DeletePlaylist(
|
if err := s.db.Queries.DeletePlaylist(
|
||||||
s.db.Ctx, playlistID,
|
s.db.Ctx, playlistID,
|
||||||
@@ -606,6 +617,11 @@ func (s *Service) DeletePlaylist(playlistID int64) error {
|
|||||||
|
|
||||||
s.emitEvent(events.PlaylistDeleted, playlistID)
|
s.emitEvent(events.PlaylistDeleted, playlistID)
|
||||||
|
|
||||||
|
// Recreate the default playlist if we just deleted it.
|
||||||
|
if s.defaultPlaylistID() == playlistID {
|
||||||
|
s.EnsureDefaultPlaylist()
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
contextMenuStyles,
|
contextMenuStyles,
|
||||||
} from '@utils/context-menu-controller.js';
|
} from '@utils/context-menu-controller.js';
|
||||||
import type { ContextMenuHost } 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/icon/icon.js';
|
||||||
import '@awesome.me/webawesome/dist/components/popup/popup.js';
|
import '@awesome.me/webawesome/dist/components/popup/popup.js';
|
||||||
@@ -60,6 +61,7 @@ export class ArtistsView
|
|||||||
private libraryCtrl = new LibraryController(this);
|
private libraryCtrl = new LibraryController(this);
|
||||||
private searchCtrl = new SearchController(this);
|
private searchCtrl = new SearchController(this);
|
||||||
private ctxMenu = new ContextMenuController(this);
|
private ctxMenu = new ContextMenuController(this);
|
||||||
|
private favCtrl = new FavoritesController(this);
|
||||||
private wheelListenerAttached = false;
|
private wheelListenerAttached = false;
|
||||||
private lastSearchTerm = '';
|
private lastSearchTerm = '';
|
||||||
|
|
||||||
@@ -889,6 +891,25 @@ export class ArtistsView
|
|||||||
void this.ctxMenu.showPlaylistSubmenu(paths);
|
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
|
* File path resolution
|
||||||
* ================================================================ */
|
* ================================================================ */
|
||||||
@@ -1102,6 +1123,18 @@ export class ArtistsView
|
|||||||
>▶</span
|
>▶</span
|
||||||
>
|
>
|
||||||
</wa-dropdown-item>
|
</wa-dropdown-item>
|
||||||
|
<wa-dropdown-item
|
||||||
|
@click=${() =>
|
||||||
|
this.onContextMenuFavoriteToggle()}
|
||||||
|
@mouseenter=${() =>
|
||||||
|
this.ctxMenu.closePlaylistSubmenu()}
|
||||||
|
>
|
||||||
|
<wa-icon
|
||||||
|
slot="icon"
|
||||||
|
name=${this.favCtrl.iconName}
|
||||||
|
></wa-icon>
|
||||||
|
${this.favCtrl.allFavorited(this.ctxMenu.playlistFilePaths) ? `Remove from ${this.favCtrl.playlistName}` : `Add to ${this.favCtrl.playlistName}`}
|
||||||
|
</wa-dropdown-item>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
|
|||||||
@@ -12,9 +12,13 @@ import {
|
|||||||
import { DirectoryPicker } from '@go/frontendutil/FrontendUtil';
|
import { DirectoryPicker } from '@go/frontendutil/FrontendUtil';
|
||||||
import { ThemeController } from '@store/controllers/theme-controller';
|
import { ThemeController } from '@store/controllers/theme-controller';
|
||||||
import { TrackListController } from '@store/controllers/tracklist-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 { Events } from '../../events';
|
||||||
import type { ConfigFieldChangeEvent } from './config-field';
|
import type { ConfigFieldChangeEvent } from './config-field';
|
||||||
import type { BackgroundShade } from '@store/theme-store';
|
import type { BackgroundShade } from '@store/theme-store';
|
||||||
|
import type { IconStyle } from '@store/favorites-store';
|
||||||
import {
|
import {
|
||||||
COLUMN_DEFS,
|
COLUMN_DEFS,
|
||||||
ALL_COLUMN_IDS,
|
ALL_COLUMN_IDS,
|
||||||
@@ -185,6 +189,12 @@ export class ConfigPage extends LitElement {
|
|||||||
// --- Track-list column config controller ---
|
// --- Track-list column config controller ---
|
||||||
private trackListCtrl = new TrackListController(this);
|
private trackListCtrl = new TrackListController(this);
|
||||||
|
|
||||||
|
// --- Favorites controller ---
|
||||||
|
private favCtrl = new FavoritesController(this);
|
||||||
|
|
||||||
|
// --- Favorites state ---
|
||||||
|
@state() private playlists: playlist.Summary[] = [];
|
||||||
|
|
||||||
// --- Library state ---
|
// --- Library state ---
|
||||||
@state() private libraryDirectory = '';
|
@state() private libraryDirectory = '';
|
||||||
@state() private selectedDirectory = '';
|
@state() private selectedDirectory = '';
|
||||||
@@ -552,6 +562,7 @@ export class ConfigPage extends LitElement {
|
|||||||
override connectedCallback(): void {
|
override connectedCallback(): void {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this.loadLibraryConfig();
|
this.loadLibraryConfig();
|
||||||
|
void this.loadPlaylists();
|
||||||
|
|
||||||
this.cancelScanStarted = EventsOn(
|
this.cancelScanStarted = EventsOn(
|
||||||
Events.LibraryScanStarted,
|
Events.LibraryScanStarted,
|
||||||
@@ -758,6 +769,56 @@ export class ConfigPage extends LitElement {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
// FAVORITES HANDLERS
|
||||||
|
// ===================================================================
|
||||||
|
|
||||||
|
private async loadPlaylists(): Promise<void> {
|
||||||
|
try {
|
||||||
|
this.playlists =
|
||||||
|
await GetAllPlaylists();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(
|
||||||
|
'Failed to load playlists:',
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleFavIconStyleChange = (
|
||||||
|
e: CustomEvent<ConfigFieldChangeEvent>,
|
||||||
|
): 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<ConfigFieldChangeEvent>,
|
||||||
|
): 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
|
// TRACK LIST COLUMN HANDLERS
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
@@ -877,6 +938,7 @@ export class ConfigPage extends LitElement {
|
|||||||
<h2>Settings</h2>
|
<h2>Settings</h2>
|
||||||
|
|
||||||
${this.renderThemeSection()}
|
${this.renderThemeSection()}
|
||||||
|
${this.renderFavoritesSection()}
|
||||||
${this.renderTrackListSection()}
|
${this.renderTrackListSection()}
|
||||||
${this.renderLibrarySection()}
|
${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`
|
||||||
|
<config-section
|
||||||
|
heading="Favorites"
|
||||||
|
description="Configure the default playlist used for quick-favouriting tracks."
|
||||||
|
>
|
||||||
|
<config-field
|
||||||
|
.schema=${{
|
||||||
|
key: 'favoritesPlaylist',
|
||||||
|
label: 'Default Playlist',
|
||||||
|
description:
|
||||||
|
'The playlist used when toggling the favourite icon on tracks.',
|
||||||
|
type: 'select' as const,
|
||||||
|
options: playlistOptions,
|
||||||
|
}}
|
||||||
|
.value=${String(
|
||||||
|
this.favCtrl.playlistId,
|
||||||
|
)}
|
||||||
|
@config-change=${this.handleFavPlaylistChange}
|
||||||
|
></config-field>
|
||||||
|
|
||||||
|
<config-field
|
||||||
|
.schema=${{
|
||||||
|
key: 'favoritesIcon',
|
||||||
|
label: 'Icon Style',
|
||||||
|
description:
|
||||||
|
'Choose heart or star for the favourite indicator.',
|
||||||
|
type: 'select' as const,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: 'heart',
|
||||||
|
label: '\u2665 Heart',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'star',
|
||||||
|
label: '\u2605 Star',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
.value=${this.favCtrl
|
||||||
|
.iconStyle}
|
||||||
|
@config-change=${this.handleFavIconStyleChange}
|
||||||
|
></config-field>
|
||||||
|
</config-section>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Track list section ---
|
// --- Track list section ---
|
||||||
|
|
||||||
private renderTrackListSection() {
|
private renderTrackListSection() {
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import {
|
|||||||
import type { DragPayload } from '@utils/drag-controller';
|
import type { DragPayload } from '@utils/drag-controller';
|
||||||
import { ContextMenuController } from '@utils/context-menu-controller.js';
|
import { ContextMenuController } from '@utils/context-menu-controller.js';
|
||||||
import type { ContextMenuHost } from '@utils/context-menu-controller.js';
|
import type { ContextMenuHost } from '@utils/context-menu-controller.js';
|
||||||
|
import { FavoritesController } from '@store/controllers/favorites-controller';
|
||||||
import {
|
import {
|
||||||
createAlbumArtDragImage,
|
createAlbumArtDragImage,
|
||||||
createDragImage,
|
createDragImage,
|
||||||
@@ -88,6 +89,7 @@ export class CoverGrid
|
|||||||
private static readonly CARD_PADDING = 5;
|
private static readonly CARD_PADDING = 5;
|
||||||
|
|
||||||
private ctxMenu = new ContextMenuController(this);
|
private ctxMenu = new ContextMenuController(this);
|
||||||
|
private favCtrl = new FavoritesController(this);
|
||||||
private selMgr = new AlbumSelectionManager();
|
private selMgr = new AlbumSelectionManager();
|
||||||
private scrollMgr = new ScrollManager(this, {
|
private scrollMgr = new ScrollManager(this, {
|
||||||
GRID_GAP: CoverGrid.GRID_GAP,
|
GRID_GAP: CoverGrid.GRID_GAP,
|
||||||
@@ -1508,6 +1510,26 @@ export class CoverGrid
|
|||||||
this.ctxMenu.close();
|
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. */
|
/** Clear the selection that was active for the context menu. */
|
||||||
private clearContextMenuSelection() {
|
private clearContextMenuSelection() {
|
||||||
if (this.contextMenuTarget.kind === 'track') {
|
if (this.contextMenuTarget.kind === 'track') {
|
||||||
@@ -1990,6 +2012,18 @@ export class CoverGrid
|
|||||||
>▶</span
|
>▶</span
|
||||||
>
|
>
|
||||||
</wa-dropdown-item>
|
</wa-dropdown-item>
|
||||||
|
<wa-dropdown-item
|
||||||
|
@click=${() =>
|
||||||
|
this.onContextMenuFavoriteToggle()}
|
||||||
|
@mouseenter=${() =>
|
||||||
|
ctxMenu.closePlaylistSubmenu()}
|
||||||
|
>
|
||||||
|
<wa-icon
|
||||||
|
slot="icon"
|
||||||
|
name=${this.favCtrl.iconName}
|
||||||
|
></wa-icon>
|
||||||
|
${this.favCtrl.allFavorited(this.ctxMenu.playlistFilePaths) ? `Remove from ${this.favCtrl.playlistName}` : `Add to ${this.favCtrl.playlistName}`}
|
||||||
|
</wa-dropdown-item>
|
||||||
${this.contextMenuTarget
|
${this.contextMenuTarget
|
||||||
.kind ===
|
.kind ===
|
||||||
'track' &&
|
'track' &&
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
contextMenuStyles,
|
contextMenuStyles,
|
||||||
} from '@utils/context-menu-controller.js';
|
} from '@utils/context-menu-controller.js';
|
||||||
import type { ContextMenuHost } 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/icon/icon.js';
|
||||||
import '@awesome.me/webawesome/dist/components/popup/popup.js';
|
import '@awesome.me/webawesome/dist/components/popup/popup.js';
|
||||||
@@ -61,6 +62,7 @@ export class GenresView
|
|||||||
private libraryCtrl = new LibraryController(this);
|
private libraryCtrl = new LibraryController(this);
|
||||||
private searchCtrl = new SearchController(this);
|
private searchCtrl = new SearchController(this);
|
||||||
private ctxMenu = new ContextMenuController(this);
|
private ctxMenu = new ContextMenuController(this);
|
||||||
|
private favCtrl = new FavoritesController(this);
|
||||||
private wheelListenerAttached = false;
|
private wheelListenerAttached = false;
|
||||||
private lastSearchTerm = '';
|
private lastSearchTerm = '';
|
||||||
|
|
||||||
@@ -873,6 +875,25 @@ export class GenresView
|
|||||||
this.ctxMenu.close();
|
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
|
* Helpers
|
||||||
* ================================================================ */
|
* ================================================================ */
|
||||||
@@ -1054,6 +1075,18 @@ export class GenresView
|
|||||||
>▶</span
|
>▶</span
|
||||||
>
|
>
|
||||||
</wa-dropdown-item>
|
</wa-dropdown-item>
|
||||||
|
<wa-dropdown-item
|
||||||
|
@click=${() =>
|
||||||
|
this.onContextMenuFavoriteToggle()}
|
||||||
|
@mouseenter=${() =>
|
||||||
|
this.ctxMenu.closePlaylistSubmenu()}
|
||||||
|
>
|
||||||
|
<wa-icon
|
||||||
|
slot="icon"
|
||||||
|
name=${this.favCtrl.iconName}
|
||||||
|
></wa-icon>
|
||||||
|
${this.favCtrl.allFavorited(this.ctxMenu.playlistFilePaths) ? `Remove from ${this.favCtrl.playlistName}` : `Add to ${this.favCtrl.playlistName}`}
|
||||||
|
</wa-dropdown-item>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import '@awesome.me/webawesome/dist/components/icon/icon.js';
|
|||||||
import '@awesome.me/webawesome/dist/components/popup/popup.js';
|
import '@awesome.me/webawesome/dist/components/popup/popup.js';
|
||||||
import type WaPopup from '@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 { PlayerController } from '@store/controllers/player-controller';
|
||||||
|
import { FavoritesController } from '@store/controllers/favorites-controller';
|
||||||
|
|
||||||
const MIN_WIDTH = 120;
|
const MIN_WIDTH = 120;
|
||||||
const MAX_WIDTH = 350;
|
const MAX_WIDTH = 350;
|
||||||
@@ -12,6 +13,7 @@ const DEFAULT_WIDTH = 200;
|
|||||||
@customElement('now-playing')
|
@customElement('now-playing')
|
||||||
export class NowPlaying extends LitElement {
|
export class NowPlaying extends LitElement {
|
||||||
private player = new PlayerController(this);
|
private player = new PlayerController(this);
|
||||||
|
private favCtrl = new FavoritesController(this);
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private isDragging = false;
|
private isDragging = false;
|
||||||
@@ -83,6 +85,14 @@ export class NowPlaying extends LitElement {
|
|||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.track-info-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.track-info {
|
.track-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -90,6 +100,33 @@ export class NowPlaying extends LitElement {
|
|||||||
min-width: 0;
|
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 {
|
.track-title {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
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`
|
return html`
|
||||||
<div class="now-playing">
|
<div class="now-playing">
|
||||||
<div class="cover-art-wrapper">
|
<div class="cover-art-wrapper">
|
||||||
@@ -199,11 +241,32 @@ export class NowPlaying extends LitElement {
|
|||||||
: nothing}
|
: nothing}
|
||||||
</wa-popup>
|
</wa-popup>
|
||||||
</div>
|
</div>
|
||||||
<div class="track-info">
|
<div class="track-info-wrapper">
|
||||||
<span class="track-title">${track.title}</span>
|
<div class="track-info">
|
||||||
<span class="track-artist">
|
<span class="track-title">
|
||||||
${track.artist || 'Unknown Artist'}
|
${track.title}
|
||||||
</span>
|
</span>
|
||||||
|
<span class="track-artist">
|
||||||
|
${track.artist || 'Unknown Artist'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
${track.filePath
|
||||||
|
? html`
|
||||||
|
<button
|
||||||
|
class="fav-btn ${isFav ? 'favorited' : ''}"
|
||||||
|
title="${isFav ? `Remove from ${this.favCtrl.playlistName}` : `Add to ${this.favCtrl.playlistName}`}"
|
||||||
|
@click=${() =>
|
||||||
|
void this.favCtrl.toggleFavorite(
|
||||||
|
track.filePath,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<wa-icon
|
||||||
|
name=${this.favCtrl.iconName}
|
||||||
|
variant=${favVariant}
|
||||||
|
></wa-icon>
|
||||||
|
</button>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import { libraryStore } from '@store/library-store';
|
|||||||
import { ContextMenuController } from '@utils/context-menu-controller.js';
|
import { ContextMenuController } from '@utils/context-menu-controller.js';
|
||||||
import type { ContextMenuHost } from '@utils/context-menu-controller.js';
|
import type { ContextMenuHost } from '@utils/context-menu-controller.js';
|
||||||
import { contextMenuStyles } from '@utils/context-menu-controller.js';
|
import { contextMenuStyles } from '@utils/context-menu-controller.js';
|
||||||
|
import { FavoritesController } from '@store/controllers/favorites-controller';
|
||||||
import '@components/track-details/track-details.js';
|
import '@components/track-details/track-details.js';
|
||||||
import type { TrackDetails } from '@components/track-details/track-details.js';
|
import type { TrackDetails } from '@components/track-details/track-details.js';
|
||||||
import type { CoverArtUrls } from '@components/track-details/track-details.js';
|
import type { CoverArtUrls } from '@components/track-details/track-details.js';
|
||||||
@@ -66,6 +67,7 @@ export class PlaylistView
|
|||||||
private searchCtrl = new SearchController(this);
|
private searchCtrl = new SearchController(this);
|
||||||
private selection = new SelectionController(this);
|
private selection = new SelectionController(this);
|
||||||
private ctxMenu = new ContextMenuController(this);
|
private ctxMenu = new ContextMenuController(this);
|
||||||
|
private favCtrl = new FavoritesController(this);
|
||||||
|
|
||||||
getContextMenuPopup(): WaPopup | undefined {
|
getContextMenuPopup(): WaPopup | undefined {
|
||||||
return this.contextMenuPopup;
|
return this.contextMenuPopup;
|
||||||
@@ -1148,6 +1150,26 @@ export class PlaylistView
|
|||||||
this.ctxMenu.close();
|
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 async removeSelectedPhantoms(): Promise<void> {
|
private async removeSelectedPhantoms(): Promise<void> {
|
||||||
if (this.activePlaylistIndex < 0) return;
|
if (this.activePlaylistIndex < 0) return;
|
||||||
|
|
||||||
@@ -2134,6 +2156,18 @@ export class PlaylistView
|
|||||||
▶
|
▶
|
||||||
</span>
|
</span>
|
||||||
</wa-dropdown-item>
|
</wa-dropdown-item>
|
||||||
|
<wa-dropdown-item
|
||||||
|
@click=${() =>
|
||||||
|
this.onContextMenuFavoriteToggle()}
|
||||||
|
@mouseenter=${() =>
|
||||||
|
this.ctxMenu.closePlaylistSubmenu()}
|
||||||
|
>
|
||||||
|
<wa-icon
|
||||||
|
slot="icon"
|
||||||
|
name=${this.favCtrl.iconName}
|
||||||
|
></wa-icon>
|
||||||
|
${this.favCtrl.allFavorited(this.getSelectedFilePaths()) ? `Remove from ${this.favCtrl.playlistName}` : `Add to ${this.favCtrl.playlistName}`}
|
||||||
|
</wa-dropdown-item>
|
||||||
${this.selection
|
${this.selection
|
||||||
.selectionCount ===
|
.selectionCount ===
|
||||||
1
|
1
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
contextMenuStyles,
|
contextMenuStyles,
|
||||||
} from '@utils/context-menu-controller.js';
|
} from '@utils/context-menu-controller.js';
|
||||||
import type { ContextMenuHost } from '@utils/context-menu-controller.js';
|
import type { ContextMenuHost } from '@utils/context-menu-controller.js';
|
||||||
|
import { FavoritesController } from '@store/controllers/favorites-controller';
|
||||||
import {
|
import {
|
||||||
hasTrackPayload,
|
hasTrackPayload,
|
||||||
getDragPayload,
|
getDragPayload,
|
||||||
@@ -52,6 +53,7 @@ export class QueuePanel
|
|||||||
private queue = new QueueController(this);
|
private queue = new QueueController(this);
|
||||||
private selection = new SelectionController(this);
|
private selection = new SelectionController(this);
|
||||||
private ctxMenu = new ContextMenuController(this);
|
private ctxMenu = new ContextMenuController(this);
|
||||||
|
private favCtrl = new FavoritesController(this);
|
||||||
|
|
||||||
@property({ type: Boolean, reflect: true })
|
@property({ type: Boolean, reflect: true })
|
||||||
open = false;
|
open = false;
|
||||||
@@ -622,6 +624,26 @@ export class QueuePanel
|
|||||||
this.ctxMenu.close();
|
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) {
|
private openTrackDetails(index: number) {
|
||||||
const queueTrack =
|
const queueTrack =
|
||||||
this.queue.tracks[index];
|
this.queue.tracks[index];
|
||||||
@@ -1330,6 +1352,18 @@ export class QueuePanel
|
|||||||
▶
|
▶
|
||||||
</span>
|
</span>
|
||||||
</wa-dropdown-item>
|
</wa-dropdown-item>
|
||||||
|
<wa-dropdown-item
|
||||||
|
@click=${() =>
|
||||||
|
this.onContextMenuFavoriteToggle()}
|
||||||
|
@mouseenter=${() =>
|
||||||
|
this.ctxMenu.closePlaylistSubmenu()}
|
||||||
|
>
|
||||||
|
<wa-icon
|
||||||
|
slot="icon"
|
||||||
|
name=${this.favCtrl.iconName}
|
||||||
|
></wa-icon>
|
||||||
|
${this.favCtrl.allFavorited(this.getSelectedFilePaths()) ? `Remove from ${this.favCtrl.playlistName}` : `Add to ${this.favCtrl.playlistName}`}
|
||||||
|
</wa-dropdown-item>
|
||||||
${this.selection
|
${this.selection
|
||||||
.selectionCount === 1
|
.selectionCount === 1
|
||||||
? html`
|
? html`
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import type { ContextMenuHost } from '@utils/context-menu-controller.js';
|
|||||||
import { PlayerController } from '@store/controllers/player-controller';
|
import { PlayerController } from '@store/controllers/player-controller';
|
||||||
import { SearchController } from '@store/controllers/search-controller';
|
import { SearchController } from '@store/controllers/search-controller';
|
||||||
import { TrackListController } from '@store/controllers/tracklist-controller';
|
import { TrackListController } from '@store/controllers/tracklist-controller';
|
||||||
|
import { FavoritesController } from '@store/controllers/favorites-controller';
|
||||||
import { queueStore } from '@store/queue-store';
|
import { queueStore } from '@store/queue-store';
|
||||||
import { LibraryController } from '@store/controllers/library-controller';
|
import { LibraryController } from '@store/controllers/library-controller';
|
||||||
import {
|
import {
|
||||||
@@ -74,6 +75,7 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
|
|||||||
private libraryCtrl = new LibraryController(this);
|
private libraryCtrl = new LibraryController(this);
|
||||||
private searchCtrl = new SearchController(this);
|
private searchCtrl = new SearchController(this);
|
||||||
private trackListCtrl = new TrackListController(this);
|
private trackListCtrl = new TrackListController(this);
|
||||||
|
private favCtrl = new FavoritesController(this);
|
||||||
private selection = new SelectionController(this);
|
private selection = new SelectionController(this);
|
||||||
private ctxMenu = new ContextMenuController(this);
|
private ctxMenu = new ContextMenuController(this);
|
||||||
private lastSearchTerm = '';
|
private lastSearchTerm = '';
|
||||||
@@ -312,24 +314,34 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
|
|||||||
|
|
||||||
private get gridTemplateColumns(): string {
|
private get gridTemplateColumns(): string {
|
||||||
const cols = this.activeColumns;
|
const cols = this.activeColumns;
|
||||||
|
const favCol = '24px';
|
||||||
|
|
||||||
if (this.columnWidths.length === 0) {
|
if (this.columnWidths.length === 0) {
|
||||||
return cols
|
return (
|
||||||
.map((c) => c.defaultWidth)
|
favCol +
|
||||||
.join(' ');
|
' ' +
|
||||||
|
cols
|
||||||
|
.map((c) => c.defaultWidth)
|
||||||
|
.join(' ')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.columnWidths
|
return (
|
||||||
.map((w) => `${w}px`)
|
favCol +
|
||||||
.join(' ');
|
' ' +
|
||||||
|
this.columnWidths
|
||||||
|
.map((w) => `${w}px`)
|
||||||
|
.join(' ')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get colBoundaryPositions(): number[] {
|
private get colBoundaryPositions(): number[] {
|
||||||
if (this.columnWidths.length === 0) return [];
|
if (this.columnWidths.length === 0) return [];
|
||||||
|
|
||||||
const padding = 8;
|
const padding = 8;
|
||||||
|
const favColWidth = 24;
|
||||||
const positions: number[] = [];
|
const positions: number[] = [];
|
||||||
let cumulative = padding;
|
let cumulative = padding + favColWidth;
|
||||||
|
|
||||||
for (
|
for (
|
||||||
let i = 0;
|
let i = 0;
|
||||||
@@ -930,7 +942,7 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-cell + .header-cell,
|
.header-row > :not(:first-child),
|
||||||
.track-row > :not(:first-child) {
|
.track-row > :not(:first-child) {
|
||||||
padding-left: 6px;
|
padding-left: 6px;
|
||||||
}
|
}
|
||||||
@@ -971,6 +983,31 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
|
|||||||
text-align: center;
|
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 {
|
.search-match {
|
||||||
background-color: rgba(255, 212, 59, 0.15);
|
background-color: rgba(255, 212, 59, 0.15);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
@@ -1258,6 +1295,26 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
|
|||||||
this.ctxMenu.close();
|
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) {
|
private openTrackDetails(filePath: string) {
|
||||||
const track = this.tracks.find(
|
const track = this.tracks.find(
|
||||||
(t) => t.FilePath === filePath,
|
(t) => t.FilePath === filePath,
|
||||||
@@ -1479,6 +1536,13 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
|
|||||||
|
|
||||||
const cols = this.activeColumns;
|
const cols = this.activeColumns;
|
||||||
|
|
||||||
|
const isFav = this.favCtrl.isFavorited(
|
||||||
|
track.FilePath,
|
||||||
|
);
|
||||||
|
const favVariant = isFav
|
||||||
|
? 'solid'
|
||||||
|
: 'regular';
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
class=${classes}
|
class=${classes}
|
||||||
@@ -1493,6 +1557,20 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
|
|||||||
this.onTrackDragStart(e, track)}
|
this.onTrackDragStart(e, track)}
|
||||||
@dragend=${this.onTrackDragEnd}
|
@dragend=${this.onTrackDragEnd}
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
class="fav-icon ${isFav ? 'favorited' : ''}"
|
||||||
|
@click=${(e: MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
void this.favCtrl.toggleFavorite(
|
||||||
|
track.FilePath,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<wa-icon
|
||||||
|
name=${this.favCtrl.iconName}
|
||||||
|
variant=${favVariant}
|
||||||
|
></wa-icon>
|
||||||
|
</div>
|
||||||
${cols.map((col) => {
|
${cols.map((col) => {
|
||||||
const val = col.accessor(track);
|
const val = col.accessor(track);
|
||||||
const centered = val === '\u2014';
|
const centered = val === '\u2014';
|
||||||
@@ -1628,6 +1706,7 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
|
|||||||
${this.renderSortToolbar()}
|
${this.renderSortToolbar()}
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<div class="header-row">
|
<div class="header-row">
|
||||||
|
<div></div>
|
||||||
${cols.map(
|
${cols.map(
|
||||||
(col) => html`
|
(col) => html`
|
||||||
<div
|
<div
|
||||||
@@ -1731,6 +1810,18 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
|
|||||||
Add to Playlist
|
Add to Playlist
|
||||||
<span class="submenu-arrow">▶</span>
|
<span class="submenu-arrow">▶</span>
|
||||||
</wa-dropdown-item>
|
</wa-dropdown-item>
|
||||||
|
<wa-dropdown-item
|
||||||
|
@click=${() =>
|
||||||
|
this.onContextMenuFavoriteToggle()}
|
||||||
|
@mouseenter=${() =>
|
||||||
|
this.ctxMenu.closePlaylistSubmenu()}
|
||||||
|
>
|
||||||
|
<wa-icon
|
||||||
|
slot="icon"
|
||||||
|
name=${this.favCtrl.iconName}
|
||||||
|
></wa-icon>
|
||||||
|
${this.favCtrl.allFavorited(this.selection.getSelectedKeysOrdered()) ? `Remove from ${this.favCtrl.playlistName}` : `Add to ${this.favCtrl.playlistName}`}
|
||||||
|
</wa-dropdown-item>
|
||||||
${this.selection.selectionCount === 1
|
${this.selection.selectionCount === 1
|
||||||
? html`
|
? html`
|
||||||
<wa-dropdown-item
|
<wa-dropdown-item
|
||||||
|
|||||||
@@ -21,10 +21,12 @@ export const Events = {
|
|||||||
PlaylistRenamed: "PlaylistRenamed",
|
PlaylistRenamed: "PlaylistRenamed",
|
||||||
PlaylistTracksChanged: "PlaylistTracksChanged",
|
PlaylistTracksChanged: "PlaylistTracksChanged",
|
||||||
PlaylistsRestored: "PlaylistsRestored",
|
PlaylistsRestored: "PlaylistsRestored",
|
||||||
|
DefaultPlaylistChanged: "DefaultPlaylistChanged",
|
||||||
|
|
||||||
// Config events
|
// Config events
|
||||||
ThemeConfigChanged: "ThemeConfigChanged",
|
ThemeConfigChanged: "ThemeConfigChanged",
|
||||||
TrackListConfigChanged: "TrackListConfigChanged",
|
TrackListConfigChanged: "TrackListConfigChanged",
|
||||||
|
FavoritesConfigChanged: "FavoritesConfigChanged",
|
||||||
|
|
||||||
// Library events
|
// Library events
|
||||||
LibraryScanStarted: "LibraryScanStarted",
|
LibraryScanStarted: "LibraryScanStarted",
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import type {
|
||||||
|
ReactiveController,
|
||||||
|
ReactiveControllerHost,
|
||||||
|
} from 'lit';
|
||||||
|
import {
|
||||||
|
favoritesStore,
|
||||||
|
} from '../favorites-store';
|
||||||
|
import type { IconStyle } from '../favorites-store';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FavoritesController connects a Lit component to the
|
||||||
|
* FavoritesStore.
|
||||||
|
*
|
||||||
|
* Usage in a component:
|
||||||
|
*
|
||||||
|
* private favCtrl = new FavoritesController(this);
|
||||||
|
*
|
||||||
|
* render() {
|
||||||
|
* const isFav = this.favCtrl.isFavorited(filePath);
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export class FavoritesController
|
||||||
|
implements ReactiveController
|
||||||
|
{
|
||||||
|
private host: ReactiveControllerHost;
|
||||||
|
private unsubscribe?: () => 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<void> {
|
||||||
|
await favoritesStore.toggleFavorite(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
async addToFavorites(
|
||||||
|
filePaths: string[],
|
||||||
|
): Promise<void> {
|
||||||
|
await favoritesStore.addToFavorites(filePaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeFromFavorites(
|
||||||
|
filePaths: string[],
|
||||||
|
): Promise<void> {
|
||||||
|
await favoritesStore.removeFromFavorites(
|
||||||
|
filePaths,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setIconStyle(
|
||||||
|
style: IconStyle,
|
||||||
|
): Promise<void> {
|
||||||
|
await favoritesStore.setIconStyle(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setDefaultPlaylist(
|
||||||
|
id: number,
|
||||||
|
): Promise<void> {
|
||||||
|
await favoritesStore.setDefaultPlaylist(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Subscriber = () => void;
|
||||||
|
|
||||||
|
class FavoritesStore {
|
||||||
|
private playlistId = 0;
|
||||||
|
private playlistName = 'Favorites';
|
||||||
|
private iconStyle: IconStyle = 'heart';
|
||||||
|
private favoritedPaths = new Set<string>();
|
||||||
|
private subscribers = new Set<Subscriber>();
|
||||||
|
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<void> {
|
||||||
|
// 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<void> {
|
||||||
|
for (const fp of filePaths) {
|
||||||
|
this.favoritedPaths.add(fp);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.notify();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await AddToDefaultPlaylist(filePaths);
|
||||||
|
} catch {
|
||||||
|
void this.loadPaths();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeFromFavorites(
|
||||||
|
filePaths: string[],
|
||||||
|
): Promise<void> {
|
||||||
|
for (const fp of filePaths) {
|
||||||
|
this.favoritedPaths.delete(fp);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.notify();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await RemoveFromDefaultPlaylist(filePaths);
|
||||||
|
} catch {
|
||||||
|
void this.loadPaths();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async setIconStyle(
|
||||||
|
style: IconStyle,
|
||||||
|
): Promise<void> {
|
||||||
|
this.iconStyle = style;
|
||||||
|
this.notify();
|
||||||
|
await SetFavoritesIconStyle(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setDefaultPlaylist(
|
||||||
|
id: number,
|
||||||
|
): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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();
|
||||||
+8
@@ -3,6 +3,10 @@
|
|||||||
import {tracklist} from '../models';
|
import {tracklist} from '../models';
|
||||||
import {context} from '../models';
|
import {context} from '../models';
|
||||||
|
|
||||||
|
export function GetFavoritesIconStyle():Promise<string>;
|
||||||
|
|
||||||
|
export function GetFavoritesPlaylistID():Promise<number>;
|
||||||
|
|
||||||
export function GetLibraryDirectory():Promise<string>;
|
export function GetLibraryDirectory():Promise<string>;
|
||||||
|
|
||||||
export function GetScanConcurrency():Promise<string>;
|
export function GetScanConcurrency():Promise<string>;
|
||||||
@@ -19,6 +23,10 @@ export function Save():Promise<void>;
|
|||||||
|
|
||||||
export function SetContext(arg1:context.Context):Promise<void>;
|
export function SetContext(arg1:context.Context):Promise<void>;
|
||||||
|
|
||||||
|
export function SetFavoritesIconStyle(arg1:string):Promise<void>;
|
||||||
|
|
||||||
|
export function SetFavoritesPlaylistID(arg1:number):Promise<void>;
|
||||||
|
|
||||||
export function SetLibraryDirectory(arg1:string):Promise<void>;
|
export function SetLibraryDirectory(arg1:string):Promise<void>;
|
||||||
|
|
||||||
export function SetScanConcurrency(arg1:string):Promise<void>;
|
export function SetScanConcurrency(arg1:string):Promise<void>;
|
||||||
|
|||||||
@@ -2,6 +2,14 @@
|
|||||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
// This file is automatically generated. DO NOT EDIT
|
// 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() {
|
export function GetLibraryDirectory() {
|
||||||
return window['go']['config']['Config']['GetLibraryDirectory']();
|
return window['go']['config']['Config']['GetLibraryDirectory']();
|
||||||
}
|
}
|
||||||
@@ -34,6 +42,14 @@ export function SetContext(arg1) {
|
|||||||
return window['go']['config']['Config']['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) {
|
export function SetLibraryDirectory(arg1) {
|
||||||
return window['go']['config']['Config']['SetLibraryDirectory'](arg1);
|
return window['go']['config']['Config']['SetLibraryDirectory'](arg1);
|
||||||
}
|
}
|
||||||
|
|||||||
+14
@@ -3,6 +3,8 @@
|
|||||||
import {playlist} from '../models';
|
import {playlist} from '../models';
|
||||||
import {context} from '../models';
|
import {context} from '../models';
|
||||||
|
|
||||||
|
export function AddToDefaultPlaylist(arg1:Array<string>):Promise<void>;
|
||||||
|
|
||||||
export function AddTracksToPlaylist(arg1:number,arg2:Array<string>):Promise<void>;
|
export function AddTracksToPlaylist(arg1:number,arg2:Array<string>):Promise<void>;
|
||||||
|
|
||||||
export function CreatePlaylist(arg1:string):Promise<playlist.Summary>;
|
export function CreatePlaylist(arg1:string):Promise<playlist.Summary>;
|
||||||
@@ -11,18 +13,26 @@ export function CreatePlaylistWithTracks(arg1:string,arg2:Array<string>):Promise
|
|||||||
|
|
||||||
export function DeletePlaylist(arg1:number):Promise<void>;
|
export function DeletePlaylist(arg1:number):Promise<void>;
|
||||||
|
|
||||||
|
export function EnsureDefaultPlaylist():Promise<void>;
|
||||||
|
|
||||||
export function FindPhantomMatches(arg1:number,arg2:Array<string>):Promise<playlist.PhantomSearchResult>;
|
export function FindPhantomMatches(arg1:number,arg2:Array<string>):Promise<playlist.PhantomSearchResult>;
|
||||||
|
|
||||||
export function GetAllPlaylists():Promise<Array<playlist.Summary>>;
|
export function GetAllPlaylists():Promise<Array<playlist.Summary>>;
|
||||||
|
|
||||||
export function GetAllPlaylistsWithTracks():Promise<Array<playlist.WithTracks>>;
|
export function GetAllPlaylistsWithTracks():Promise<Array<playlist.WithTracks>>;
|
||||||
|
|
||||||
|
export function GetDefaultPlaylistInfo():Promise<playlist.Summary>;
|
||||||
|
|
||||||
|
export function GetDefaultPlaylistTrackPaths():Promise<Array<string>>;
|
||||||
|
|
||||||
export function GetPhantomCandidates(arg1:number,arg2:string):Promise<Array<playlist.CandidateTrack>>;
|
export function GetPhantomCandidates(arg1:number,arg2:string):Promise<Array<playlist.CandidateTrack>>;
|
||||||
|
|
||||||
export function GetPlaylistTracks(arg1:number):Promise<Array<playlist.Track>>;
|
export function GetPlaylistTracks(arg1:number):Promise<Array<playlist.Track>>;
|
||||||
|
|
||||||
export function ImportPlaylist(arg1:string):Promise<playlist.Summary>;
|
export function ImportPlaylist(arg1:string):Promise<playlist.Summary>;
|
||||||
|
|
||||||
|
export function RemoveFromDefaultPlaylist(arg1:Array<string>):Promise<void>;
|
||||||
|
|
||||||
export function RemovePhantomTracks(arg1:number,arg2:Array<string>):Promise<void>;
|
export function RemovePhantomTracks(arg1:number,arg2:Array<string>):Promise<void>;
|
||||||
|
|
||||||
export function RemoveTracksFromPlaylist(arg1:number,arg2:Array<number>):Promise<void>;
|
export function RemoveTracksFromPlaylist(arg1:number,arg2:Array<number>):Promise<void>;
|
||||||
@@ -36,3 +46,7 @@ export function RestoreAllPlaylists():Promise<void>;
|
|||||||
export function SearchLibrary(arg1:string):Promise<Array<playlist.CandidateTrack>>;
|
export function SearchLibrary(arg1:string):Promise<Array<playlist.CandidateTrack>>;
|
||||||
|
|
||||||
export function SetContext(arg1:context.Context):Promise<void>;
|
export function SetContext(arg1:context.Context):Promise<void>;
|
||||||
|
|
||||||
|
export function SetFavoritesConfig(arg1:playlist.FavoritesConfigProvider):Promise<void>;
|
||||||
|
|
||||||
|
export function ToggleDefaultPlaylistTrack(arg1:string):Promise<boolean>;
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
// This file is automatically generated. DO NOT EDIT
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
export function AddToDefaultPlaylist(arg1) {
|
||||||
|
return window['go']['playlist']['Service']['AddToDefaultPlaylist'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
export function AddTracksToPlaylist(arg1, arg2) {
|
export function AddTracksToPlaylist(arg1, arg2) {
|
||||||
return window['go']['playlist']['Service']['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);
|
return window['go']['playlist']['Service']['DeletePlaylist'](arg1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function EnsureDefaultPlaylist() {
|
||||||
|
return window['go']['playlist']['Service']['EnsureDefaultPlaylist']();
|
||||||
|
}
|
||||||
|
|
||||||
export function FindPhantomMatches(arg1, arg2) {
|
export function FindPhantomMatches(arg1, arg2) {
|
||||||
return window['go']['playlist']['Service']['FindPhantomMatches'](arg1, arg2);
|
return window['go']['playlist']['Service']['FindPhantomMatches'](arg1, arg2);
|
||||||
}
|
}
|
||||||
@@ -30,6 +38,14 @@ export function GetAllPlaylistsWithTracks() {
|
|||||||
return window['go']['playlist']['Service']['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) {
|
export function GetPhantomCandidates(arg1, arg2) {
|
||||||
return window['go']['playlist']['Service']['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);
|
return window['go']['playlist']['Service']['ImportPlaylist'](arg1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function RemoveFromDefaultPlaylist(arg1) {
|
||||||
|
return window['go']['playlist']['Service']['RemoveFromDefaultPlaylist'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
export function RemovePhantomTracks(arg1, arg2) {
|
export function RemovePhantomTracks(arg1, arg2) {
|
||||||
return window['go']['playlist']['Service']['RemovePhantomTracks'](arg1, arg2);
|
return window['go']['playlist']['Service']['RemovePhantomTracks'](arg1, arg2);
|
||||||
}
|
}
|
||||||
@@ -69,3 +89,11 @@ export function SearchLibrary(arg1) {
|
|||||||
export function SetContext(arg1) {
|
export function SetContext(arg1) {
|
||||||
return window['go']['playlist']['Service']['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);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user