diff --git a/locales/en.json b/locales/en.json index b86cdc4..a2d69cd 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1,11 +1,15 @@ { "+0X5KL": "Scanning...", + "/clOBU": "Weekly", + "0+uHjb": "A - Z", "2/2yg+": "Add", "3HBU/l": "Remote Songlist", "47FYwb": "Cancel", "4cTGtq": "Sync song lists", "5ztrr5": "Cache remote songs in local", + "6/NYCb": "Streaming does not currently support adjusting playback progress", "A+Fjz9": "Use streaming (Experimental)", + "AbbGDv": "Each startup", "AzGCoc": "Cicadas", "D3idYv": "Settings", "D9WUz8": "Albums", @@ -13,6 +17,7 @@ "DUIDEq": "Storage", "F62y+K": "Sign Out", "FpsJ5J": "163 Account", + "HAlOn1": "Name", "J3ca41": "Play", "MMDobX": "WebDAV", "OI9isw": "NetEase Cloud Music", @@ -26,18 +31,26 @@ "ZaKnsn": "Sign in to get the song list...", "ZqtXuy": "Scan storages", "cyR7Kh": "Back", + "du1laW": "Never", + "e0bM5J": "Use QRCode", + "eJzYtm": "Z - A", "gjBiyj": "Loading...", + "hP4Dbx": "Time (Reversed)", "i+P3rZ": "Auto-scanning behavior", "j+BR5t": "Songlists", + "lKv8ex": "Default", "m6bUGd": "Sort By:", "mOFG3K": "Start", + "pLEcFz": "Multiselect", "pY1mMS": "Songs", "sVX1a7": "Folders to scan", "sm72ff": "Playlist ({total})", + "ug01Mk": "Time", "xMnH8F": "Load More", "xmcVZ0": "Search", "y1Z3or": "Language", "yOcWit": "Add Folders", "yoNyUi": "You've not logged in...", - "zxUgzJ": "Artists" + "zxUgzJ": "Artists", + "zxvhnE": "Daily" } diff --git a/locales/zh-cn.json b/locales/zh-cn.json index 0b9f099..14f7528 100644 --- a/locales/zh-cn.json +++ b/locales/zh-cn.json @@ -1,10 +1,15 @@ { "+0X5KL": "扫描中...", + "/clOBU": "每周", + "0+uHjb": "A - Z", "2/2yg+": "添加", "3HBU/l": "远程歌单", + "4cTGtq": "同步歌单", "47FYwb": "取消", "5ztrr5": "在本地缓存远程歌曲", + "6/NYCb": "流式播放目前不支持调整播放进度", "A+Fjz9": "流式播放 (实验性)", + "AbbGDv": "每次启动时", "AzGCoc": "知音", "D3idYv": "设置", "D9WUz8": "专辑", @@ -12,6 +17,7 @@ "DUIDEq": "存储", "F62y+K": "登出", "FpsJ5J": "网易账户", + "HAlOn1": "名字", "J3ca41": "播放", "MMDobX": "WebDAV", "OI9isw": "网易云音乐", @@ -25,12 +31,19 @@ "ZaKnsn": "登录以获取个人歌单...", "ZqtXuy": "扫描", "cyR7Kh": "返回", + "du1laW": "永不", + "e0bM5J": "扫码登录", + "eJzYtm": "Z - A", + "hP4Dbx": "时间倒序", "gjBiyj": "加载中...", "i+P3rZ": "自动扫描行为", "m6bUGd": "排序方式", "mOFG3K": "开始", + "pLEcFz": "多选", + "lKv8ex": "默认", "sVX1a7": "要扫描的文件夹", "sm72ff": "播放列表 ({total})", + "ug01Mk": "时间顺序", "xMnH8F": "加载更多", "y1Z3or": "语言", "xmcVZ0": "搜索", @@ -38,5 +51,6 @@ "yoNyUi": "你还没有登录...", "j+BR5t": "歌单", "pY1mMS": "所有音乐", - "zxUgzJ": "艺术家" + "zxUgzJ": "艺术家", + "zxvhnE": "每日" } diff --git a/src-tauri/src/audio.rs b/src-tauri/src/audio.rs index 6dc91c4..516962d 100644 --- a/src-tauri/src/audio.rs +++ b/src-tauri/src/audio.rs @@ -271,9 +271,6 @@ pub async fn play_local_file(app: tauri::AppHandle, audio_state: State<'_, Audio let sink = Sink::try_new(&audio_state.stream_handle) .map_err(|e| AppError::SinkCreationError(e.to_string()))?; - // Report the actual duration - let _ = app.emit("update_duration", source.total_duration().unwrap().as_millis()); - sink.append(source); *sink_guard = Some(sink); diff --git a/src/components/navigation.tsx b/src/components/navigation.tsx index 1eb895f..6014707 100644 --- a/src/components/navigation.tsx +++ b/src/components/navigation.tsx @@ -32,7 +32,7 @@ export default function Navigation () { { section: intl.formatMessage({ defaultMessage: 'Storage' }), items: [ { path: '/local', icon: 'i-fluent:folder-20-regular', label: intl.formatMessage({ defaultMessage: 'Local' }) }, - { path: '/webdav', icon: 'i-fluent:folder-globe-20-regular', label: intl.formatMessage({ defaultMessage: 'WebDAV' }) }, + /* { path: '/webdav', icon: 'i-fluent:folder-globe-20-regular', label: intl.formatMessage({ defaultMessage: 'WebDAV' }) }, */ { path: '/ncm', icon: 'i-simple-icons:neteasecloudmusic', label: intl.formatMessage({ defaultMessage: 'NCM' }) }, ] }, diff --git a/src/components/song-item.tsx b/src/components/song-item.tsx index f6e16a3..14ecf15 100644 --- a/src/components/song-item.tsx +++ b/src/components/song-item.tsx @@ -15,55 +15,67 @@ interface SongItemProps { onClick (song: AbstractSong): void; hideBg?: boolean; onSelect? (checked: boolean): void; + onAddToLib? (song: AbstractSong): void; } export default function SongItem (props: SongItemProps) { const [songlists, setSonglists] = useAtom(songlistsJotai); const showContextMenu = useCallback(async () => { - const menu = await Menu.new({ - items: [ - await MenuItem.new({ - text: 'Play', + const items = [ + await MenuItem.new({ + text: 'Play', + action: () => { + player.setCurrentSong(props.song); + } + }), + await MenuItem.new({ + text: 'Add to playlist', + action: () => { + props.onClick(props.song); + } + }), + await PredefinedMenuItem.new({ + item: 'Separator' + }), + await Submenu.new({ + text: 'Add to', + items: await Promise.all(songlists.map((songlist, index) => (MenuItem.new({ + text: songlist.name, action: () => { - player.setCurrentSong(props.song); + const newSonglist = { + ...songlist, + songs: [...songlist.songs, props.song] + }; + const newSonglists = [...songlists]; + newSonglists[index] = newSonglist; + setSonglists(newSonglists); } - }), + })))) + }) + ]; + if (props.onAddToLib) { + items.push( await MenuItem.new({ - text: 'Add to playlist', + text: 'Add to library', action: () => { - props.onClick(props.song); + props.onAddToLib!(props.song); } - }), - await PredefinedMenuItem.new({ - item: 'Separator' - }), - await Submenu.new({ - text: 'Add to', - items: await Promise.all(songlists.map((songlist, index) => (MenuItem.new({ - text: songlist.name, - action: () => { - const newSonglist = { - ...songlist, - songs: [...songlist.songs, props.song] - }; - const newSonglists = [...songlists]; - newSonglists[index] = newSonglist; - setSonglists(newSonglists); - } - })))) - }), - await IconMenuItem.new({ - icon: NativeIcon.Remove, - text: 'Remove' - }), - await IconMenuItem.new({ - icon: NativeIcon.Info, - text: 'Info' }) - ] - }); + ); + } + items.push( + await IconMenuItem.new({ + icon: NativeIcon.Remove, + text: 'Remove' + }), + await IconMenuItem.new({ + icon: NativeIcon.Info, + text: 'Info' + }) + ); + const menu = await Menu.new({items}); menu.popup(); - }, [songlists]); + }, [songlists, props.onAddToLib]); return ( { diff --git a/src/components/songlist-item.tsx b/src/components/songlist-item.tsx index 460a5f7..bb4188a 100644 --- a/src/components/songlist-item.tsx +++ b/src/components/songlist-item.tsx @@ -11,21 +11,24 @@ interface SonglistItemProps { name: string; cover?: string; onClick (id: string | number, index?: number): void; - onPlayAll (id: string | number, index?: number): void; + onPlayAll? (id: string | number, index?: number): void; onDelete? (id: string | number, index?: number): void; hideBg?: boolean; } export default function SonglistItem (props: SonglistItemProps) { const showContextMenu = useCallback(async () => { - const items = [ - await MenuItem.new({ - text: 'Play', - action: () => { - props.onPlayAll(props.id, props.index); - } - }) - ]; + const items = []; + if (props.onPlayAll) { + items.push( + await MenuItem.new({ + text: 'Play', + action: () => { + props.onPlayAll?.(props.id, props.index); + } + }) + ); + } if (props.onDelete) { items.push( await MenuItem.new({ diff --git a/src/jotais/library.ts b/src/jotais/library.ts index ae545fc..925ed74 100644 --- a/src/jotais/library.ts +++ b/src/jotais/library.ts @@ -1,14 +1,12 @@ import { atom } from 'jotai'; import { scannedJotai, Song, storagesJotai } from './storage'; import sharedStore from './shared-store'; -import { atomWithStorage } from 'jotai/utils'; import { backendStorage } from '../utils/local-utitity'; export const libraryJotai = atom[]>([]); sharedStore.sub(scannedJotai, () => { const allScanned = sharedStore.get(scannedJotai); - const library = sharedStore.get(libraryJotai); - if (!allScanned || library.length > 0) return; + if (!allScanned) return; const list: Song[] = []; const storages = sharedStore.get(storagesJotai); diff --git a/src/pages/local.tsx b/src/pages/local.tsx index 6446fce..b48eab1 100644 --- a/src/pages/local.tsx +++ b/src/pages/local.tsx @@ -18,14 +18,6 @@ const localStorageJotai = focusAtom(storagesJotai, (optic) => optic.prop('local' const songlistJotai = focusAtom(localStorageJotai, (optic) => optic.prop('songList')); const scannedJotai = focusAtom(localStorageJotai, (optic) => optic.prop('scanned')); -const sortOptions = [ - {value: 'default', label: 'Default'} as const, - {value: 'a-z', label: 'A - Z'} as const, - { value: 'z-a', label: 'Z - A' } as const, - { value: 'time_desc', label: 'Time (Reversed)' } as const, - { value: 'time_asc', label: 'Time' } as const -]; - export default function Local () { const _list = useAtomValue(songlistJotai); const [list, setList] = useState(_list); @@ -34,6 +26,13 @@ export default function Local () { const [keyword, setKeyword] = useState(''); const [sortBy, setSortBy] = useState('a-z'); const intl = useIntl(); + const sortOptions = [ + { value: 'default', label: intl.formatMessage({ defaultMessage: 'Default'}) } as const, + { value: 'a-z', label: intl.formatMessage({ defaultMessage: 'A - Z' }) } as const, + { value: 'z-a', label: intl.formatMessage({ defaultMessage: 'Z - A' }) } as const, + { value: 'time_desc', label: intl.formatMessage({ defaultMessage: 'Time (Reversed)' }) } as const, + { value: 'time_asc', label: intl.formatMessage({ defaultMessage: 'Time' }) } as const + ]; const handleClickSong = useCallback((song: Song<'local'>) => { player.clearPlaylist(); player.addToPlaylist(...list); diff --git a/src/pages/ncm.tsx b/src/pages/ncm.tsx index ab9bf00..90346c9 100644 --- a/src/pages/ncm.tsx +++ b/src/pages/ncm.tsx @@ -6,7 +6,7 @@ import Input from '../components/base/input'; import { SetStateAction, useCallback, useEffect, useState } from 'react'; import { Virtuoso } from 'react-virtuoso'; import { nowPlayingBarJotai } from '../jotais/play'; -import { storagesJotai, type Song as AbstractSong } from '../jotais/storage'; +import { Song, storagesJotai, type Song as AbstractSong } from '../jotais/storage'; import SongItem from '../components/song-item'; import * as player from '../utils/player'; import Spinner from '../components/base/spinner'; @@ -171,7 +171,9 @@ export default function NCM () { return renderLoadMoreButton(); } const song = searchResult[index]; - return handleClickSong(song, searchResult)} hideBg={!(index % 2)} />; + return { + ncmInstance.addToLib(song as Song<'ncm'>); + }} song={song} onClick={() => handleClickSong(song, searchResult)} hideBg={!(index % 2)} />; }} /> @@ -227,7 +229,9 @@ export default function NCM () { totalCount={songlistDetail.length} itemContent={(index) => { const song = songlistDetail[index]; - return handleClickSong(song, songlistDetail)} hideBg={!(index % 2)} />; + return { + ncmInstance.addToLib(song as Song<'ncm'>); + }} song={song} onClick={() => handleClickSong(song, songlistDetail)} hideBg={!(index % 2)} />; }} /> ) : ( diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index 77f3cef..cceb631 100644 --- a/src/pages/settings.tsx +++ b/src/pages/settings.tsx @@ -15,17 +15,9 @@ import Modal from '../components/base/modal'; import md5 from 'md5'; import { NCMConfig } from '../storages/ncm'; import Spinner from '../components/base/spinner'; -import { nowPlayingBarJotai } from '../jotais/play'; -import { FormattedMessage } from 'react-intl'; +import { FormattedMessage, useIntl } from 'react-intl'; import { langMap } from '../../locales'; -const autoScanOptions = [ - {value: 'startup', label: 'Each startup'} as const, - { value: 'daily', label: 'Daily' } as const, - { value: 'weekly', label: 'Weekly' } as const, - { value: 'never', label: 'Never' } as const -]; - const streamingJotai = focusAtom(settingsJotai, (optic) => optic.prop('streaming')); const localStorageConfigJotai = focusAtom(storagesConfigJotai, (optic) => optic.prop('local')) as unknown as WritableAtom], void>; const ncmStorageConfigJotai = focusAtom(storagesConfigJotai, (optic) => optic.prop('ncm')) as unknown as WritableAtom], void>; @@ -56,7 +48,14 @@ export default function Settings () { const [streaming, setStreaming] = useAtom(streamingJotai); const [ncmProfile, setNCMProfile] = useAtom(ncmProfileJotai); const [ncmAPI, setNcmAPI] = useAtom(ncmApiJotai); - const barOpen = useAtomValue(nowPlayingBarJotai); + const intl = useIntl(); + + const autoScanOptions = [ + { value: 'startup', label: intl.formatMessage({ defaultMessage: 'Each startup'}) } as const, + { value: 'daily', label: intl.formatMessage({ defaultMessage: 'Daily'}) } as const, + { value: 'weekly', label: intl.formatMessage({ defaultMessage: 'Weekly'}) } as const, + { value: 'never', label: intl.formatMessage({ defaultMessage: 'Never'}) } as const + ]; useEffect(() => { if (!qr) { @@ -155,7 +154,7 @@ export default function Settings () { - + @@ -325,7 +324,9 @@ export default function Settings () {
{ setQr(true); - } } className='color-fg-pri grow-1 font-size-sm cursor-pointer'>Use QRCode + } } className='color-fg-pri grow-1 font-size-sm cursor-pointer'> + +