Skip to content

Commit

Permalink
✨ feat: add remote songs to library
Browse files Browse the repository at this point in the history
Signed-off-by: SimonShiki <[email protected]>
  • Loading branch information
SimonShiki committed Aug 11, 2024
1 parent 0ae7814 commit e861c23
Show file tree
Hide file tree
Showing 13 changed files with 148 additions and 95 deletions.
15 changes: 14 additions & 1 deletion locales/en.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
{
"+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",
"D9n+q2": "Random",
"DUIDEq": "Storage",
"F62y+K": "Sign Out",
"FpsJ5J": "163 Account",
"HAlOn1": "Name",
"J3ca41": "Play",
"MMDobX": "WebDAV",
"OI9isw": "NetEase Cloud Music",
Expand All @@ -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"
}
16 changes: 15 additions & 1 deletion locales/zh-cn.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
{
"+0X5KL": "扫描中...",
"/clOBU": "每周",
"0+uHjb": "A - Z",
"2/2yg+": "添加",
"3HBU/l": "远程歌单",
"4cTGtq": "同步歌单",
"47FYwb": "取消",
"5ztrr5": "在本地缓存远程歌曲",
"6/NYCb": "流式播放目前不支持调整播放进度",
"A+Fjz9": "流式播放 (实验性)",
"AbbGDv": "每次启动时",
"AzGCoc": "知音",
"D3idYv": "设置",
"D9WUz8": "专辑",
"D9n+q2": "随机播放",
"DUIDEq": "存储",
"F62y+K": "登出",
"FpsJ5J": "网易账户",
"HAlOn1": "名字",
"J3ca41": "播放",
"MMDobX": "WebDAV",
"OI9isw": "网易云音乐",
Expand All @@ -25,18 +31,26 @@
"ZaKnsn": "登录以获取个人歌单...",
"ZqtXuy": "扫描",
"cyR7Kh": "返回",
"du1laW": "永不",
"e0bM5J": "扫码登录",
"eJzYtm": "Z - A",
"hP4Dbx": "时间倒序",
"gjBiyj": "加载中...",
"i+P3rZ": "自动扫描行为",
"m6bUGd": "排序方式",
"mOFG3K": "开始",
"pLEcFz": "多选",
"lKv8ex": "默认",
"sVX1a7": "要扫描的文件夹",
"sm72ff": "播放列表 ({total})",
"ug01Mk": "时间顺序",
"xMnH8F": "加载更多",
"y1Z3or": "语言",
"xmcVZ0": "搜索",
"yOcWit": "添加文件夹",
"yoNyUi": "你还没有登录...",
"j+BR5t": "歌单",
"pY1mMS": "所有音乐",
"zxUgzJ": "艺术家"
"zxUgzJ": "艺术家",
"zxvhnE": "每日"
}
3 changes: 0 additions & 3 deletions src-tauri/src/audio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/components/navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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' }) },
]
},
Expand Down
86 changes: 49 additions & 37 deletions src/components/song-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,55 +15,67 @@ interface SongItemProps {
onClick (song: AbstractSong<string>): void;
hideBg?: boolean;
onSelect? (checked: boolean): void;
onAddToLib? (song: AbstractSong<string>): 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 (
<Card onContextMenu={showContextMenu} onDoubleClick={() => {
Expand Down
21 changes: 12 additions & 9 deletions src/components/songlist-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
4 changes: 1 addition & 3 deletions src/jotais/library.ts
Original file line number Diff line number Diff line change
@@ -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<Song<string>[]>([]);
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<string>[] = [];
const storages = sharedStore.get(storagesJotai);
Expand Down
15 changes: 7 additions & 8 deletions src/pages/local.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -34,6 +26,13 @@ export default function Local () {
const [keyword, setKeyword] = useState('');
const [sortBy, setSortBy] = useState<SortOptions>('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);
Expand Down
10 changes: 7 additions & 3 deletions src/pages/ncm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -171,7 +171,9 @@ export default function NCM () {
return renderLoadMoreButton();
}
const song = searchResult[index];
return <SongItem song={song} onClick={() => handleClickSong(song, searchResult)} hideBg={!(index % 2)} />;
return <SongItem onAddToLib={(song) => {
ncmInstance.addToLib(song as Song<'ncm'>);
}} song={song} onClick={() => handleClickSong(song, searchResult)} hideBg={!(index % 2)} />;
}}
/>
</div>
Expand Down Expand Up @@ -227,7 +229,9 @@ export default function NCM () {
totalCount={songlistDetail.length}
itemContent={(index) => {
const song = songlistDetail[index];
return <SongItem song={song} onClick={() => handleClickSong(song, songlistDetail)} hideBg={!(index % 2)} />;
return <SongItem onAddToLib={(song) => {
ncmInstance.addToLib(song as Song<'ncm'>);
}} song={song} onClick={() => handleClickSong(song, songlistDetail)} hideBg={!(index % 2)} />;
}}
/>
) : (
Expand Down
25 changes: 13 additions & 12 deletions src/pages/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<LocalConfig, [SetStateAction<LocalConfig>], void>;
const ncmStorageConfigJotai = focusAtom(storagesConfigJotai, (optic) => optic.prop('ncm')) as unknown as WritableAtom<NCMConfig, [SetStateAction<NCMConfig>], void>;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -155,7 +154,7 @@ export default function Settings () {
<span className='grow-1'>
<FormattedMessage defaultMessage='Use streaming (Experimental)' />
</span>
<Tooltip content='Streaming does not currently support adjusting playback progress' placement='left' tooltipClassName='w-60'>
<Tooltip content={intl.formatMessage({ defaultMessage: 'Streaming does not currently support adjusting playback progress'})} placement='left' tooltipClassName='w-60'>
<Switch checked={streaming} onChange={setStreaming} />
</Tooltip>
</div>
Expand Down Expand Up @@ -325,7 +324,9 @@ export default function Settings () {
<div className='flex mt-2 gap-2 items-center'>
<span onClick={() => {
setQr(true);
} } className='color-fg-pri grow-1 font-size-sm cursor-pointer'>Use QRCode</span>
} } className='color-fg-pri grow-1 font-size-sm cursor-pointer'>
<FormattedMessage defaultMessage='Use QRCode' />
</span>
<Button size='lg' onClick={() => {
setNcmAuthModalOpen(false);
} }>
Expand Down
Loading

0 comments on commit e861c23

Please sign in to comment.