@@ -1395,7 +1395,9 @@ <h1 data-i18n="appTitle">皮卡丘的音乐站</h1>
13951395 < select id ="playlist-select "> </ select >
13961396 < button id ="add-current-btn " class ="btn btn-secondary btn-icon ripple-target " title ="把当前歌曲加入歌单 "> +</ button >
13971397 < button id ="new-playlist-btn " class ="btn btn-ghost ripple-target " data-i18n ="newPlaylist "> 新建歌单</ button >
1398+ < button id ="import-playlist-btn " class ="btn btn-ghost ripple-target " data-i18n ="importPlaylist "> 导入歌单</ button >
13981399 < button id ="export-playlist-btn " class ="btn btn-ghost ripple-target " data-i18n ="exportPlaylist "> 导出歌单</ button >
1400+ < input id ="import-playlist-input " type ="file " accept ="application/json,.json " style ="display:none; " />
13991401 </ div >
14001402 < div class ="playmode-row ">
14011403 < button class ="playmode-btn active ripple-target " data-mode ="list " title ="列表循环 "> 🔁</ button >
@@ -1532,6 +1534,7 @@ <h1 data-i18n="appTitle">皮卡丘的音乐站</h1>
15321534 playlistInfoFavorites :"收藏列表" ,
15331535 playlistInfoPlaylist :"歌单" ,
15341536 newPlaylist :"新建歌单" ,
1537+ importPlaylist :"导入歌单" ,
15351538 exportPlaylist :"导出歌单" ,
15361539 footerText :"本站仅作为学习演示,音乐版权归各平台与原作者所有。" ,
15371540 toastAddedFavorite :"已添加到收藏" ,
@@ -1545,6 +1548,9 @@ <h1 data-i18n="appTitle">皮卡丘的音乐站</h1>
15451548 toastLyricStyleSwitched :"已切换歌词炫酷效果。" ,
15461549 toastDownloadNotReady :"当前歌曲还未加载完成,稍后再试。" ,
15471550 toastPlaylistCreated :"歌单创建成功。" ,
1551+ toastPlaylistImported :"导入完成" ,
1552+ toastPlaylistImportEmpty :"导入文件里没有可用歌单或收藏。" ,
1553+ toastPlaylistImportError :"导入失败,请确认文件是本站导出的 JSON。" ,
15481554 toastPlaylistExported :"已导出歌单文件。" ,
15491555 toastPlaylistExportEmpty :"暂无可导出的歌单。" ,
15501556 toastPlaylistEmpty :"当前歌单为空,先添加几首歌吧~" ,
@@ -1603,6 +1609,7 @@ <h1 data-i18n="appTitle">皮卡丘的音乐站</h1>
16031609 playlistInfoFavorites :"Favorites List" ,
16041610 playlistInfoPlaylist :"Playlist" ,
16051611 newPlaylist :"Create" ,
1612+ importPlaylist :"Import" ,
16061613 exportPlaylist :"Export" ,
16071614 footerText :"For demo only. All music copyrights belong to original owners." ,
16081615 toastAddedFavorite :"Added to favorites" ,
@@ -1616,6 +1623,9 @@ <h1 data-i18n="appTitle">皮卡丘的音乐站</h1>
16161623 toastLyricStyleSwitched :"Lyrics FX toggled." ,
16171624 toastDownloadNotReady :"Song not fully loaded yet. Try again later." ,
16181625 toastPlaylistCreated :"Playlist created." ,
1626+ toastPlaylistImported :"Import completed" ,
1627+ toastPlaylistImportEmpty :"No usable playlists or favorites found in this file." ,
1628+ toastPlaylistImportError :"Import failed. Please choose a JSON file exported by this site." ,
16191629 toastPlaylistExported :"Playlist file exported." ,
16201630 toastPlaylistExportEmpty :"No playlist to export." ,
16211631 toastPlaylistEmpty :"Playlist is empty. Add some songs first." ,
@@ -1846,6 +1856,104 @@ <h1 data-i18n="appTitle">皮卡丘的音乐站</h1>
18461856 showToast ( t ( 'toastPlaylistExported' ) ) ;
18471857 }
18481858
1859+ function mergeImportedTracks ( targetList , rawTracks ) {
1860+ let added = 0 ;
1861+ if ( ! Array . isArray ( rawTracks ) ) return added ;
1862+
1863+ rawTracks . forEach ( raw => {
1864+ const imported = deserializeTrack ( raw ) ;
1865+ if ( ! imported || ! imported . uid ) return ;
1866+
1867+ const track = state . trackMap . get ( imported . uid ) || imported ;
1868+ if ( ! state . trackMap . has ( track . uid ) ) state . trackMap . set ( track . uid , track ) ;
1869+
1870+ if ( ! targetList . some ( item => item . uid === track . uid ) ) {
1871+ targetList . push ( track ) ;
1872+ added ++ ;
1873+ }
1874+ } ) ;
1875+
1876+ return added ;
1877+ }
1878+
1879+ function importPlaylistData ( data ) {
1880+ if ( ! data || typeof data !== 'object' ) throw new Error ( 'invalid import data' ) ;
1881+
1882+ let addedFavorites = 0 ;
1883+ let addedPlaylists = 0 ;
1884+ let addedPlaylistTracks = 0 ;
1885+
1886+ addedFavorites = mergeImportedTracks ( state . favorites , data . favorites ) ;
1887+
1888+ const importedPlaylists = Array . isArray ( data . playlists ) ? data . playlists : [ ] ;
1889+ importedPlaylists . forEach ( ( pl , idx ) => {
1890+ if ( ! pl || typeof pl !== 'object' ) return ;
1891+
1892+ const fallbackName = state . language === 'zh' ? '导入歌单' : 'Imported Playlist' ;
1893+ const name = ( pl . name || fallbackName ) . toString ( ) . trim ( ) || fallbackName ;
1894+ const rawId = ( pl . id || '' ) . toString ( ) . trim ( ) ;
1895+
1896+ let target = rawId ? state . playlists . find ( item => item . id === rawId ) : null ;
1897+ if ( ! target ) target = state . playlists . find ( item => item . name === name ) ;
1898+
1899+ if ( ! target ) {
1900+ let id = rawId || ( 'pl-import-' + Date . now ( ) + '-' + idx + '-' + Math . random ( ) . toString ( 16 ) . slice ( 2 ) ) ;
1901+ if ( state . playlists . some ( item => item . id === id ) ) {
1902+ id = 'pl-import-' + Date . now ( ) + '-' + idx + '-' + Math . random ( ) . toString ( 16 ) . slice ( 2 ) ;
1903+ }
1904+ target = { id, name, tracks :[ ] } ;
1905+ state . playlists . push ( target ) ;
1906+ addedPlaylists ++ ;
1907+ }
1908+
1909+ addedPlaylistTracks += mergeImportedTracks ( target . tracks , pl . tracks ) ;
1910+ } ) ;
1911+
1912+ const totalAdded = addedFavorites + addedPlaylists + addedPlaylistTracks ;
1913+ const hasUsableData = ( Array . isArray ( data . favorites ) && data . favorites . length ) || importedPlaylists . length ;
1914+ if ( ! hasUsableData ) return { empty :true , addedFavorites, addedPlaylists, addedPlaylistTracks, totalAdded} ;
1915+
1916+ rebuildLibraryTrackMap ( ) ;
1917+ renderPlaylistOptions ( ) ;
1918+ saveLibraryToStorage ( ) ;
1919+ renderPlaylistList ( ) ;
1920+ updateMainFavButton ( ) ;
1921+
1922+ return { empty :false , addedFavorites, addedPlaylists, addedPlaylistTracks, totalAdded} ;
1923+ }
1924+
1925+ function handleImportPlaylistFile ( e ) {
1926+ const input = e . target ;
1927+ const file = input . files && input . files [ 0 ] ;
1928+ if ( ! file ) return ;
1929+
1930+ const reader = new FileReader ( ) ;
1931+ reader . onload = ( ) => {
1932+ try {
1933+ const data = JSON . parse ( reader . result ) ;
1934+ const stat = importPlaylistData ( data ) ;
1935+ if ( stat . empty ) {
1936+ showToast ( t ( 'toastPlaylistImportEmpty' ) ) ;
1937+ } else {
1938+ const msg = state . language === 'zh'
1939+ ? `${ t ( 'toastPlaylistImported' ) } :新增 ${ stat . addedPlaylists } 个歌单,${ stat . addedFavorites } 首收藏,${ stat . addedPlaylistTracks } 首歌单歌曲。`
1940+ : `${ t ( 'toastPlaylistImported' ) } : ${ stat . addedPlaylists } playlists, ${ stat . addedFavorites } favorites, ${ stat . addedPlaylistTracks } playlist tracks added.` ;
1941+ showToast ( msg ) ;
1942+ }
1943+ } catch ( err ) {
1944+ console . error ( 'import playlist failed' , err ) ;
1945+ showToast ( t ( 'toastPlaylistImportError' ) ) ;
1946+ } finally {
1947+ input . value = '' ;
1948+ }
1949+ } ;
1950+ reader . onerror = ( ) => {
1951+ showToast ( t ( 'toastPlaylistImportError' ) ) ;
1952+ input . value = '' ;
1953+ } ;
1954+ reader . readAsText ( file , 'utf-8' ) ;
1955+ }
1956+
18491957 function renderPlaylistOptions ( ) {
18501958 if ( ! dom . playlistSelect ) return ;
18511959 const prev = dom . playlistSelect . value || state . playContext . playlistId ;
@@ -2947,6 +3055,8 @@ <h1 data-i18n="appTitle">皮卡丘的音乐站</h1>
29473055 dom . playlistSelectRow = $ ( 'playlist-select-row' ) ;
29483056 dom . playlistSelect = $ ( 'playlist-select' ) ;
29493057 dom . newPlaylistBtn = $ ( 'new-playlist-btn' ) ;
3058+ dom . importPlaylistBtn = $ ( 'import-playlist-btn' ) ;
3059+ dom . importPlaylistInput = $ ( 'import-playlist-input' ) ;
29503060 dom . exportPlaylistBtn = $ ( 'export-playlist-btn' ) ;
29513061 dom . addCurrentBtn = $ ( 'add-current-btn' ) ;
29523062
@@ -3090,6 +3200,8 @@ <h1 data-i18n="appTitle">皮卡丘的音乐站</h1>
30903200 } ) ;
30913201
30923202 dom . newPlaylistBtn . addEventListener ( 'click' , openPlaylistModal ) ;
3203+ dom . importPlaylistBtn . addEventListener ( 'click' , ( ) => dom . importPlaylistInput . click ( ) ) ;
3204+ dom . importPlaylistInput . addEventListener ( 'change' , handleImportPlaylistFile ) ;
30933205 dom . exportPlaylistBtn . addEventListener ( 'click' , exportPlaylistData ) ;
30943206 dom . playlistConfirmBtn . addEventListener ( 'click' , createPlaylist ) ;
30953207 dom . playlistCancelBtn . addEventListener ( 'click' , closePlaylistModal ) ;
0 commit comments