From c7b21aeb01c188706882f4af82e118b56257a252 Mon Sep 17 00:00:00 2001 From: Dirk van Zon Date: Sun, 3 May 2026 12:36:57 +0200 Subject: [PATCH 01/13] Added project page --- src/auto-imports.d.ts | 2 +- src/components.d.ts | 2 +- src/components/AlbumList.vue | 15 +-- src/components/CassetteActionsBar.vue | 129 ++++++++++++++++++++++---- src/components/PlaylistList.vue | 15 +-- src/components/SearchAlbumsTab.vue | 37 -------- src/components/SearchPlaylistsTab.vue | 38 -------- src/components/cassette/Cassette.vue | 9 -- src/layouts/cassetteControls.vue | 3 +- src/pages/project.vue | 39 ++++++++ src/parsers/episodeDtoParser.ts | 5 +- src/parsers/trackDtoParser.ts | 10 +- src/stores/album.ts | 17 ++-- src/stores/cassette.ts | 10 +- src/stores/layout.ts | 9 +- src/stores/playlists.ts | 18 ++-- src/stores/project.ts | 24 +++++ src/stores/tracks.ts | 8 ++ src/typed-router.d.ts | 15 ++- src/types/tapeify/models.ts | 19 ++-- 20 files changed, 261 insertions(+), 163 deletions(-) create mode 100644 src/pages/project.vue create mode 100644 src/stores/project.ts diff --git a/src/auto-imports.d.ts b/src/auto-imports.d.ts index 00281ab..417365e 100644 --- a/src/auto-imports.d.ts +++ b/src/auto-imports.d.ts @@ -1,4 +1,4 @@ - +/* eslint-disable */ /* prettier-ignore */ // @ts-nocheck // noinspection JSUnusedGlobalSymbols diff --git a/src/components.d.ts b/src/components.d.ts index b0e8fcb..4c94616 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -1,4 +1,4 @@ - +/* eslint-disable */ // @ts-nocheck // biome-ignore lint: disable // oxlint-disable diff --git a/src/components/AlbumList.vue b/src/components/AlbumList.vue index d39445b..4bde27c 100644 --- a/src/components/AlbumList.vue +++ b/src/components/AlbumList.vue @@ -1,8 +1,12 @@ diff --git a/src/components/PlaylistList.vue b/src/components/PlaylistList.vue index 5c1e198..7f5e2ee 100644 --- a/src/components/PlaylistList.vue +++ b/src/components/PlaylistList.vue @@ -1,8 +1,12 @@ diff --git a/src/components/SearchPlaylistsTab.vue b/src/components/SearchPlaylistsTab.vue index a8841ad..03670a9 100644 --- a/src/components/SearchPlaylistsTab.vue +++ b/src/components/SearchPlaylistsTab.vue @@ -1,5 +1,4 @@ diff --git a/src/components/cassette/Cassette.vue b/src/components/cassette/Cassette.vue index f32ba4e..9127c17 100644 --- a/src/components/cassette/Cassette.vue +++ b/src/components/cassette/Cassette.vue @@ -21,11 +21,6 @@ const topAlert = computed(() => { return cassetteStore.alertForCassette(props.cassetteId) }) -function addCassette() { - cassetteStore.addCassette() - layoutStore.calculateLayout() -} - function removeCassette() { cassetteStore.removeCassette(props.cassetteId) anchorsStore.removeAnchoresByCassetteId(props.cassetteId) @@ -88,11 +83,7 @@ const name = computed({ - Cassette - - - Sides diff --git a/src/layouts/cassetteControls.vue b/src/layouts/cassetteControls.vue index a27deb6..a6d311b 100644 --- a/src/layouts/cassetteControls.vue +++ b/src/layouts/cassetteControls.vue @@ -1,5 +1,4 @@ diff --git a/src/pages/project.vue b/src/pages/project.vue new file mode 100644 index 0000000..216ddcc --- /dev/null +++ b/src/pages/project.vue @@ -0,0 +1,39 @@ + +{ + "meta": { + "layout": "cassetteControls" + } +} + + + + + diff --git a/src/parsers/episodeDtoParser.ts b/src/parsers/episodeDtoParser.ts index 43274ed..ad534dd 100644 --- a/src/parsers/episodeDtoParser.ts +++ b/src/parsers/episodeDtoParser.ts @@ -3,7 +3,7 @@ import type { Track } from "@/types/tapeify/models"; import { GetSmallestImage } from "@/utils/images/imageUtils"; import { v4 as uuidv4 } from 'uuid'; -export function ParsePlaylistEpisodeDTO(episodeDTO: EpisodeDTO): Track { +export function ParsePlaylistEpisodeDTO(episodeDTO: EpisodeDTO, playlistId: string): Track { return { name: episodeDTO.name, spotifyId: episodeDTO.id, @@ -12,6 +12,7 @@ export function ParsePlaylistEpisodeDTO(episodeDTO: EpisodeDTO): Track { explicit: episodeDTO.explicit, durationMs: episodeDTO.duration_ms, artists: episodeDTO.album.artists.map(a => a.type), - image: GetSmallestImage(episodeDTO.album.images) + image: GetSmallestImage(episodeDTO.album.images), + origin: playlistId } } \ No newline at end of file diff --git a/src/parsers/trackDtoParser.ts b/src/parsers/trackDtoParser.ts index 63ec546..12830e4 100644 --- a/src/parsers/trackDtoParser.ts +++ b/src/parsers/trackDtoParser.ts @@ -3,7 +3,7 @@ import type { Track } from "@/types/tapeify/models"; import { GetSmallestImage } from "@/utils/images/imageUtils"; import { v4 as uuidv4 } from 'uuid'; -export function ParsePlaylistTrackDTO(track: PlaylistTrackDTO): Track { +export function ParsePlaylistTrackDTO(track: PlaylistTrackDTO, playlistId: string): Track { return { name: track.name, spotifyId: track.id, @@ -12,11 +12,12 @@ export function ParsePlaylistTrackDTO(track: PlaylistTrackDTO): Track { explicit: track.explicit, durationMs: track.duration_ms, artists: track.artists.map(a => a.name), - image: GetSmallestImage(track.album.images) + image: GetSmallestImage(track.album.images), + origin: playlistId } } -export function ParseAlbumTrackDTO(track: TrackDTO, albumImage: URL | undefined): Track { +export function ParseAlbumTrackDTO(track: TrackDTO, albumImage: URL | undefined, albumId: string): Track { return { name: track.name, spotifyId: track.id, @@ -25,6 +26,7 @@ export function ParseAlbumTrackDTO(track: TrackDTO, albumImage: URL | undefined) explicit: track.explicit, durationMs: track.duration_ms, artists: track.artists.map(a => a.name), - image: albumImage + image: albumImage, + origin: albumId } } diff --git a/src/stores/album.ts b/src/stores/album.ts index 2f7414c..33b286e 100644 --- a/src/stores/album.ts +++ b/src/stores/album.ts @@ -8,12 +8,14 @@ import { apiClient } from '@/api/clients' import { useProfileStore } from './profile' import type { AlbumDTO } from '@/types/spotify/dto' import { ParseAlbumDTO } from '@/parsers/albumDtoParser' +import { useProjectStore } from './project' export const useAlbumsStore = defineStore('albums', { actions: { async FetchAlbumTracks(albumId: string) { const tracksStore = useTracksStore() const cassetteStore = useCassettesStore() + const projectStore = useProjectStore() const response = await apiClient.get('/albums/' + albumId) const album = response.data @@ -34,21 +36,20 @@ export const useAlbumsStore = defineStore('albums', { }) for (const item of tracksResponse.data.items) { - tracksStore.AddTrack(ParseAlbumTrackDTO(item, imageUrl)) + tracksStore.AddTrack(ParseAlbumTrackDTO(item, imageUrl, albumId)) } offset += limit } - cassetteStore.updateName('default', album.name) - cassetteStore.updateMetadata({ + projectStore.addOrigin({ + name: album.name, + type: 'album', owner_display_name: album.artists[0].name, - owner_url: album.artists[0].external_urls.spotify, - description: '', - image_url: new URL(album.images[0].url), original_item_url: album.external_urls.spotify, - item_name: album.name, - }) + owner_url: album.artists[0].external_urls.spotify, + description: "" + }, album.id) }, async searchAlbums(query: string, limit: number = 10, offset: number = 0) { const profileStore = useProfileStore() diff --git a/src/stores/cassette.ts b/src/stores/cassette.ts index 819c06c..b98bf86 100644 --- a/src/stores/cassette.ts +++ b/src/stores/cassette.ts @@ -2,7 +2,6 @@ import { defineStore } from 'pinia' import type { Cassette, CassetteAlert, - CassetteMetadata, TapeSideLayout, } from '@/types/tapeify/models' import { v4 as uuidv4 } from 'uuid' @@ -14,9 +13,8 @@ import { useLayoutStore } from './layout' export const useCassettesStore = defineStore('cassettes', { state: () => ({ possibleLengthsMin: [30, 45, 60, 90, 120], - metadata: {} as CassetteMetadata, cassettes: [ - { id: 'default', name: 'My First Cassette', capacityMs: 90 * 60000, sidesCount: 2 }, + { id: 'default', name: 'Cassette', capacityMs: 90 * 60000, sidesCount: 2 }, ] as Cassette[], alerts: {} as Record, }), @@ -34,7 +32,7 @@ export const useCassettesStore = defineStore('cassettes', { addCassette() { this.cassettes.push({ id: uuidv4(), - name: `${this.metadata.item_name} ${this.cassettes.length + 1}`, + name: `Cassette ${this.cassettes.length + 1}`, capacityMs: 90 * 60000, sidesCount: 2 }) @@ -68,10 +66,6 @@ export const useCassettesStore = defineStore('cassettes', { cassette.sidesCount = newSidesCount } }, - - updateMetadata(newMetadata: CassetteMetadata) { - this.metadata = newMetadata - }, initAlerts() { const layoutStore = useLayoutStore() diff --git a/src/stores/layout.ts b/src/stores/layout.ts index 0911818..049676d 100644 --- a/src/stores/layout.ts +++ b/src/stores/layout.ts @@ -5,6 +5,7 @@ import { useCassettesStore } from "./cassette"; import { useTracksStore } from "./tracks"; import { useAnchorsStore } from "./anchor"; import { TapeSide } from "@/sorting/tapeSideLayout"; +import { useProjectStore } from "./project"; export const useLayoutStore = defineStore('layout', { state: () => ({ @@ -63,6 +64,7 @@ export const useLayoutStore = defineStore('layout', { const cassetteStore = useCassettesStore() const trackStore = useTracksStore() const anchorsStore = useAnchorsStore() + const projectStore = useProjectStore() this.orderedTracks = [] this.trackLocations = {} @@ -79,10 +81,11 @@ export const useLayoutStore = defineStore('layout', { const trackSorter = trackSorterRegistry.create(this.selectedSortType, sides) - const tracks = trackStore.availableTracks + const availableTracks = trackStore.availableTracks + const tracksInSelectedOrigins = availableTracks.filter(track => projectStore.selectedOrigins.includes(track.origin)) - const anchored_tracks = trackSorter.prepackAnchoredTracks(tracks, anchorsStore.anchors) - const unanchored_tracks = tracks.filter(t => !anchored_tracks.includes(t)) + const anchored_tracks = trackSorter.prepackAnchoredTracks(tracksInSelectedOrigins, anchorsStore.anchors) + const unanchored_tracks = tracksInSelectedOrigins.filter(t => !anchored_tracks.includes(t)) trackSorter.sortTracks(sides, unanchored_tracks) this._calculate_cassette_layout(sides) diff --git a/src/stores/playlists.ts b/src/stores/playlists.ts index 9d07013..9a4abc8 100644 --- a/src/stores/playlists.ts +++ b/src/stores/playlists.ts @@ -9,7 +9,7 @@ import { apiClient } from '@/api/clients' import { ParsePlaylistDTO } from '@/parsers/playlistDtoParser' import type { Playlist, PlaylistSearchResult, Track } from '@/types/tapeify/models' import { useProfileStore } from './profile' -import { GetSmallestImage } from '@/utils/images/imageUtils' +import { useProjectStore } from './project' export const usePlaylistsStore = defineStore('playlists', { actions: { @@ -56,6 +56,7 @@ export const usePlaylistsStore = defineStore('playlists', { async FetchPlaylistTracks(playlistId: string) { const cassetteStore = useCassettesStore() const tracksStore = useTracksStore() + const projectStore = useProjectStore() const playlistResponse = await apiClient.get('/playlists/' + playlistId) const playlist = playlistResponse.data @@ -77,25 +78,24 @@ export const usePlaylistsStore = defineStore('playlists', { for (const item of tracks.items) { const track = item.track if (track.type === 'track') { - tracksStore.AddTrack(ParsePlaylistTrackDTO(track as PlaylistTrackDTO)) + tracksStore.AddTrack(ParsePlaylistTrackDTO(track as PlaylistTrackDTO, playlistId)) } if (track.type === 'episode') { - tracksStore.AddTrack(ParsePlaylistEpisodeDTO(track as EpisodeDTO)) + tracksStore.AddTrack(ParsePlaylistEpisodeDTO(track as EpisodeDTO, playlistId)) } } offset += limit } - cassetteStore.updateName('default', playlist.name) - cassetteStore.updateMetadata({ + projectStore.addOrigin({ + name: playlist.name, + type: 'playlist', owner_display_name: playlist.owner.display_name, + original_item_url: playlist.external_urls.spotify, owner_url: playlist.owner.external_urls.spotify, description: playlist.description, - image_url: new URL(playlist.images[0].url), - original_item_url: playlist.external_urls.spotify, - item_name: playlist.name, - }) + }, playlist.id) }, async UploadNewPlaylist(name: string, description: string, isPublic: boolean): Promise { const profileStore = useProfileStore() diff --git a/src/stores/project.ts b/src/stores/project.ts new file mode 100644 index 0000000..0ace4bf --- /dev/null +++ b/src/stores/project.ts @@ -0,0 +1,24 @@ +import type { Origin } from "@/types/tapeify/models"; +import { defineStore } from "pinia"; + +export const useProjectStore = defineStore('project', { + state: () => ({ + origins: {} as Record, + selectedOrigins: [] as string[], + }), + getters: { + hasOrigins: (state) => Object.keys(state.origins).length > 0, + }, + actions: { + addOrigin(origin: Origin, id: string) { + this.origins[id] = origin; + if (!this.selectedOrigins.includes(id)) { + this.selectedOrigins.push(id); + } + }, + removeOrigin(id: string) { + delete this.origins[id]; + this.selectedOrigins = this.selectedOrigins.filter(originId => originId !== id); + } + } +}) \ No newline at end of file diff --git a/src/stores/tracks.ts b/src/stores/tracks.ts index 0c0943f..82b4dd8 100644 --- a/src/stores/tracks.ts +++ b/src/stores/tracks.ts @@ -42,6 +42,14 @@ export const useTracksStore = defineStore('tracks', { }, ClearSelectedTracks() { this.selectedTracks = [] + }, + RemoveTracksByOrigin(originId: string) { + const tracksToRemove = this._masterTrackList.filter(track => track.origin === originId) + const trackIdsToRemove = tracksToRemove.map(t => t.id) + + this._masterTrackList = this._masterTrackList.filter(track => track.origin !== originId) + this.unavailableTrackIds = this.unavailableTrackIds.filter(id => !trackIdsToRemove.includes(id)) + this.selectedTracks = this.selectedTracks.filter(id => !trackIdsToRemove.includes(id)) } } }) diff --git a/src/typed-router.d.ts b/src/typed-router.d.ts index 43541d4..c79862e 100644 --- a/src/typed-router.d.ts +++ b/src/typed-router.d.ts @@ -1,4 +1,4 @@ - +/* eslint-disable */ /* prettier-ignore */ // @ts-nocheck // noinspection ES6UnusedImports @@ -44,6 +44,13 @@ declare module 'vue-router/auto-routes' { { spotify_id: ParamValue }, | never >, + '/project': RouteRecordInfo< + '/project', + '/project', + Record, + Record, + | never + >, '/search': RouteRecordInfo< '/search', '/search', @@ -82,6 +89,12 @@ declare module 'vue-router/auto-routes' { views: | never } + 'src/pages/project.vue': { + routes: + | '/project' + views: + | never + } 'src/pages/search.vue': { routes: | '/search' diff --git a/src/types/tapeify/models.ts b/src/types/tapeify/models.ts index 24049e3..247e7a6 100644 --- a/src/types/tapeify/models.ts +++ b/src/types/tapeify/models.ts @@ -58,6 +58,7 @@ export interface Track { explicit: boolean durationMs: number artists: string[] + origin: string } export interface TrackLocation { @@ -77,15 +78,6 @@ export interface CassetteLayout { sides: Array } -export interface CassetteMetadata { - owner_display_name: string - item_name: string - original_item_url: string - owner_url: string - description: string - image_url?: URL -} - export interface CassetteAlert { message: string priority: number @@ -119,3 +111,12 @@ export type AlertRule = { payload?: TPayload ) => CassetteAlert['action'] } + +export interface Origin { + type: 'playlist' | 'album', + name: string + owner_display_name: string + original_item_url: string + owner_url: string + description: string +} From 4268403a4c33d85b0461752f86dd57b24baa0c4c Mon Sep 17 00:00:00 2001 From: Dirk van Zon Date: Mon, 4 May 2026 11:41:16 +0200 Subject: [PATCH 02/13] Fix reference error --- src/components/CassetteActionsBar.vue | 15 ++++++++++----- src/components/PlaylistList.vue | 2 +- src/components/SearchAlbumsTab.vue | 1 - src/pages/project.vue | 17 ++++++----------- src/stores/playlists.ts | 2 -- src/stores/tracks.ts | 3 ++- 6 files changed, 19 insertions(+), 21 deletions(-) diff --git a/src/components/CassetteActionsBar.vue b/src/components/CassetteActionsBar.vue index 9df0c53..1ca9a6f 100644 --- a/src/components/CassetteActionsBar.vue +++ b/src/components/CassetteActionsBar.vue @@ -1,4 +1,5 @@ diff --git a/src/stores/playlists.ts b/src/stores/playlists.ts index 9a4abc8..7805ec2 100644 --- a/src/stores/playlists.ts +++ b/src/stores/playlists.ts @@ -4,7 +4,6 @@ import type { CreatePlaylistResponse, GetPlaylistsResponse, GetPlaylistTracksRes import type { EpisodeDTO, PlaylistDTO, PlaylistTrackDTO } from '@/types/spotify/dto' import { ParsePlaylistTrackDTO } from '@/parsers/trackDtoParser' import { ParsePlaylistEpisodeDTO } from '@/parsers/episodeDtoParser' -import { useCassettesStore } from './cassette' import { apiClient } from '@/api/clients' import { ParsePlaylistDTO } from '@/parsers/playlistDtoParser' import type { Playlist, PlaylistSearchResult, Track } from '@/types/tapeify/models' @@ -54,7 +53,6 @@ export const usePlaylistsStore = defineStore('playlists', { }, async FetchPlaylistTracks(playlistId: string) { - const cassetteStore = useCassettesStore() const tracksStore = useTracksStore() const projectStore = useProjectStore() diff --git a/src/stores/tracks.ts b/src/stores/tracks.ts index 82b4dd8..37f7cd9 100644 --- a/src/stores/tracks.ts +++ b/src/stores/tracks.ts @@ -43,13 +43,14 @@ export const useTracksStore = defineStore('tracks', { ClearSelectedTracks() { this.selectedTracks = [] }, - RemoveTracksByOrigin(originId: string) { + RemoveTracksByOrigin(originId: string): string[] { const tracksToRemove = this._masterTrackList.filter(track => track.origin === originId) const trackIdsToRemove = tracksToRemove.map(t => t.id) this._masterTrackList = this._masterTrackList.filter(track => track.origin !== originId) this.unavailableTrackIds = this.unavailableTrackIds.filter(id => !trackIdsToRemove.includes(id)) this.selectedTracks = this.selectedTracks.filter(id => !trackIdsToRemove.includes(id)) + return trackIdsToRemove } } }) From 146c907aa4a3257a701bfab2f9c30bea36c6f5b6 Mon Sep 17 00:00:00 2001 From: Dirk van Zon Date: Mon, 4 May 2026 11:45:00 +0200 Subject: [PATCH 03/13] Added dialogs subfolder --- src/components.d.ts | 3 +- src/components/CassetteActionsBar.vue | 38 ++----------------- src/components/dialogs/AddSourceDialog.vue | 36 ++++++++++++++++++ .../{ => dialogs}/UploadCassetteDialog.vue | 0 src/main.ts | 4 +- 5 files changed, 45 insertions(+), 36 deletions(-) create mode 100644 src/components/dialogs/AddSourceDialog.vue rename src/components/{ => dialogs}/UploadCassetteDialog.vue (100%) diff --git a/src/components.d.ts b/src/components.d.ts index 4c94616..0386c36 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -11,6 +11,7 @@ export {} /* prettier-ignore */ declare module 'vue' { export interface GlobalComponents { + AddSourceDialog: typeof import('./components/dialogs/AddSourceDialog.vue')['default'] AlbumList: typeof import('./components/AlbumList.vue')['default'] AppFooter: typeof import('./components/AppFooter.vue')['default'] Cassette: typeof import('./components/cassette/Cassette.vue')['default'] @@ -25,7 +26,7 @@ declare module 'vue' { SearchPlaylistsTab: typeof import('./components/SearchPlaylistsTab.vue')['default'] UnavailableCassetteItem: typeof import('./components/UnavailableCassetteItem.vue')['default'] UnavailableTracksList: typeof import('./components/UnavailableTracksList.vue')['default'] - UploadCassetteDialog: typeof import('./components/UploadCassetteDialog.vue')['default'] + UploadCassetteDialog: typeof import('./components/dialogs/UploadCassetteDialog.vue')['default'] UserPlaylistsTab: typeof import('./components/UserPlaylistsTab.vue')['default'] } } diff --git a/src/components/CassetteActionsBar.vue b/src/components/CassetteActionsBar.vue index 1ca9a6f..38145fa 100644 --- a/src/components/CassetteActionsBar.vue +++ b/src/components/CassetteActionsBar.vue @@ -30,8 +30,6 @@ function removeOrigin(originId: string) { layoutStore.calculateLayout() } -const selectedTab = ref('user_playlists') - const origins = computed(() => { return Object.entries(projectStore.origins).map(([id, origin]) => ({ id, @@ -48,36 +46,7 @@ const origins = computed(() => {