playlist creation, viewing, integration with other views. still wip for full playlist functionality

This commit is contained in:
2026-02-16 00:27:36 -05:00
parent 666ff9869e
commit 4c09e7abee
19 changed files with 1304 additions and 35 deletions
+7
View File
@@ -18,6 +18,7 @@ import (
"yellowjacket/backend/frontendutil" "yellowjacket/backend/frontendutil"
"yellowjacket/backend/library" "yellowjacket/backend/library"
"yellowjacket/backend/player" "yellowjacket/backend/player"
"yellowjacket/backend/playlist"
"yellowjacket/backend/queue" "yellowjacket/backend/queue"
) )
@@ -31,6 +32,7 @@ type YellowJacketApp struct {
database *database.DB database *database.DB
library *library.Library library *library.Library
player *player.Player player *player.Player
playlist *playlist.Service
queue *queue.Queue queue *queue.Queue
appContext context.Context appContext context.Context
appConfig *config.Config appConfig *config.Config
@@ -93,9 +95,13 @@ func NewYellowJacketApp(
yjApp.assetHandler.RegisterHandler("/covers/", coverHandler) yjApp.assetHandler.RegisterHandler("/covers/", coverHandler)
// create playlist service
yjApp.playlist = playlist.NewService(yjApp.logger, yjApp.database)
yjApp.FEBindings = []any{ yjApp.FEBindings = []any{
yjApp.FrontendUtil, yjApp.FrontendUtil,
yjApp.library, yjApp.library,
yjApp.playlist,
} }
return yjApp, nil return yjApp, nil
@@ -113,6 +119,7 @@ func (yj *YellowJacketApp) OnStartup(ctx context.Context) {
yj.appConfig.SetContext(ctx) yj.appConfig.SetContext(ctx)
yj.FrontendUtil.SetContext(ctx) yj.FrontendUtil.SetContext(ctx)
yj.library.SetContext(ctx) yj.library.SetContext(ctx)
yj.playlist.SetContext(ctx)
var err error var err error
// create player // create player
@@ -30,3 +30,7 @@ DELETE FROM playlist_tracks WHERE id = ?;
-- name: ClearPlaylistTracks :exec -- name: ClearPlaylistTracks :exec
DELETE FROM playlist_tracks WHERE playlist_id = ?; 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 = ?;
@@ -99,6 +99,18 @@ func (q *Queries) GetAllPlaylists(ctx context.Context) ([]Playlist, error) {
return items, nil 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 const getPlaylist = `-- name: GetPlaylist :one
SELECT id, name, created_at, updated_at FROM playlists WHERE id = ? LIMIT 1 SELECT id, name, created_at, updated_at FROM playlists WHERE id = ? LIMIT 1
` `
+190
View File
@@ -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
}
+4 -4
View File
@@ -4,6 +4,7 @@ import '@components/cover-grid/cover-grid.ts';
import '@components/now-playing/now-playing.ts'; import '@components/now-playing/now-playing.ts';
import '@components/sidebar/app-sidebar.ts'; import '@components/sidebar/app-sidebar.ts';
import '@components/queue-panel/queue-panel.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/styles/themes/default.css';
import '@awesome.me/webawesome/dist/components/icon/icon.js'; import '@awesome.me/webawesome/dist/components/icon/icon.js';
import { setBasePath } from '@awesome.me/webawesome/dist/webawesome.js'; import { setBasePath } from '@awesome.me/webawesome/dist/webawesome.js';
@@ -24,6 +25,9 @@ document.addEventListener('navigate', (e: Event) => {
case 'tracks': case 'tracks':
mainContent.innerHTML = '<track-list></track-list>'; mainContent.innerHTML = '<track-list></track-list>';
break; break;
case 'playlists':
mainContent.innerHTML = '<playlist-view></playlist-view>';
break;
default: default:
mainContent.innerHTML = `<div style="padding: 1em; color: #b3b3b3;"> mainContent.innerHTML = `<div style="padding: 1em; color: #b3b3b3;">
<p>Coming soon: ${view}</p> <p>Coming soon: ${view}</p>
@@ -46,8 +50,4 @@ if (queueButton && queuePanel) {
} }
}); });
// Close panel when the component dispatches a close event
queuePanel.addEventListener('queue-panel-close', () => {
queuePanel.removeAttribute('open');
});
} }
+1 -1
View File
@@ -1 +1 @@
74e25cdcdccb20fc50b40dc29ec5f6f9 cf76bbfd46ad4447fbfbaa4a1c6845ca
@@ -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/popup/popup.js';
import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js'; import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js';
import '@awesome.me/webawesome/dist/components/icon/icon.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') @customElement('cover-grid')
export class CoverGrid extends LitElement { export class CoverGrid extends LitElement {
@@ -143,6 +145,20 @@ export class CoverGrid extends LitElement {
.context-menu-panel wa-dropdown-item:hover { .context-menu-panel wa-dropdown-item:hover {
background-color: rgba(255, 255, 255, 0.1); 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() @state()
@@ -157,9 +173,18 @@ export class CoverGrid extends LitElement {
@state() @state()
private contextMenuAlbum: library.Album | null = null; private contextMenuAlbum: library.Album | null = null;
@state()
private playlistSubmenuOpen = false;
@state()
private playlistFilePaths: string[] = [];
@query('#context-menu') @query('#context-menu')
private contextMenuPopup!: HTMLElement; private contextMenuPopup!: HTMLElement;
@query('#playlist-submenu')
private playlistSubmenuPopup!: HTMLElement;
override connectedCallback() { override connectedCallback() {
super.connectedCallback(); super.connectedCallback();
this.loadAlbums(); this.loadAlbums();
@@ -253,8 +278,10 @@ export class CoverGrid extends LitElement {
private closeContextMenu() { private closeContextMenu() {
if (!this.contextMenuOpen) return; if (!this.contextMenuOpen) return;
this.closePlaylistSubmenu();
this.contextMenuOpen = false; this.contextMenuOpen = false;
this.contextMenuAlbum = null; this.contextMenuAlbum = null;
this.playlistFilePaths = [];
const popup = this.contextMenuPopup; 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 => { private renderAlbumCard = (album: library.Album): unknown => {
return html` return html`
<div <div
@@ -376,10 +449,38 @@ export class CoverGrid extends LitElement {
<wa-icon slot="icon" name="forward-step"></wa-icon> <wa-icon slot="icon" name="forward-step"></wa-icon>
Play Next Play Next
</wa-dropdown-item> </wa-dropdown-item>
<wa-dropdown-item
class="submenu-item"
@mouseenter=${() => this.showPlaylistSubmenu()}
@click=${(e: Event) => {
e.stopPropagation();
void this.showPlaylistSubmenu();
}}
>
<wa-icon slot="icon" name="plus"></wa-icon>
Add to Playlist
<span class="submenu-arrow">&#9654;</span>
</wa-dropdown-item>
</div> </div>
` `
: nothing} : nothing}
</wa-popup> </wa-popup>
<wa-popup
id="playlist-submenu"
placement="right-start"
.active=${this.playlistSubmenuOpen}
>
${this.playlistSubmenuOpen
? html`
<playlist-picker
.filePaths=${this.playlistFilePaths}
@playlist-action-complete=${this.onPlaylistActionComplete}
@click=${(e: Event) => e.stopPropagation()}
></playlist-picker>
`
: nothing}
</wa-popup>
`; `;
} }
} }
@@ -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<HTMLInputElement>(
'.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`
<div class="picker-panel">
${this.playlists.length > 0
? html`
${this.playlists.map(
(p) => html`
<wa-dropdown-item
@click=${() =>
this.handleSelectPlaylist(p.ID)}
>
${p.Name}
</wa-dropdown-item>
`,
)}
<div class="separator"></div>
`
: nothing}
<wa-dropdown-item @click=${this.handleShowCreate}>
<wa-icon slot="icon" name="plus"></wa-icon>
New Playlist
</wa-dropdown-item>
</div>
`;
}
private renderCreateForm() {
const canCreate = this.newPlaylistName.trim().length > 0;
return html`
<div class="picker-panel">
<div class="create-form">
<input
type="text"
placeholder="Playlist name"
.value=${this.newPlaylistName}
@input=${this.handleInputChange}
@keydown=${this.handleInputKeydown}
@click=${(e: Event) => e.stopPropagation()}
/>
<div class="button-row">
<button @click=${this.handleCancelCreate}>
Cancel
</button>
<button
class="primary"
?disabled=${!canCreate || this.loading}
@click=${this.handleCreatePlaylist}
>
Create
</button>
</div>
</div>
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'playlist-picker': PlaylistPicker;
}
}
@@ -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<HTMLInputElement>(
'.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`
<div class="header">
<h2>Playlists</h2>
<button
class="new-playlist-button"
@click=${this.handleNewPlaylistClick}
>
<wa-icon name="plus"></wa-icon>
New Playlist
</button>
</div>
${this.creating ? this.renderCreateForm() : ''}
${this.loading
? html`<div class="loading">Loading playlists...</div>`
: this.renderPlaylistList()}
`;
}
private renderCreateForm() {
const canCreate = this.newPlaylistName.trim().length > 0;
return html`
<div class="create-form">
<input
type="text"
placeholder="Playlist name"
.value=${this.newPlaylistName}
@input=${this.handleInputChange}
@keydown=${this.handleInputKeydown}
/>
<button @click=${this.handleCancelCreate}>Cancel</button>
<button
class="primary"
?disabled=${!canCreate}
@click=${this.handleCreatePlaylist}
>
Create
</button>
</div>
`;
}
private renderPlaylistList() {
if (this.playlists.length === 0) {
return html`
<div class="empty-state">
<wa-icon name="list"></wa-icon>
<p>No playlists yet</p>
<p style="font-size: 12px;">
Create a playlist to get started.
</p>
</div>
`;
}
return html`
<ul class="playlist-list">
${this.playlists.map(
(p) => html`
<li class="playlist-item">
<wa-icon
class="playlist-icon"
name="list"
></wa-icon>
<span class="playlist-name">${p.Name}</span>
</li>
`,
)}
</ul>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'playlist-view': PlaylistView;
}
}
@@ -1,7 +1,10 @@
import { LitElement, html, css, unsafeCSS } from 'lit'; import { LitElement, html, css, nothing, unsafeCSS } from 'lit';
import { customElement, property, state } from 'lit/decorators.js'; import { customElement, property, state, query } from 'lit/decorators.js';
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 { QueueController } from '@store/controllers/queue-controller'; 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 MIN_WIDTH = 200;
const MAX_WIDTH = 500; const MAX_WIDTH = 500;
@@ -17,6 +20,22 @@ export class QueuePanel extends LitElement {
@state() @state()
private isDragging = false; 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; private panelWidth = DEFAULT_WIDTH;
static override styles = css` static override styles = css`
@@ -75,7 +94,7 @@ export class QueuePanel extends LitElement {
font-weight: 600; font-weight: 600;
} }
.close-button { .save-playlist-button {
background: none; background: none;
border: none; border: none;
color: inherit; color: inherit;
@@ -85,10 +104,19 @@ export class QueuePanel extends LitElement {
align-items: center; align-items: center;
} }
.close-button:hover { .save-playlist-button:hover {
color: #ffd43b; color: #ffd43b;
} }
.save-playlist-button:disabled {
color: #555;
cursor: not-allowed;
}
#save-playlist-popup {
z-index: 210;
}
.track-list { .track-list {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
@@ -193,24 +221,56 @@ export class QueuePanel extends LitElement {
this.style.setProperty('--queue-width', `${this.panelWidth}px`); this.style.setProperty('--queue-width', `${this.panelWidth}px`);
document.addEventListener('mousemove', this.handleMouseMove); document.addEventListener('mousemove', this.handleMouseMove);
document.addEventListener('mouseup', this.handleMouseUp); document.addEventListener('mouseup', this.handleMouseUp);
document.addEventListener('click', this.closePickerHandler);
} }
override disconnectedCallback() { override disconnectedCallback() {
super.disconnectedCallback(); super.disconnectedCallback();
document.removeEventListener('mousemove', this.handleMouseMove); document.removeEventListener('mousemove', this.handleMouseMove);
document.removeEventListener('mouseup', this.handleMouseUp); document.removeEventListener('mouseup', this.handleMouseUp);
document.removeEventListener('click', this.closePickerHandler);
} }
private handleClose() { private async handleSaveAsPlaylist() {
this.open = false; if (this.queue.tracks.length === 0) return;
this.dispatchEvent(
new CustomEvent('queue-panel-close', { this.playlistPickerOpen = !this.playlistPickerOpen;
bubbles: true,
composed: true, 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) { private handleRemoveTrack(e: Event, position: number) {
e.stopPropagation(); e.stopPropagation();
this.queue.removeFromQueue(position); this.queue.removeFromQueue(position);
@@ -275,11 +335,32 @@ export class QueuePanel extends LitElement {
></div> ></div>
<div class="header"> <div class="header">
<h3>Queue</h3> <h3>Queue</h3>
<button class="close-button" @click=${this.handleClose}> <button
<wa-icon name="xmark"></wa-icon> class="save-playlist-button"
@click=${this.handleSaveAsPlaylist}
?disabled=${tracks.length === 0}
title="Save queue as playlist"
>
<wa-icon name="plus"></wa-icon>
</button> </button>
</div> </div>
<wa-popup
id="save-playlist-popup"
placement="bottom-end"
.active=${this.playlistPickerOpen}
>
${this.playlistPickerOpen
? html`
<playlist-picker
.filePaths=${tracks.map((t) => t.filePath)}
@playlist-action-complete=${this.onPlaylistActionComplete}
@click=${(e: Event) => e.stopPropagation()}
></playlist-picker>
`
: nothing}
</wa-popup>
${tracks.length === 0 ${tracks.length === 0
? html` ? html`
<div class="empty-state"> <div class="empty-state">
@@ -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/popup/popup.js';
import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js'; import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js';
import '@awesome.me/webawesome/dist/components/icon/icon.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') @customElement('track-list')
export class TrackList extends LitElement { export class TrackList extends LitElement {
@@ -26,9 +28,15 @@ export class TrackList extends LitElement {
@state() @state()
private contextMenuTrack: library.Track | null = null; private contextMenuTrack: library.Track | null = null;
@state()
private playlistSubmenuOpen = false;
@query('#context-menu') @query('#context-menu')
private contextMenuPopup!: HTMLElement; private contextMenuPopup!: HTMLElement;
@query('#playlist-submenu')
private playlistSubmenuPopup!: HTMLElement;
private closeHandler = () => this.closeContextMenu(); private closeHandler = () => this.closeContextMenu();
static override styles = css` static override styles = css`
@@ -128,6 +136,20 @@ export class TrackList extends LitElement {
.context-menu-panel wa-dropdown-item:hover { .context-menu-panel wa-dropdown-item:hover {
background-color: rgba(255, 255, 255, 0.1); 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() { override connectedCallback() {
@@ -214,6 +236,7 @@ export class TrackList extends LitElement {
private closeContextMenu() { private closeContextMenu() {
if (!this.contextMenuOpen) return; if (!this.contextMenuOpen) return;
this.closePlaylistSubmenu();
this.contextMenuOpen = false; this.contextMenuOpen = false;
this.contextMenuTrack = null; 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 { private isActiveTrack(track: library.Track): boolean {
const currentTrack = this.player.currentTrack; const currentTrack = this.player.currentTrack;
@@ -298,10 +359,38 @@ export class TrackList extends LitElement {
<wa-icon slot="icon" name="forward-step"></wa-icon> <wa-icon slot="icon" name="forward-step"></wa-icon>
Play Next Play Next
</wa-dropdown-item> </wa-dropdown-item>
<wa-dropdown-item
class="submenu-item"
@mouseenter=${() => this.showPlaylistSubmenu()}
@click=${(e: Event) => {
e.stopPropagation();
void this.showPlaylistSubmenu();
}}
>
<wa-icon slot="icon" name="plus"></wa-icon>
Add to Playlist
<span class="submenu-arrow">&#9654;</span>
</wa-dropdown-item>
</div> </div>
` `
: nothing} : nothing}
</wa-popup> </wa-popup>
<wa-popup
id="playlist-submenu"
placement="right-start"
.active=${this.playlistSubmenuOpen}
>
${this.playlistSubmenuOpen && this.contextMenuTrack
? html`
<playlist-picker
.filePaths=${[this.contextMenuTrack.FilePath]}
@playlist-action-complete=${this.onPlaylistActionComplete}
@click=${(e: Event) => e.stopPropagation()}
></playlist-picker>
`
: nothing}
</wa-popup>
`; `;
} }
} }
+19
View File
@@ -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"];
}
}
}
+14
View File
@@ -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<string>):Promise<void>;
export function CreatePlaylist(arg1:string):Promise<playlist.Summary>;
export function CreatePlaylistWithTracks(arg1:string,arg2:Array<string>):Promise<playlist.Summary>;
export function GetAllPlaylists():Promise<Array<playlist.Summary>>;
export function SetContext(arg1:context.Context):Promise<void>;
+23
View File
@@ -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);
}
View File
Vendored Regular → Executable
View File
View File
+6 -2
View File
@@ -85,6 +85,8 @@ require (
github.com/ckaznocha/intrange v0.3.1 // indirect github.com/ckaznocha/intrange v0.3.1 // indirect
github.com/cli/browser v1.3.0 // indirect github.com/cli/browser v1.3.0 // indirect
github.com/clipperhouse/uax29/v2 v2.2.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/creack/pty v1.1.24 // indirect
github.com/cubicdaiya/gonp v1.0.4 // indirect github.com/cubicdaiya/gonp v1.0.4 // indirect
github.com/curioswitch/go-reassign v0.3.0 // 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/dlclark/regexp2 v1.11.5 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/oto/v3 v3.3.3 // 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/evilmartians/lefthook v1.13.6 // indirect
github.com/fatih/color v1.18.0 // indirect github.com/fatih/color v1.18.0 // indirect
github.com/fatih/structtag v1.2.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/crypto v0.48.0 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // 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/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/mod v0.33.0 // indirect
golang.org/x/net v0.50.0 // indirect golang.org/x/net v0.50.0 // indirect
golang.org/x/sys v0.41.0 // indirect golang.org/x/sys v0.41.0 // indirect
@@ -377,6 +380,7 @@ tool (
github.com/sqlc-dev/sqlc/cmd/sqlc github.com/sqlc-dev/sqlc/cmd/sqlc
github.com/wailsapp/wails/v2/cmd/wails github.com/wailsapp/wails/v2/cmd/wails
golang.org/x/vuln/cmd/govulncheck golang.org/x/vuln/cmd/govulncheck
yellowjacket
) )
// replace github.com/TheCodeOfCaleb/beep/v2 => /mnt/vault/dev/golang/beep/ // replace github.com/TheCodeOfCaleb/beep/v2 => /mnt/vault/dev/golang/beep/
+115 -14
View File
@@ -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/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 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo=
github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk= 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 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= 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/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 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= 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/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 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 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 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= 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 h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU=
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok= 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= 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.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/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/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 h1:kebVCZme/BrCTqonh/J+VYCl1+Of5C18bvyn3DRPl5M=
github.com/kaptinlin/go-i18n v0.2.2/go.mod h1:MiwkeHryBopAhC/M3zEwIM/2IN8TvTqJQswPw6kceqM= 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= 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-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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-tty v0.0.7 h1:KJ486B6qI8+wBO7kQxYgmmEFDaFEE96JMBQ7h400N8Q= 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/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 h1:XrG0xOeHs+4FQ8gJR97zDz5uOFMW7OwFWiFVzqopKgY=
github.com/samber/lo v1.50.0/go.mod h1:RjZyNk6WSnUFRKK6EyOhsRJMqft3G+pg7dCWHQCWvsc= 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 h1:Ea18xuIRQXLAUidVDox3AbwfUhD0/1IvohyTutOIFoc=
github.com/schollz/progressbar/v3 v3.19.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec= 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= 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-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-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.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.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= 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 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= 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= 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.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 h1:w13vZbU4o5rKOFFR8y7M+c4A5jXDC0uXTdHYRP8X2DQ=
golang.org/x/image v0.12.0/go.mod h1:Lu90jvHG7GfemOIcldsh9A2hS01ocl6oNO7ype5mEnk= 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-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-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 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.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.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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= 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-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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/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.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.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.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= 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-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-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/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-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.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.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 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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= 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.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 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-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-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-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.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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= 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.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.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 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.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.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.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.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 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-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-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 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.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.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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= 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-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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=