6363
6464 <!-- conditionally visible: only when token is present -->
6565 <template v-if =" hasToken " >
66+ <k-section >
67+ <k-box icon =" search" theme =" white" style =" margin-bottom : var (--spacing-1 )" >
68+ <k-search-input
69+ :value =" searchQuery"
70+ :disabled =" !hasToken"
71+ :placeholder =" 'Search Apple Music...'"
72+ @input =" onSearchInput"
73+ @submit =" performSearch"
74+ />
75+ </k-box >
76+
77+ <k-box v-if =" searching" icon =" loader" style =" --width : 100% " >Searching...</k-box >
78+
79+ <k-items
80+ v-else-if =" searchResults.length"
81+ :items =" searchResultItems"
82+ layout =" list"
83+ size =" small"
84+ />
85+
86+ <k-box v-else-if =" searchQuery && !searching" theme =" none" >No matches found</k-box >
87+ </k-section >
88+
6689 <k-section label =" Recently Played Tracks" >
6790 <k-button slot =" options" size =" xs" variant =" filled" icon =" refresh" @click =" fetchRecent()" />
6891
6992 <k-box v-if =" error" theme =" negative" >{{ error }}</k-box >
70- <k-box v-else-if =" loading" icon =" loader" >Loading… </k-box >
93+ <k-box v-else-if =" loading" icon =" loader" >Loading... </k-box >
7194
7295 <div v-else >
7396 <k-collection
96119</template >
97120
98121<script >
122+ import { makeTrackOptions } from ' ../trackOptions' ;
123+
99124export default {
100125 name: ' Apple Music' ,
101126 props: {
@@ -116,7 +141,14 @@ export default {
116141 limit: this .songsLimit ,
117142 offset: 0 ,
118143 language: ' en-US' ,
119- storefrontInfo: null
144+ storefrontInfo: null ,
145+ // search
146+ searchQuery: ' ' ,
147+ searchResults: [],
148+ searching: false ,
149+ searchError: null ,
150+ searchLimit: 10 ,
151+ _searchTimer: null
120152 };
121153 },
122154
@@ -138,6 +170,12 @@ export default {
138170 this .error = null ;
139171 this .loading = false ;
140172 this .storefrontInfo = null ;
173+
174+ // clear search ui too
175+ this .searchQuery = ' ' ;
176+ this .searchResults = [];
177+ this .searching = false ;
178+ this .searchError = null ;
141179 }
142180 }
143181 },
@@ -148,9 +186,44 @@ export default {
148186 ? ' Apple Music'
149187 : ' Authorize Apple Music'
150188 },
189+
190+ // map your searchResults into k-items
191+ searchResultItems () {
192+ const rawSf = (this .storefrontInfo ? .id || this .storefront || ' us' )
193+ .toString ()
194+ .toLowerCase ();
195+ const storefront = rawSf === ' auto' ? ' us' : rawSf;
196+
197+ return (this .searchResults || []).map ((r ) => {
198+ const base = {
199+ id: r .id ,
200+ text: r .text ,
201+ info: r .info ,
202+ icon: ' music' ,
203+ ... (r .image
204+ ? { image: { src: r .image , ratio: ' 1/1' , cover: true , back: ' pattern' } }
205+ : {}),
206+ ... (r .link ? { link: r .link } : {})
207+ };
208+
209+ const appleMusicUrl = r? .id
210+ ? ` https://music.apple.com/${ storefront} /song/${ encodeURIComponent (r .id )} `
211+ : null ;
212+
213+ const opts = makeTrackOptions ({
214+ url: appleMusicUrl,
215+ onCopy : (text , msg = ' Link copied to clipboard' ) => this .copyToClipboard (text, msg),
216+ onEmbed : (url ) => this .buildEmbedCode (url),
217+ onError : (msg ) => this .notify (' error' , msg)
218+ });
219+ if (opts .length ) base .options = opts;
220+ return base;
221+ });
222+ },
223+
151224 collectionItems () {
152225 return (this .items || []).map ((item ) => {
153- const appleMusicUrl = this .trackUrl (item); // external Apple Music URL (may be null)
226+ const appleMusicUrl = this .trackUrl (item); // external apple music url if present
154227 const isInline = typeof item .id === ' string' && item .id .startsWith (' i.' );
155228
156229 const base = {
@@ -169,37 +242,15 @@ export default {
169242 ... (isInline ? {} : { link: ' applemusic/song/' + item .id })
170243 };
171244
172- // only add options if link is present
245+ // only add options if appleMusicUrl is present
173246 if (appleMusicUrl) {
174- base .options = [
175- {
176- icon: ' headphones' ,
177- text: ' Listen' ,
178- click : () => {
179- if (link) {
180- window .open (link, ' _blank' );
181- }
182- }
183- },
184- ' -' ,
185- {
186- icon: ' url' ,
187- text: ' Copy Link' ,
188- click : () => this .copyToClipboard (link, ' Link copied to clipboard' )
189- },
190- {
191- icon: ' code' ,
192- text: ' Embed Code' ,
193- click : () => {
194- const iframe = this .buildEmbedCode (link);
195- if (! iframe) {
196- this .notify (' error' , ' Could not create embed code for this link' );
197- return ;
198- }
199- this .copyToClipboard (iframe, ' Embed copied to clipboard' )
200- }
201- }
202- ];
247+ const opts = makeTrackOptions ({
248+ url: appleMusicUrl, // external apple music url
249+ onCopy : (text , msg = ' Link copied to clipboard' ) => this .copyToClipboard (text, msg),
250+ onEmbed : (url ) => this .buildEmbedCode (url),
251+ onError : (msg ) => this .notify (' error' , msg)
252+ });
253+ if (opts .length ) base .options = opts;
203254 }
204255
205256 return base;
@@ -279,6 +330,52 @@ export default {
279330 },
280331
281332 methods: {
333+ // search pple music catalog
334+ onSearchInput (q ) {
335+ this .searchQuery = q || ' ' ;
336+ clearTimeout (this ._searchTimer );
337+ if (! this .searchQuery .trim ()) {
338+ this .searchResults = [];
339+ this .searchError = null ;
340+ return ;
341+ }
342+
343+ // set a reasonable timeout
344+ this ._searchTimer = setTimeout (() => this .performSearch (this .searchQuery ), 400 );
345+ },
346+
347+ async performSearch (q ) {
348+ const term = (typeof q === ' string' ? q : this .searchQuery ).trim ();
349+ if (! term) return ;
350+ try {
351+ this .searching = true ;
352+ this .searchError = null ;
353+
354+ const rawSf =
355+ (this .storefrontInfo ? .id || this .storefront || ' us' ).toString ().toLowerCase ();
356+ const sf = rawSf === ' auto' ? ' us' : rawSf;
357+
358+ const qs = new URLSearchParams ({
359+ q: term,
360+ limit: String (this .searchLimit ),
361+ language: this .language ,
362+ sf
363+ }).toString ();
364+
365+ const res = await fetch (` /applemusic/search?${ qs} ` , { credentials: ' same-origin' });
366+ const data = await res .json ();
367+
368+ if (! res .ok || ! data .ok ) throw new Error (data? .error || ` HTTP ${ res .status } ` );
369+ this .searchResults = data .results ;
370+ } catch (e) {
371+ this .searchResults = [];
372+ this .searchError = e? .message || ' Search failed' ;
373+ this .notify (' error' , this .searchError );
374+ } finally {
375+ this .searching = false ;
376+ }
377+ },
378+
282379 // copy to clipboard
283380 async copyToClipboard (text , successMsg = ' Copied' ) {
284381 try {
0 commit comments