From 4c09e7abee4e2f889d6f9d9d09187dee1b573b99 Mon Sep 17 00:00:00 2001 From: Caleb Allen Date: Mon, 16 Feb 2026 00:27:36 -0500 Subject: [PATCH] playlist creation, viewing, integration with other views. still wip for full playlist functionality --- backend/app.go | 7 + backend/database/sql/queries/playlists.sql | 4 + backend/database/sql/sqlcgen/playlists.sql.go | 12 + backend/playlist/playlist.go | 190 ++++++++++ frontend/index.ts | 8 +- frontend/package.json.md5 | 2 +- .../src/components/cover-grid/cover-grid.ts | 101 ++++++ .../playlist-picker/playlist-picker.ts | 299 ++++++++++++++++ .../components/playlist-view/playlist-view.ts | 325 ++++++++++++++++++ .../src/components/queue-panel/queue-panel.ts | 109 +++++- .../src/components/track-list/track-list.ts | 89 +++++ frontend/wailsjs/go/models.ts | 19 + frontend/wailsjs/go/playlist/Service.d.ts | 14 + frontend/wailsjs/go/playlist/Service.js | 23 ++ frontend/wailsjs/runtime/package.json | 0 frontend/wailsjs/runtime/runtime.d.ts | 0 frontend/wailsjs/runtime/runtime.js | 0 go.mod | 8 +- go.sum | 129 ++++++- 19 files changed, 1304 insertions(+), 35 deletions(-) create mode 100644 backend/playlist/playlist.go create mode 100644 frontend/src/components/playlist-picker/playlist-picker.ts create mode 100644 frontend/src/components/playlist-view/playlist-view.ts create mode 100755 frontend/wailsjs/go/playlist/Service.d.ts create mode 100755 frontend/wailsjs/go/playlist/Service.js mode change 100644 => 100755 frontend/wailsjs/runtime/package.json mode change 100644 => 100755 frontend/wailsjs/runtime/runtime.d.ts mode change 100644 => 100755 frontend/wailsjs/runtime/runtime.js diff --git a/backend/app.go b/backend/app.go index f1c00db..172a03c 100644 --- a/backend/app.go +++ b/backend/app.go @@ -18,6 +18,7 @@ import ( "yellowjacket/backend/frontendutil" "yellowjacket/backend/library" "yellowjacket/backend/player" + "yellowjacket/backend/playlist" "yellowjacket/backend/queue" ) @@ -31,6 +32,7 @@ type YellowJacketApp struct { database *database.DB library *library.Library player *player.Player + playlist *playlist.Service queue *queue.Queue appContext context.Context appConfig *config.Config @@ -93,9 +95,13 @@ func NewYellowJacketApp( yjApp.assetHandler.RegisterHandler("/covers/", coverHandler) + // create playlist service + yjApp.playlist = playlist.NewService(yjApp.logger, yjApp.database) + yjApp.FEBindings = []any{ yjApp.FrontendUtil, yjApp.library, + yjApp.playlist, } return yjApp, nil @@ -113,6 +119,7 @@ func (yj *YellowJacketApp) OnStartup(ctx context.Context) { yj.appConfig.SetContext(ctx) yj.FrontendUtil.SetContext(ctx) yj.library.SetContext(ctx) + yj.playlist.SetContext(ctx) var err error // create player diff --git a/backend/database/sql/queries/playlists.sql b/backend/database/sql/queries/playlists.sql index 36156cc..599d588 100644 --- a/backend/database/sql/queries/playlists.sql +++ b/backend/database/sql/queries/playlists.sql @@ -30,3 +30,7 @@ DELETE FROM playlist_tracks WHERE id = ?; -- name: ClearPlaylistTracks :exec DELETE FROM playlist_tracks WHERE playlist_id = ?; + +-- name: GetNextPlaylistTrackPosition :one +SELECT COALESCE(MAX(position), -1) + 1 AS next_position +FROM playlist_tracks WHERE playlist_id = ?; diff --git a/backend/database/sql/sqlcgen/playlists.sql.go b/backend/database/sql/sqlcgen/playlists.sql.go index 45f5d34..fe4e944 100644 --- a/backend/database/sql/sqlcgen/playlists.sql.go +++ b/backend/database/sql/sqlcgen/playlists.sql.go @@ -99,6 +99,18 @@ func (q *Queries) GetAllPlaylists(ctx context.Context) ([]Playlist, error) { return items, nil } +const getNextPlaylistTrackPosition = `-- name: GetNextPlaylistTrackPosition :one +SELECT COALESCE(MAX(position), -1) + 1 AS next_position +FROM playlist_tracks WHERE playlist_id = ? +` + +func (q *Queries) GetNextPlaylistTrackPosition(ctx context.Context, playlistID int64) (int64, error) { + row := q.db.QueryRowContext(ctx, getNextPlaylistTrackPosition, playlistID) + var next_position int64 + err := row.Scan(&next_position) + return next_position, err +} + const getPlaylist = `-- name: GetPlaylist :one SELECT id, name, created_at, updated_at FROM playlists WHERE id = ? LIMIT 1 ` diff --git a/backend/playlist/playlist.go b/backend/playlist/playlist.go new file mode 100644 index 0000000..6bb9136 --- /dev/null +++ b/backend/playlist/playlist.go @@ -0,0 +1,190 @@ +// Package playlist provides playlist management functionality. +package playlist + +import ( + "context" + "errors" + "fmt" + "log/slog" + "strings" + + "yellowjacket/backend/database" + "yellowjacket/backend/database/sql/sqlcgen" +) + +var ( + errEmptyName = errors.New("playlist name cannot be empty") + errEmptyFilePath = errors.New("file path cannot be empty") + errNoFilePaths = errors.New("no file paths provided") +) + +// Summary is a lightweight representation of a playlist for the picker UI. +type Summary struct { + ID int64 `json:"ID"` + Name string `json:"Name"` +} + +// Service manages playlist operations. +type Service struct { + ctx context.Context + logger *slog.Logger + db *database.DB +} + +// NewService creates a new playlist service. +func NewService( + logger *slog.Logger, + db *database.DB, +) *Service { + return &Service{ + logger: logger.WithGroup("playlist"), + db: db, + } +} + +// SetContext sets the Wails runtime context. +func (s *Service) SetContext(ctx context.Context) { + s.ctx = ctx +} + +// GetAllPlaylists returns all playlists ordered by most recently updated. +func (s *Service) GetAllPlaylists() ([]Summary, error) { + playlists, err := s.db.Queries.GetAllPlaylists(s.db.Ctx) + if err != nil { + s.logger.Error("Failed to get playlists", "err", err) + + return nil, fmt.Errorf("failed to get playlists: %w", err) + } + + summaries := make([]Summary, 0, len(playlists)) + for _, p := range playlists { + summaries = append(summaries, Summary{ + ID: p.ID, + Name: p.Name, + }) + } + + return summaries, nil +} + +// CreatePlaylist creates a new empty playlist with the given name. +func (s *Service) CreatePlaylist(name string) (Summary, error) { + trimmed := strings.TrimSpace(name) + if trimmed == "" { + return Summary{}, errEmptyName + } + + created, err := s.db.Queries.CreatePlaylist(s.db.Ctx, trimmed) + if err != nil { + s.logger.Error("Failed to create playlist", "name", trimmed, "err", err) + + return Summary{}, fmt.Errorf("failed to create playlist: %w", err) + } + + s.logger.Info("Playlist created", "id", created.ID, "name", created.Name) + + return Summary{ID: created.ID, Name: created.Name}, nil +} + +// AddTracksToPlaylist adds one or more tracks to an existing playlist. +func (s *Service) AddTracksToPlaylist( + playlistID int64, + filePaths []string, +) error { + if len(filePaths) == 0 { + return errNoFilePaths + } + + nextPos, err := s.db.Queries.GetNextPlaylistTrackPosition( + s.db.Ctx, + playlistID, + ) + if err != nil { + s.logger.Error( + "Failed to get next position", + "playlistId", playlistID, + "err", err, + ) + + return fmt.Errorf("failed to get next track position: %w", err) + } + + for i, fp := range filePaths { + if err := s.addSingleTrack(playlistID, fp, nextPos+int64(i)); err != nil { + return err + } + } + + s.logger.Info( + "Tracks added to playlist", + "playlistId", playlistID, + "count", len(filePaths), + ) + + return nil +} + +// CreatePlaylistWithTracks creates a new playlist and populates it with tracks. +func (s *Service) CreatePlaylistWithTracks( + name string, + filePaths []string, +) (Summary, error) { + summary, err := s.CreatePlaylist(name) + if err != nil { + return Summary{}, err + } + + if len(filePaths) > 0 { + if err := s.AddTracksToPlaylist(summary.ID, filePaths); err != nil { + return Summary{}, fmt.Errorf( + "playlist created but failed to add tracks: %w", + err, + ) + } + } + + return summary, nil +} + +// addSingleTrack looks up the audio file by path and inserts it into the playlist. +func (s *Service) addSingleTrack( + playlistID int64, + filePath string, + position int64, +) error { + if strings.TrimSpace(filePath) == "" { + return errEmptyFilePath + } + + audioFile, err := s.db.Queries.GetAudioFileByPath(s.db.Ctx, filePath) + if err != nil { + s.logger.Error( + "Failed to find audio file", + "filePath", filePath, + "err", err, + ) + + return fmt.Errorf("failed to find audio file %q: %w", filePath, err) + } + + _, err = s.db.Queries.AddPlaylistTrack( + s.db.Ctx, + sqlcgen.AddPlaylistTrackParams{ + PlaylistID: playlistID, + AudioFileID: audioFile.ID, + Position: position, + }, + ) + if err != nil { + s.logger.Error( + "Failed to add track to playlist", + "playlistId", playlistID, + "audioFileId", audioFile.ID, + "err", err, + ) + + return fmt.Errorf("failed to add track to playlist: %w", err) + } + + return nil +} diff --git a/frontend/index.ts b/frontend/index.ts index ee38a47..0f1d5de 100644 --- a/frontend/index.ts +++ b/frontend/index.ts @@ -4,6 +4,7 @@ import '@components/cover-grid/cover-grid.ts'; import '@components/now-playing/now-playing.ts'; import '@components/sidebar/app-sidebar.ts'; import '@components/queue-panel/queue-panel.ts'; +import '@components/playlist-view/playlist-view.ts'; import '@awesome.me/webawesome/dist/styles/themes/default.css'; import '@awesome.me/webawesome/dist/components/icon/icon.js'; import { setBasePath } from '@awesome.me/webawesome/dist/webawesome.js'; @@ -24,6 +25,9 @@ document.addEventListener('navigate', (e: Event) => { case 'tracks': mainContent.innerHTML = ''; break; + case 'playlists': + mainContent.innerHTML = ''; + break; default: mainContent.innerHTML = `

Coming soon: ${view}

@@ -46,8 +50,4 @@ if (queueButton && queuePanel) { } }); - // Close panel when the component dispatches a close event - queuePanel.addEventListener('queue-panel-close', () => { - queuePanel.removeAttribute('open'); - }); } diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index b7e89aa..5746819 100755 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -74e25cdcdccb20fc50b40dc29ec5f6f9 \ No newline at end of file +cf76bbfd46ad4447fbfbaa4a1c6845ca \ No newline at end of file diff --git a/frontend/src/components/cover-grid/cover-grid.ts b/frontend/src/components/cover-grid/cover-grid.ts index e45fb14..4b0b3ab 100644 --- a/frontend/src/components/cover-grid/cover-grid.ts +++ b/frontend/src/components/cover-grid/cover-grid.ts @@ -9,6 +9,8 @@ import { grid } from '@lit-labs/virtualizer/layouts/grid.js'; import '@awesome.me/webawesome/dist/components/popup/popup.js'; import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js'; import '@awesome.me/webawesome/dist/components/icon/icon.js'; +import '@components/playlist-picker/playlist-picker.js'; +import type { PlaylistPicker } from '@components/playlist-picker/playlist-picker.js'; @customElement('cover-grid') export class CoverGrid extends LitElement { @@ -143,6 +145,20 @@ export class CoverGrid extends LitElement { .context-menu-panel wa-dropdown-item:hover { background-color: rgba(255, 255, 255, 0.1); } + + .submenu-item { + position: relative; + } + + .submenu-arrow { + font-size: 10px; + margin-left: auto; + padding-left: 12px; + } + + #playlist-submenu { + z-index: 210; + } `; @state() @@ -157,9 +173,18 @@ export class CoverGrid extends LitElement { @state() private contextMenuAlbum: library.Album | null = null; + @state() + private playlistSubmenuOpen = false; + + @state() + private playlistFilePaths: string[] = []; + @query('#context-menu') private contextMenuPopup!: HTMLElement; + @query('#playlist-submenu') + private playlistSubmenuPopup!: HTMLElement; + override connectedCallback() { super.connectedCallback(); this.loadAlbums(); @@ -253,8 +278,10 @@ export class CoverGrid extends LitElement { private closeContextMenu() { if (!this.contextMenuOpen) return; + this.closePlaylistSubmenu(); this.contextMenuOpen = false; this.contextMenuAlbum = null; + this.playlistFilePaths = []; const popup = this.contextMenuPopup; @@ -263,6 +290,52 @@ export class CoverGrid extends LitElement { } } + private async showPlaylistSubmenu() { + if (this.playlistSubmenuOpen) return; + + if (this.contextMenuAlbum) { + this.playlistFilePaths = await this.getAlbumFilePaths( + this.contextMenuAlbum, + ); + } + + this.playlistSubmenuOpen = true; + + await this.updateComplete; + + const submenu = this.playlistSubmenuPopup; + const trigger = this.shadowRoot?.querySelector( + '.submenu-item', + ); + + if (submenu && trigger) { + (submenu as any).anchor = trigger; + (submenu as any).active = true; + } + + const picker = this.shadowRoot?.querySelector( + 'playlist-picker', + ) as PlaylistPicker | null; + + picker?.reset(); + } + + private closePlaylistSubmenu() { + if (!this.playlistSubmenuOpen) return; + + this.playlistSubmenuOpen = false; + + const submenu = this.playlistSubmenuPopup; + + if (submenu) { + (submenu as any).active = false; + } + } + + private onPlaylistActionComplete = () => { + this.closeContextMenu(); + }; + private renderAlbumCard = (album: library.Album): unknown => { return html`
Play Next + this.showPlaylistSubmenu()} + @click=${(e: Event) => { + e.stopPropagation(); + void this.showPlaylistSubmenu(); + }} + > + + Add to Playlist + +
` : nothing} + + + ${this.playlistSubmenuOpen + ? html` + e.stopPropagation()} + > + ` + : nothing} + `; } } diff --git a/frontend/src/components/playlist-picker/playlist-picker.ts b/frontend/src/components/playlist-picker/playlist-picker.ts new file mode 100644 index 0000000..fde4416 --- /dev/null +++ b/frontend/src/components/playlist-picker/playlist-picker.ts @@ -0,0 +1,299 @@ +import { LitElement, html, css, nothing } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; + +import '@awesome.me/webawesome/dist/components/icon/icon.js'; +import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js'; + +import { + GetAllPlaylists, + AddTracksToPlaylist, + CreatePlaylistWithTracks, +} from '@go/playlist/Service'; +import type { playlist } from '@go/models'; + +/** + * A reusable playlist picker that displays existing playlists + * and allows creating new ones. Accepts file paths and handles + * adding tracks to the selected/created playlist. + * + * @fires playlist-action-complete - When tracks have been added successfully. + */ +@customElement('playlist-picker') +export class PlaylistPicker extends LitElement { + /** File paths to add when a playlist is selected or created. */ + @property({ type: Array }) filePaths: string[] = []; + + @state() private mode: 'list' | 'create' = 'list'; + @state() private playlists: playlist.Summary[] = []; + @state() private newPlaylistName = ''; + @state() private loading = false; + + static override styles = css` + :host { + display: block; + } + + .picker-panel { + background-color: #343a40; + border: 1px solid #444; + border-radius: 6px; + padding: 4px 0; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5); + min-width: 180px; + max-height: 300px; + overflow-y: auto; + } + + .picker-panel wa-dropdown-item { + cursor: pointer; + --wa-color-text-normal: #fff; + font-size: 13px; + } + + .picker-panel wa-dropdown-item:hover { + background-color: rgba(255, 255, 255, 0.1); + } + + .separator { + height: 1px; + background: #555; + margin: 4px 0; + } + + .create-form { + padding: 8px 12px; + display: flex; + flex-direction: column; + gap: 8px; + } + + .create-form input { + background: #2a2d30; + border: 1px solid #555; + border-radius: 4px; + color: #fff; + padding: 6px 8px; + font-size: 13px; + outline: none; + font-family: inherit; + } + + .create-form input:focus { + border-color: #ffd43b; + } + + .create-form input::placeholder { + color: #888; + } + + .button-row { + display: flex; + gap: 6px; + justify-content: flex-end; + } + + .button-row button { + background: #495057; + border: none; + border-radius: 4px; + color: #fff; + padding: 4px 10px; + font-size: 12px; + cursor: pointer; + font-family: inherit; + } + + .button-row button:hover { + background: #5a6268; + } + + .button-row button.primary { + background: #ffd43b; + color: #000; + } + + .button-row button.primary:hover { + background: #ffe066; + } + + .button-row button.primary:disabled { + background: #665a1e; + color: #888; + cursor: not-allowed; + } + + .empty-message { + padding: 8px 12px; + color: #888; + font-size: 13px; + } + `; + + override connectedCallback() { + super.connectedCallback(); + this.loadPlaylists(); + } + + private async loadPlaylists() { + try { + this.playlists = await GetAllPlaylists(); + } catch (err) { + console.error('Failed to load playlists:', err); + this.playlists = []; + } + } + + private handleSelectPlaylist = async (playlistId: number) => { + if (this.loading || this.filePaths.length === 0) return; + + this.loading = true; + + try { + await AddTracksToPlaylist(playlistId, this.filePaths); + this.dispatchComplete(); + } catch (err) { + console.error('Failed to add tracks to playlist:', err); + } finally { + this.loading = false; + } + }; + + private handleShowCreate = () => { + this.mode = 'create'; + this.newPlaylistName = ''; + + void this.updateComplete.then(() => { + const input = + this.shadowRoot?.querySelector( + '.create-form input', + ); + + input?.focus(); + }); + }; + + private handleCancelCreate = () => { + this.mode = 'list'; + this.newPlaylistName = ''; + }; + + private handleCreatePlaylist = async () => { + const name = this.newPlaylistName.trim(); + if (!name || this.loading) return; + + this.loading = true; + + try { + await CreatePlaylistWithTracks(name, this.filePaths); + this.dispatchComplete(); + } catch (err) { + console.error('Failed to create playlist:', err); + } finally { + this.loading = false; + } + }; + + private handleInputChange = (e: Event) => { + const input = e.target as HTMLInputElement; + this.newPlaylistName = input.value; + }; + + private handleInputKeydown = (e: KeyboardEvent) => { + if (e.key === 'Enter') { + void this.handleCreatePlaylist(); + } else if (e.key === 'Escape') { + this.handleCancelCreate(); + } + + // Stop propagation so parent context menu handlers don't interfere. + e.stopPropagation(); + }; + + private dispatchComplete() { + this.dispatchEvent( + new CustomEvent('playlist-action-complete', { + bubbles: true, + composed: true, + }), + ); + } + + /** Resets the picker to its initial list state. */ + reset() { + this.mode = 'list'; + this.newPlaylistName = ''; + this.loading = false; + this.loadPlaylists(); + } + + override render() { + if (this.mode === 'create') { + return this.renderCreateForm(); + } + + return this.renderPlaylistList(); + } + + private renderPlaylistList() { + return html` +
+ ${this.playlists.length > 0 + ? html` + ${this.playlists.map( + (p) => html` + + this.handleSelectPlaylist(p.ID)} + > + ${p.Name} + + `, + )} +
+ ` + : nothing} + + + New Playlist + +
+ `; + } + + private renderCreateForm() { + const canCreate = this.newPlaylistName.trim().length > 0; + + return html` +
+
+ e.stopPropagation()} + /> +
+ + +
+
+
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'playlist-picker': PlaylistPicker; + } +} diff --git a/frontend/src/components/playlist-view/playlist-view.ts b/frontend/src/components/playlist-view/playlist-view.ts new file mode 100644 index 0000000..fd1042d --- /dev/null +++ b/frontend/src/components/playlist-view/playlist-view.ts @@ -0,0 +1,325 @@ +import { LitElement, html, css } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; + +import '@awesome.me/webawesome/dist/components/icon/icon.js'; + +import { + GetAllPlaylists, + CreatePlaylist, +} from '@go/playlist/Service'; +import type { playlist } from '@go/models'; + +@customElement('playlist-view') +export class PlaylistView extends LitElement { + @state() private playlists: playlist.Summary[] = []; + @state() private loading = true; + @state() private creating = false; + @state() private newPlaylistName = ''; + + static override styles = css` + :host { + display: flex; + flex-direction: column; + overflow: hidden; + } + + .header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px; + flex-shrink: 0; + border-bottom: 1px solid #333; + } + + .header h2 { + margin: 0; + font-size: 18px; + font-weight: 600; + color: #fff; + } + + .new-playlist-button { + background: none; + border: 1px solid #555; + border-radius: 4px; + color: #fff; + padding: 6px 12px; + font-size: 13px; + cursor: pointer; + display: flex; + align-items: center; + gap: 6px; + font-family: inherit; + } + + .new-playlist-button:hover { + border-color: #ffd43b; + color: #ffd43b; + } + + .create-form { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 16px; + border-bottom: 1px solid #333; + flex-shrink: 0; + } + + .create-form input { + flex: 1; + background: #2a2d30; + border: 1px solid #555; + border-radius: 4px; + color: #fff; + padding: 6px 10px; + font-size: 13px; + outline: none; + font-family: inherit; + } + + .create-form input:focus { + border-color: #ffd43b; + } + + .create-form input::placeholder { + color: #888; + } + + .create-form button { + background: #495057; + border: none; + border-radius: 4px; + color: #fff; + padding: 6px 12px; + font-size: 13px; + cursor: pointer; + font-family: inherit; + } + + .create-form button:hover { + background: #5a6268; + } + + .create-form button.primary { + background: #ffd43b; + color: #000; + } + + .create-form button.primary:hover { + background: #ffe066; + } + + .create-form button.primary:disabled { + background: #665a1e; + color: #888; + cursor: not-allowed; + } + + .playlist-list { + flex: 1; + overflow-y: auto; + padding: 0; + margin: 0; + list-style: none; + } + + .playlist-item { + display: flex; + align-items: center; + padding: 12px 16px; + gap: 12px; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + } + + .playlist-item:hover { + background-color: rgba(255, 255, 255, 0.05); + } + + .playlist-icon { + font-size: 18px; + color: #888; + flex-shrink: 0; + } + + .playlist-name { + font-size: 14px; + color: #fff; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .loading { + display: flex; + justify-content: center; + align-items: center; + padding: 32px; + color: #b3b3b3; + } + + .empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 48px 20px; + color: #b3b3b3; + text-align: center; + gap: 8px; + } + + .empty-state wa-icon { + font-size: 32px; + } + + .empty-state p { + margin: 4px 0; + } + `; + + override connectedCallback() { + super.connectedCallback(); + this.loadPlaylists(); + } + + private async loadPlaylists() { + try { + this.loading = true; + const result = await GetAllPlaylists(); + this.playlists = result ?? []; + } catch (err) { + console.error('Failed to load playlists:', err); + this.playlists = []; + } finally { + this.loading = false; + } + } + + private handleNewPlaylistClick = () => { + this.creating = true; + this.newPlaylistName = ''; + + void this.updateComplete.then(() => { + const input = + this.shadowRoot?.querySelector( + '.create-form input', + ); + + input?.focus(); + }); + }; + + private handleCancelCreate = () => { + this.creating = false; + this.newPlaylistName = ''; + }; + + private handleCreatePlaylist = async () => { + const name = this.newPlaylistName.trim(); + if (!name) return; + + try { + await CreatePlaylist(name); + this.creating = false; + this.newPlaylistName = ''; + await this.loadPlaylists(); + } catch (err) { + console.error('Failed to create playlist:', err); + } + }; + + private handleInputChange = (e: Event) => { + const input = e.target as HTMLInputElement; + this.newPlaylistName = input.value; + }; + + private handleInputKeydown = (e: KeyboardEvent) => { + if (e.key === 'Enter') { + void this.handleCreatePlaylist(); + } else if (e.key === 'Escape') { + this.handleCancelCreate(); + } + }; + + override render() { + return html` +
+

Playlists

+ +
+ + ${this.creating ? this.renderCreateForm() : ''} + ${this.loading + ? html`
Loading playlists...
` + : this.renderPlaylistList()} + `; + } + + private renderCreateForm() { + const canCreate = this.newPlaylistName.trim().length > 0; + + return html` +
+ + + +
+ `; + } + + private renderPlaylistList() { + if (this.playlists.length === 0) { + return html` +
+ +

No playlists yet

+

+ Create a playlist to get started. +

+
+ `; + } + + return html` +
    + ${this.playlists.map( + (p) => html` +
  • + + ${p.Name} +
  • + `, + )} +
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'playlist-view': PlaylistView; + } +} diff --git a/frontend/src/components/queue-panel/queue-panel.ts b/frontend/src/components/queue-panel/queue-panel.ts index 0e000d7..43b4948 100644 --- a/frontend/src/components/queue-panel/queue-panel.ts +++ b/frontend/src/components/queue-panel/queue-panel.ts @@ -1,7 +1,10 @@ -import { LitElement, html, css, unsafeCSS } from 'lit'; -import { customElement, property, state } from 'lit/decorators.js'; +import { LitElement, html, css, nothing, unsafeCSS } from 'lit'; +import { customElement, property, state, query } from 'lit/decorators.js'; import '@awesome.me/webawesome/dist/components/icon/icon.js'; +import '@awesome.me/webawesome/dist/components/popup/popup.js'; import { QueueController } from '@store/controllers/queue-controller'; +import '@components/playlist-picker/playlist-picker.js'; +import type { PlaylistPicker } from '@components/playlist-picker/playlist-picker.js'; const MIN_WIDTH = 200; const MAX_WIDTH = 500; @@ -17,6 +20,22 @@ export class QueuePanel extends LitElement { @state() private isDragging = false; + @state() + private playlistPickerOpen = false; + + @query('#save-playlist-popup') + private savePlaylistPopup!: HTMLElement; + + private closePickerHandler = (e: MouseEvent) => { + const path = e.composedPath(); + const popup = this.savePlaylistPopup; + const btn = this.shadowRoot?.querySelector('.save-playlist-button'); + + if (popup && !path.includes(popup) && (!btn || !path.includes(btn))) { + this.closePlaylistPicker(); + } + }; + private panelWidth = DEFAULT_WIDTH; static override styles = css` @@ -75,7 +94,7 @@ export class QueuePanel extends LitElement { font-weight: 600; } - .close-button { + .save-playlist-button { background: none; border: none; color: inherit; @@ -85,10 +104,19 @@ export class QueuePanel extends LitElement { align-items: center; } - .close-button:hover { + .save-playlist-button:hover { color: #ffd43b; } + .save-playlist-button:disabled { + color: #555; + cursor: not-allowed; + } + + #save-playlist-popup { + z-index: 210; + } + .track-list { flex: 1; overflow-y: auto; @@ -193,24 +221,56 @@ export class QueuePanel extends LitElement { this.style.setProperty('--queue-width', `${this.panelWidth}px`); document.addEventListener('mousemove', this.handleMouseMove); document.addEventListener('mouseup', this.handleMouseUp); + document.addEventListener('click', this.closePickerHandler); } override disconnectedCallback() { super.disconnectedCallback(); document.removeEventListener('mousemove', this.handleMouseMove); document.removeEventListener('mouseup', this.handleMouseUp); + document.removeEventListener('click', this.closePickerHandler); } - private handleClose() { - this.open = false; - this.dispatchEvent( - new CustomEvent('queue-panel-close', { - bubbles: true, - composed: true, - }), - ); + private async handleSaveAsPlaylist() { + if (this.queue.tracks.length === 0) return; + + this.playlistPickerOpen = !this.playlistPickerOpen; + + await this.updateComplete; + + const popup = this.savePlaylistPopup; + const btn = this.shadowRoot?.querySelector('.save-playlist-button'); + + if (popup && btn) { + (popup as any).anchor = btn; + (popup as any).active = this.playlistPickerOpen; + } + + if (this.playlistPickerOpen) { + const picker = this.shadowRoot?.querySelector( + 'playlist-picker', + ) as PlaylistPicker | null; + + picker?.reset(); + } } + private closePlaylistPicker() { + if (!this.playlistPickerOpen) return; + + this.playlistPickerOpen = false; + + const popup = this.savePlaylistPopup; + + if (popup) { + (popup as any).active = false; + } + } + + private onPlaylistActionComplete = () => { + this.closePlaylistPicker(); + }; + private handleRemoveTrack(e: Event, position: number) { e.stopPropagation(); this.queue.removeFromQueue(position); @@ -275,11 +335,32 @@ export class QueuePanel extends LitElement { >

Queue

-
+ + ${this.playlistPickerOpen + ? html` + t.filePath)} + @playlist-action-complete=${this.onPlaylistActionComplete} + @click=${(e: Event) => e.stopPropagation()} + > + ` + : nothing} + + ${tracks.length === 0 ? html`
diff --git a/frontend/src/components/track-list/track-list.ts b/frontend/src/components/track-list/track-list.ts index b2d0e47..1b7a52a 100644 --- a/frontend/src/components/track-list/track-list.ts +++ b/frontend/src/components/track-list/track-list.ts @@ -11,6 +11,8 @@ import { flow } from '@lit-labs/virtualizer/layouts/flow.js'; import '@awesome.me/webawesome/dist/components/popup/popup.js'; import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js'; import '@awesome.me/webawesome/dist/components/icon/icon.js'; +import '@components/playlist-picker/playlist-picker.js'; +import type { PlaylistPicker } from '@components/playlist-picker/playlist-picker.js'; @customElement('track-list') export class TrackList extends LitElement { @@ -26,9 +28,15 @@ export class TrackList extends LitElement { @state() private contextMenuTrack: library.Track | null = null; + @state() + private playlistSubmenuOpen = false; + @query('#context-menu') private contextMenuPopup!: HTMLElement; + @query('#playlist-submenu') + private playlistSubmenuPopup!: HTMLElement; + private closeHandler = () => this.closeContextMenu(); static override styles = css` @@ -128,6 +136,20 @@ export class TrackList extends LitElement { .context-menu-panel wa-dropdown-item:hover { background-color: rgba(255, 255, 255, 0.1); } + + .submenu-item { + position: relative; + } + + .submenu-arrow { + font-size: 10px; + margin-left: auto; + padding-left: 12px; + } + + #playlist-submenu { + z-index: 210; + } `; override connectedCallback() { @@ -214,6 +236,7 @@ export class TrackList extends LitElement { private closeContextMenu() { if (!this.contextMenuOpen) return; + this.closePlaylistSubmenu(); this.contextMenuOpen = false; this.contextMenuTrack = null; @@ -224,6 +247,44 @@ export class TrackList extends LitElement { } } + private async showPlaylistSubmenu() { + if (this.playlistSubmenuOpen) return; + + this.playlistSubmenuOpen = true; + + await this.updateComplete; + + const submenu = this.playlistSubmenuPopup; + const trigger = this.shadowRoot?.querySelector('.submenu-item'); + + if (submenu && trigger) { + (submenu as any).anchor = trigger; + (submenu as any).active = true; + } + + const picker = this.shadowRoot?.querySelector( + 'playlist-picker', + ) as PlaylistPicker | null; + + picker?.reset(); + } + + private closePlaylistSubmenu() { + if (!this.playlistSubmenuOpen) return; + + this.playlistSubmenuOpen = false; + + const submenu = this.playlistSubmenuPopup; + + if (submenu) { + (submenu as any).active = false; + } + } + + private onPlaylistActionComplete = () => { + this.closeContextMenu(); + }; + private isActiveTrack(track: library.Track): boolean { const currentTrack = this.player.currentTrack; @@ -298,10 +359,38 @@ export class TrackList extends LitElement { Play Next + this.showPlaylistSubmenu()} + @click=${(e: Event) => { + e.stopPropagation(); + void this.showPlaylistSubmenu(); + }} + > + + Add to Playlist + +
` : nothing} + + + ${this.playlistSubmenuOpen && this.contextMenuTrack + ? html` + e.stopPropagation()} + > + ` + : nothing} + `; } } diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index 267b08b..bad5357 100755 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -43,3 +43,22 @@ export namespace library { } +export namespace playlist { + + export class Summary { + ID: number; + Name: string; + + static createFrom(source: any = {}) { + return new Summary(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.ID = source["ID"]; + this.Name = source["Name"]; + } + } + +} + diff --git a/frontend/wailsjs/go/playlist/Service.d.ts b/frontend/wailsjs/go/playlist/Service.d.ts new file mode 100755 index 0000000..5a6a21d --- /dev/null +++ b/frontend/wailsjs/go/playlist/Service.d.ts @@ -0,0 +1,14 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT +import {playlist} from '../models'; +import {context} from '../models'; + +export function AddTracksToPlaylist(arg1:number,arg2:Array):Promise; + +export function CreatePlaylist(arg1:string):Promise; + +export function CreatePlaylistWithTracks(arg1:string,arg2:Array):Promise; + +export function GetAllPlaylists():Promise>; + +export function SetContext(arg1:context.Context):Promise; diff --git a/frontend/wailsjs/go/playlist/Service.js b/frontend/wailsjs/go/playlist/Service.js new file mode 100755 index 0000000..e260d5a --- /dev/null +++ b/frontend/wailsjs/go/playlist/Service.js @@ -0,0 +1,23 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function AddTracksToPlaylist(arg1, arg2) { + return window['go']['playlist']['Service']['AddTracksToPlaylist'](arg1, arg2); +} + +export function CreatePlaylist(arg1) { + return window['go']['playlist']['Service']['CreatePlaylist'](arg1); +} + +export function CreatePlaylistWithTracks(arg1, arg2) { + return window['go']['playlist']['Service']['CreatePlaylistWithTracks'](arg1, arg2); +} + +export function GetAllPlaylists() { + return window['go']['playlist']['Service']['GetAllPlaylists'](); +} + +export function SetContext(arg1) { + return window['go']['playlist']['Service']['SetContext'](arg1); +} diff --git a/frontend/wailsjs/runtime/package.json b/frontend/wailsjs/runtime/package.json old mode 100644 new mode 100755 diff --git a/frontend/wailsjs/runtime/runtime.d.ts b/frontend/wailsjs/runtime/runtime.d.ts old mode 100644 new mode 100755 diff --git a/frontend/wailsjs/runtime/runtime.js b/frontend/wailsjs/runtime/runtime.js old mode 100644 new mode 100755 diff --git a/go.mod b/go.mod index 50fa355..bff8311 100644 --- a/go.mod +++ b/go.mod @@ -85,6 +85,8 @@ require ( github.com/ckaznocha/intrange v0.3.1 // indirect github.com/cli/browser v1.3.0 // indirect github.com/clipperhouse/uax29/v2 v2.2.0 // indirect + github.com/cloudflare/circl v1.3.7 // indirect + github.com/containerd/console v1.0.3 // indirect github.com/creack/pty v1.1.24 // indirect github.com/cubicdaiya/gonp v1.0.4 // indirect github.com/curioswitch/go-reassign v0.3.0 // indirect @@ -96,7 +98,9 @@ require ( github.com/dlclark/regexp2 v1.11.5 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/ebitengine/oto/v3 v3.3.3 // indirect - github.com/ebitengine/purego v0.8.4 // indirect + github.com/ebitengine/purego v0.9.1 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/ettle/strcase v0.2.0 // indirect github.com/evilmartians/lefthook v1.13.6 // indirect github.com/fatih/color v1.18.0 // indirect github.com/fatih/structtag v1.2.0 // indirect @@ -341,7 +345,6 @@ require ( golang.org/x/crypto v0.48.0 // indirect golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/exp/typeparams v0.0.0-20251125195548-87e1e737ad39 // indirect - golang.org/x/image v0.12.0 // indirect golang.org/x/mod v0.33.0 // indirect golang.org/x/net v0.50.0 // indirect golang.org/x/sys v0.41.0 // indirect @@ -377,6 +380,7 @@ tool ( github.com/sqlc-dev/sqlc/cmd/sqlc github.com/wailsapp/wails/v2/cmd/wails golang.org/x/vuln/cmd/govulncheck + yellowjacket ) // replace github.com/TheCodeOfCaleb/beep/v2 => /mnt/vault/dev/golang/beep/ diff --git a/go.sum b/go.sum index 5661295..ad1b980 100644 --- a/go.sum +++ b/go.sum @@ -216,8 +216,14 @@ github.com/ckaznocha/intrange v0.3.1 h1:j1onQyXvHUsPWujDH6WIjhyH26gkRt/txNlV7Lsp github.com/ckaznocha/intrange v0.3.1/go.mod h1:QVepyz1AkUoFQkpEqksSYpNpUo3c5W7nWh/s6SHIJJk= github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo= github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY= github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= @@ -274,8 +280,27 @@ github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3 github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= +github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/ghostiam/protogetter v0.3.20 h1:oW7OPFit2FxZOpmMRPP9FffU4uUpfeE/rEdE1f+MzD0= +github.com/ghostiam/protogetter v0.3.20/go.mod h1:FjIu5Yfs6FT391m+Fjp3fbAYJ6rkL/J6ySpZBfnODuI= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= +github.com/go-critic/go-critic v0.14.3 h1:5R1qH2iFeo4I/RJU8vTezdqs08Egi4u5p6vOESA0pog= +github.com/go-critic/go-critic v0.14.3/go.mod h1:xwntfW6SYAd7h1OqDzmN6hBX/JxsEKl5up/Y2bsxgVQ= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= +github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.13.2 h1:7O7xvsK7K+rZPKW6AQR1YyNhfywkv7B8/FsP3ki6Zv0= +github.com/go-git/go-git/v5 v5.13.2/go.mod h1:hWdW5P4YZRjmpGHwRH2v3zkWcNl6HeXaXQEMGb3NJ9A= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU= github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -516,6 +541,10 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jszwec/csvutil v1.5.1/go.mod h1:Rpu7Uu9giO9subDyMCIQfHVDuLrcaC36UA4YcJjGBkg= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/julz/importas v0.2.0 h1:y+MJN/UdL63QbFJHws9BVC5RpA2iq0kpjrFajTGivjQ= +github.com/julz/importas v0.2.0/go.mod h1:pThlt589EnCYtMnmhmRYY/qn9lCf/frPOK+WMx3xiJY= github.com/kaptinlin/go-i18n v0.2.2 h1:kebVCZme/BrCTqonh/J+VYCl1+Of5C18bvyn3DRPl5M= github.com/kaptinlin/go-i18n v0.2.2/go.mod h1:MiwkeHryBopAhC/M3zEwIM/2IN8TvTqJQswPw6kceqM= github.com/kaptinlin/jsonpointer v0.4.8 h1:HocHcXrOBfP/nUJw0YYjed/TlQvuCAY6uRs3Qok7F6g= @@ -625,6 +654,8 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-tty v0.0.7 h1:KJ486B6qI8+wBO7kQxYgmmEFDaFEE96JMBQ7h400N8Q= @@ -778,6 +809,14 @@ github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDj github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs= github.com/samber/lo v1.50.0 h1:XrG0xOeHs+4FQ8gJR97zDz5uOFMW7OwFWiFVzqopKgY= github.com/samber/lo v1.50.0/go.mod h1:RjZyNk6WSnUFRKK6EyOhsRJMqft3G+pg7dCWHQCWvsc= +github.com/sanposhiho/wastedassign/v2 v2.1.0 h1:crurBF7fJKIORrV85u9UUpePDYGWnwvv3+A96WvwXT0= +github.com/sanposhiho/wastedassign/v2 v2.1.0/go.mod h1:+oSmSC+9bQ+VUAxA66nBb0Z7N8CK7mscKTDYC6aIek4= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tMEOsumirXcOJqAw= +github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ= +github.com/sashamelentyev/usestdlibvars v1.29.0 h1:8J0MoRrw4/NAXtjQqTHrbW9NN+3iMf7Knkq057v4XOQ= +github.com/sashamelentyev/usestdlibvars v1.29.0/go.mod h1:8PpnjHMk5VdeWlVb4wCdrB8PNbLqZ3wBZTZWkrpZZL8= github.com/schollz/progressbar/v3 v3.19.0 h1:Ea18xuIRQXLAUidVDox3AbwfUhD0/1IvohyTutOIFoc= github.com/schollz/progressbar/v3 v3.19.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec= github.com/securego/gosec/v2 v2.22.11 h1:tW+weM/hCM/GX3iaCV91d5I6hqaRT2TPsFM1+USPXwg= @@ -971,8 +1010,21 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= @@ -985,6 +1037,12 @@ golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+o golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4= golang.org/x/image v0.12.0 h1:w13vZbU4o5rKOFFR8y7M+c4A5jXDC0uXTdHYRP8X2DQ= golang.org/x/image v0.12.0/go.mod h1:Lu90jvHG7GfemOIcldsh9A2hS01ocl6oNO7ype5mEnk= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= @@ -1002,8 +1060,15 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= -golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -1037,8 +1102,21 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1048,6 +1126,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1105,15 +1185,24 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4 h1:bTLqdHv7xrGlFbvf5/TXNxy/iUwwdkjhqQTJDjW7aj0= +golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4/go.mod h1:g5NllXBEermZrmR51cJDQxmJUHUOfRAaNyWBM+R+548= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= -golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -1121,9 +1210,13 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -1174,8 +1267,16 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= -golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= +golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= +golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= +golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= +golang.org/x/vuln v1.1.4 h1:Ju8QsuyhX3Hk8ma3CesTbO8vfJD9EvUBgHvkxHBzj0I= +golang.org/x/vuln v1.1.4/go.mod h1:F+45wmU18ym/ca5PLTPLsSzr2KppzswxPP603ldA67s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=