diff --git a/publish/changeLog.md b/publish/changeLog.md index 865d67d..1174121 100644 --- a/publish/changeLog.md +++ b/publish/changeLog.md @@ -13,6 +13,7 @@ - 新增列表设置-是否显示歌曲专辑名,默认关闭 - 新增列表设置-是否显示歌曲时长,默认开启 - 新增是否允许通过歌词调整播放进度功能,默认关闭,可到播放详情页右上角设置开启 +- 新增“不喜欢歌曲”功能,可以在我的列表或者在线列表内歌曲的右击菜单使用,还可以去“设置-其他”手动编辑不喜欢规则,注:“上一曲”、“下一曲”功能将跳过符合“不喜欢歌曲”规则的歌曲,但你仍可以手动播放这些歌曲 - 新增设置-播放设置-是否启用音频卸载,该设置之前默认是启用的,现在添加开关允许将其关闭,若出现播放器问题可尝试将其关闭 ### 优化 diff --git a/src/components/OnlineList/ListMenu.tsx b/src/components/OnlineList/ListMenu.tsx index bd51ce3..9a5a617 100644 --- a/src/components/OnlineList/ListMenu.tsx +++ b/src/components/OnlineList/ListMenu.tsx @@ -1,6 +1,7 @@ import { useMemo, useRef, useImperativeHandle, forwardRef, useState } from 'react' import { useI18n } from '@/lang' import Menu, { type MenuType, type Position } from '@/components/common/Menu' +import { hasDislike } from '@/core/dislikeList' export interface SelectInfo { musicInfo: LX.Music.MusicInfoOnline @@ -15,6 +16,7 @@ export interface ListMenuProps { onPlayLater: (selectInfo: SelectInfo) => void onAdd: (selectInfo: SelectInfo) => void onCopyName: (selectInfo: SelectInfo) => void + onDislikeMusic: (selectInfo: SelectInfo) => void } export interface ListMenuType { show: (selectInfo: SelectInfo, position: Position) => void @@ -29,10 +31,12 @@ export default forwardRef((props: ListMenuProps, re const [visible, setVisible] = useState(false) const menuRef = useRef(null) const selectInfoRef = useRef(initSelectInfo as SelectInfo) + const [isDislikeMusic, setDislikeMusic] = useState(false) useImperativeHandle(ref, () => ({ show(selectInfo, position) { selectInfoRef.current = selectInfo + setDislikeMusic(hasDislike(selectInfo.musicInfo)) if (visible) menuRef.current?.show(position) else { setVisible(true) @@ -50,8 +54,9 @@ export default forwardRef((props: ListMenuProps, re // { action: 'download', label: '下载' }, { action: 'add', label: t('add_to') }, { action: 'copyName', label: t('copy_name') }, + { action: 'dislike', label: t('dislike'), disabled: isDislikeMusic }, ] as const - }, [t]) + }, [t, isDislikeMusic]) const handleMenuPress = ({ action }: typeof menus[number]) => { const selectInfo = selectInfoRef.current @@ -68,6 +73,9 @@ export default forwardRef((props: ListMenuProps, re case 'copyName': props.onCopyName(selectInfo) break + case 'dislike': + props.onDislikeMusic(selectInfo) + break default: break } diff --git a/src/components/OnlineList/index.tsx b/src/components/OnlineList/index.tsx index d1569f4..7b082af 100644 --- a/src/components/OnlineList/index.tsx +++ b/src/components/OnlineList/index.tsx @@ -6,7 +6,7 @@ import ListMenu, { type ListMenuType, type Position, type SelectInfo } from './L import ListMusicMultiAdd, { type MusicMultiAddModalType as ListAddMultiType } from '@/components/MusicMultiAddModal' import ListMusicAdd, { type MusicAddModalType as ListMusicAddType } from '@/components/MusicAddModal' import MultipleModeBar, { type MultipleModeBarType, type SelectMode } from './MultipleModeBar' -import { handlePlay, handlePlayLater, handleShare } from './listAction' +import { handleDislikeMusic, handlePlay, handlePlayLater, handleShare } from './listAction' import { createStyle } from '@/utils/tools' export interface OnlineListProps { @@ -109,6 +109,7 @@ export default forwardRef(({ onPlayLater={info => { hancelExitSelect(); handlePlayLater(info.musicInfo, info.selectedList, hancelExitSelect) }} onCopyName={info => { handleShare(info.musicInfo) }} onAdd={handleAddMusic} + onDislikeMusic={info => { void handleDislikeMusic(info.musicInfo) }} /> {/* */} diff --git a/src/components/OnlineList/listAction.ts b/src/components/OnlineList/listAction.ts index 8fbedd6..d1c7f3c 100644 --- a/src/components/OnlineList/listAction.ts +++ b/src/components/OnlineList/listAction.ts @@ -1,10 +1,12 @@ import { LIST_IDS } from '@/config/constant' import { addListMusics } from '@/core/list' -import { playList } from '@/core/player/player' +import { playList, playNext } from '@/core/player/player' import { addTempPlayList } from '@/core/player/tempPlayList' import settingState from '@/store/setting/state' import { getListMusicSync } from '@/utils/listManage' import { shareMusic } from '@/utils/tools' +import { addDislikeInfo, hasDislike } from '@/core/dislikeList' +import playerState from '@/store/player/state' export const handlePlay = (musicInfo: LX.Music.MusicInfoOnline) => { void addListMusics(LIST_IDS.DEFAULT, [musicInfo], settingState.setting['list.addMusicLocationType']).then(() => { @@ -27,3 +29,10 @@ export const handleShare = (musicInfo: LX.Music.MusicInfoOnline) => { shareMusic(settingState.setting['common.shareType'], settingState.setting['download.fileName'], musicInfo) } +export const handleDislikeMusic = async(musicInfo: LX.Music.MusicInfoOnline) => { + await addDislikeInfo([{ name: musicInfo.name, singer: musicInfo.singer }]) + if (hasDislike(playerState.playMusicInfo.musicInfo)) { + void playNext(true) + } +} + diff --git a/src/components/common/Dialog.tsx b/src/components/common/Dialog.tsx index 8515585..72eb01e 100644 --- a/src/components/common/Dialog.tsx +++ b/src/components/common/Dialog.tsx @@ -63,6 +63,7 @@ export interface DialogProps { closeBtn?: boolean title?: string children: React.ReactNode | React.ReactNode[] + height?: number | `${number}%` } export interface DialogType { @@ -76,6 +77,7 @@ export default forwardRef(({ closeBtn = true, title = '', children, + height, }: DialogProps, ref) => { const theme = useTheme() const { keyboardShown, keyboardHeight } = useKeyboard() @@ -98,7 +100,7 @@ export default forwardRef(({ return ( - true}> + true}> {title} {closeBtnComponent} diff --git a/src/components/common/Input.tsx b/src/components/common/Input.tsx index 6c213af..07f8897 100644 --- a/src/components/common/Input.tsx +++ b/src/components/common/Input.tsx @@ -3,6 +3,7 @@ import { TextInput, View, TouchableOpacity, StyleSheet, type TextInputProps } fr import { Icon } from '@/components/common/Icon' import { createStyle } from '@/utils/tools' import { useTheme } from '@/store/theme/hook' +import { setSpText } from '@/utils/pixelRatio' const styles = createStyle({ content: { @@ -46,6 +47,7 @@ export interface InputProps extends TextInputProps { onChangeText?: (value: string) => void onClearText?: () => void clearBtn?: boolean + size?: number } @@ -56,7 +58,7 @@ export interface InputType { isFocused: () => boolean } -export default forwardRef(({ onChangeText, onClearText, clearBtn, style, ...props }, ref) => { +export default forwardRef(({ onChangeText, onClearText, clearBtn, style, size = 14, ...props }, ref) => { const inputRef = useRef(null) const theme = useTheme() // const scaleClearBtn = useRef(new Animated.Value(0)).current @@ -113,7 +115,7 @@ export default forwardRef(({ onChangeText, onClearText, c autoCapitalize="none" onChangeText={changeText} autoComplete="off" - style={StyleSheet.compose({ ...styles.input, color: theme['c-font'] }, style)} + style={StyleSheet.compose({ ...styles.input, color: theme['c-font'], fontSize: setSpText(size) }, style)} placeholderTextColor={theme['c-primary-dark-100-alpha-600']} selectionColor={theme['c-primary-light-100-alpha-300']} ref={inputRef} {...props} /> diff --git a/src/config/constant.ts b/src/config/constant.ts index be86397..c878bfb 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -2,6 +2,11 @@ export const HEADER_HEIGHT = 42 export const LIST_ITEM_HEIGHT = 54 export const LIST_SCROLL_POSITION_KEY = '__LIST_SCROLL_POSITION_KEY__' +export const SPLIT_CHAR = { + DISLIKE_NAME: '@', + DISLIKE_NAME_ALIAS: '#', +} as const + export const LIST_IDS = { DEFAULT: 'default', LOVE: 'love', @@ -65,6 +70,8 @@ export const storageDataPrefix = { theme: '@theme', cheatTip: '@cheat_tip', + + dislikeList: '@dislike_list', } as const // v0.x.x 版本的 data keys diff --git a/src/core/dislikeList.ts b/src/core/dislikeList.ts new file mode 100644 index 0000000..a946262 --- /dev/null +++ b/src/core/dislikeList.ts @@ -0,0 +1,28 @@ +// import { toRaw } from '@common/utils/vueTools' +import { SPLIT_CHAR } from '@/config/constant' +import { action, state } from '@/store/dislikeList' +import { getDislikeListRules, saveDislikeListRules } from '@/utils/data' + + +export const initDislikeInfo = async() => { + const rules = await getDislikeListRules() + action.initDislikeInfo(rules) +} + +export const addDislikeInfo = async(infos: LX.Dislike.DislikeMusicInfo[]) => { + const rules = state.dislikeInfo.rules += '\n' + infos.map(info => `${info.name ?? ''}${SPLIT_CHAR.DISLIKE_NAME}${info.singer ?? ''}`).join('\n') + await saveDislikeListRules(rules) + return action.overwirteDislikeInfo(rules) +} + +export const overwirteDislikeInfo = async(rules: string) => { + await saveDislikeListRules(rules) + return action.overwirteDislikeInfo(rules) +} + + +export const hasDislike = (info: LX.Music.MusicInfo | LX.Download.ListItem | null) => { + if (!info) return false + return action.hasDislike(info) +} + diff --git a/src/core/init/dataInit.ts b/src/core/init/dataInit.ts index 7927e80..8de9c81 100644 --- a/src/core/init/dataInit.ts +++ b/src/core/init/dataInit.ts @@ -5,6 +5,7 @@ import { getUserLists, setUserList } from '@/core/list' import { setNavActiveId } from '../common' import { getViewPrevState } from '@/utils/data' import { bootLog } from '@/utils/bootLog' +import { initDislikeInfo } from '@/core/dislikeList' // import { play, playList } from '../player/player' // const initPrevPlayInfo = async(appSetting: LX.AppSetting) => { @@ -26,6 +27,7 @@ export default async(appSetting: LX.AppSetting) => { void musicSdkInit() // 初始化音乐sdk bootLog('User list init...') setUserList(await getUserLists()) // 获取用户列表 + await initDislikeInfo() // 获取不喜欢列表 bootLog('User list inited.') setNavActiveId((await getViewPrevState()).id) // await initPrevPlayInfo(appSetting).catch(err => log.error(err)) // 初始化上次的歌曲播放信息 diff --git a/src/core/player/player.ts b/src/core/player/player.ts index ff355bf..fa49200 100644 --- a/src/core/player/player.ts +++ b/src/core/player/player.ts @@ -317,6 +317,7 @@ export const playNext = async(isAutoToggle = false): Promise => { list: currentList, playedList, playerMusicInfo: currentList[playInfo.playerPlayIndex], + isNext: true, }) if (!filteredList.length) return handleToggleStop() @@ -410,6 +411,7 @@ export const playPrev = async(isAutoToggle = false): Promise => { list: currentList, playedList, playerMusicInfo: currentList[playInfo.playerPlayIndex], + isNext: false, }) if (!filteredList.length) return handleToggleStop() diff --git a/src/core/player/utils.ts b/src/core/player/utils.ts index e232da4..0f76d34 100644 --- a/src/core/player/utils.ts +++ b/src/core/player/utils.ts @@ -1,9 +1,11 @@ import { clearPlayedList } from './playedList' +import { SPLIT_CHAR } from '@/config/constant' +import { state } from '@/store/dislikeList' /** * 过滤列表中已播放的歌曲 */ -export const filterMusicList = ({ playedList, listId, list, playerMusicInfo }: { +export const filterMusicList = ({ playedList, listId, list, playerMusicInfo, dislikeInfo, isNext }: { /** * 已播放列表 */ @@ -24,15 +26,35 @@ export const filterMusicList = ({ playedList, listId, list, playerMusicInfo }: { * 播放器内当前歌曲(`playInfo.playerPlayIndex`指向的歌曲) */ playerMusicInfo?: LX.Music.MusicInfo | LX.Download.ListItem + + /** + * 不喜欢的歌曲名字列表 + */ + dislikeInfo: Omit + + isNext: boolean }) => { let playerIndex = -1 let canPlayList: Array = [] const filteredPlayedList = playedList.filter(pmInfo => pmInfo.listId == listId && !pmInfo.isTempPlay).map(({ musicInfo }) => musicInfo) + const hasDislike = (info: LX.Music.MusicInfo) => { + const name = info.name?.replaceAll(SPLIT_CHAR.DISLIKE_NAME, SPLIT_CHAR.DISLIKE_NAME_ALIAS).toLocaleLowerCase().trim() ?? '' + const singer = info.singer?.replaceAll(SPLIT_CHAR.DISLIKE_NAME, SPLIT_CHAR.DISLIKE_NAME_ALIAS).toLocaleLowerCase().trim() ?? '' + return dislikeInfo.musicNames.has(name) || dislikeInfo.singerNames.has(singer) || + dislikeInfo.names.has(`${name}${SPLIT_CHAR.DISLIKE_NAME}${singer}`) + } + + let isDislike = false const filteredList: Array = list.filter(s => { // if (!assertApiSupport(s.source)) return false - if ('progress' in s && !s.isComplate) return false + if ('progress' in s) { + if (!s.isComplate) return false + } else if (hasDislike(s)) { + if (s.id != playerMusicInfo?.id) return false + isDislike = true + } canPlayList.push(s) @@ -44,7 +66,34 @@ export const filterMusicList = ({ playedList, listId, list, playerMusicInfo }: { return true }) if (playerMusicInfo) { - playerIndex = (filteredList.length ? filteredList : canPlayList).findIndex(m => m.id == playerMusicInfo.id) + if (isDislike) { + if (filteredList.length <= 1) { + filteredList.splice(0, 1) + if (canPlayList.length > 1) { + let currentMusicIndex = canPlayList.findIndex(m => m.id == playerMusicInfo.id) + if (isNext) { + playerIndex = currentMusicIndex - 1 + if (playerIndex < 0 && canPlayList.length > 1) playerIndex = canPlayList.length - 2 + } else { + playerIndex = currentMusicIndex + if (canPlayList.length <= 1) playerIndex = -1 + } + canPlayList.splice(currentMusicIndex, 1) + } else canPlayList.splice(0, 1) + } else { + let currentMusicIndex = filteredList.findIndex(m => m.id == playerMusicInfo.id) + if (isNext) { + playerIndex = currentMusicIndex - 1 + if (playerIndex < 0 && filteredList.length > 1) playerIndex = filteredList.length - 2 + } else { + playerIndex = currentMusicIndex + if (filteredList.length <= 1) playerIndex = -1 + } + filteredList.splice(currentMusicIndex, 1) + } + } else { + playerIndex = (filteredList.length ? filteredList : canPlayList).findIndex(m => m.id == playerMusicInfo.id) + } } return { filteredList, @@ -56,11 +105,12 @@ export const filterMusicList = ({ playedList, listId, list, playerMusicInfo }: { /** * 过滤列表中已播放的歌曲 */ -export const filterList = ({ playedList, listId, list, playerMusicInfo }: { +export const filterList = ({ playedList, listId, list, playerMusicInfo, isNext }: { playedList: LX.Player.PlayMusicInfo[] | readonly LX.Player.PlayMusicInfo[] listId: string list: Array playerMusicInfo?: LX.Music.MusicInfo | LX.Download.ListItem + isNext: boolean }) => { // if (this.list.listName === null) return // console.log(isCheckFile) @@ -70,6 +120,8 @@ export const filterList = ({ playedList, listId, list, playerMusicInfo }: { playedList, // savePath: global.lx.setting['download.savePath'], playerMusicInfo, + dislikeInfo: { names: state.dislikeInfo.names, musicNames: state.dislikeInfo.musicNames, singerNames: state.dislikeInfo.singerNames }, + isNext, }) if (!filteredList.length && playedList.length) { diff --git a/src/lang/en_us.json b/src/lang/en_us.json index 6fe8066..43e46c5 100644 --- a/src/lang/en_us.json +++ b/src/lang/en_us.json @@ -35,6 +35,7 @@ "dialog_confirm": "OK", "disagree": "Disagree", "disagree_tip": "Cancelled...", + "dislike": "Dislike", "duplicate_list_tip": "You have previously favorited the list [{name}], do you want to update the songs?", "exit_app_tip": "Are you sure you want to quit the app?", "input_error": "Don't input indiscriminately 😡", @@ -142,6 +143,10 @@ "search_hot_search": "popular searches", "search_type_music": "Music", "search_type_songlist": "Song list", + "setting_dislike_list_tips": "1. If there is a \"@\" symbol in the song or singer's name, you need to replace it with \"#\"\n2. Specify a song of a singer: @\n3. Specify a song: \n4. Specify a certain singer:@", + "setting__other_dislike_list": "Dislike song rule", + "setting__other_dislike_list_label": "Number of rules: {num}", + "setting__other_dislike_list_saved_tip": "Saved", "setting__other_lyric_raw_clear_btn": "Clear lyrics cache", "setting__other_lyric_raw_label": "Number of lyrics:", "setting__other_meta_cache": "Other cache management", @@ -240,6 +245,7 @@ "setting_other_cache_clear_success_tip": "Cache clearing completed", "setting_other_cache_getting": "Statistics cached...", "setting_other_cache_size": "Currently used cache size: ", + "setting_other_dislike_list_show_btn": "Edit dislike song rules", "setting_other_log": "Error log (log when abnormal operation occurs)", "setting_other_log_btn_clean": "Clear", "setting_other_log_btn_hide": "Close", diff --git a/src/lang/zh_cn.json b/src/lang/zh_cn.json index 59415c1..e8c0181 100644 --- a/src/lang/zh_cn.json +++ b/src/lang/zh_cn.json @@ -35,6 +35,7 @@ "dialog_confirm": "好的", "disagree": "我就不", "disagree_tip": "那算了... 🙄", + "dislike": "不喜欢", "duplicate_list_tip": "你之前已收藏过该列表 [{name}],是否需要更新里面的歌曲?", "exit_app_tip": "确定要退出应用吗?", "input_error": "不要乱输好吧😡", @@ -142,6 +143,9 @@ "search_hot_search": "热门搜索", "search_type_music": "歌曲", "search_type_songlist": "歌单", + "setting__other_dislike_list": "不喜欢的歌曲规则", + "setting__other_dislike_list_label": "规则数量:{num}", + "setting__other_dislike_list_saved_tip": "已保存", "setting__other_lyric_raw_clear_btn": "清理歌词缓存", "setting__other_lyric_raw_label": "歌词数量:", "setting__other_meta_cache": "其他缓存管理", @@ -234,12 +238,14 @@ "setting_lyric_desktop_theme": "歌词主题色", "setting_lyric_desktop_toggle_anima": "显示歌词切换动画", "setting_lyric_desktop_view_width": "窗口百分比宽度", + "setting_dislike_list_tips": "1. 每条一行,若歌曲或者歌手名字中存在“@”符号,需要将其替换成“#”\n2. 指定某歌手的某首歌:<歌曲名>@<歌手名>\n3. 指定某首歌:<歌曲名>\n4. 指定某歌手:@<歌手名>", "setting_other": "其他", "setting_other_cache": "缓存管理(包括歌曲、歌词、错误日志等缓存,没有歌曲播放相关的问题不建议清理)", "setting_other_cache_clear_btn": "清理缓存", "setting_other_cache_clear_success_tip": "缓存清理完成", "setting_other_cache_getting": "统计缓存中...", "setting_other_cache_size": "当前已用缓存大小:", + "setting_other_dislike_list_show_btn": "编辑不喜欢歌曲规则", "setting_other_log": "错误日志(运行发生异常时的日志)", "setting_other_log_btn_clean": "清空", "setting_other_log_btn_hide": "关闭", diff --git a/src/screens/Home/Views/Mylist/MusicList/ListMenu.tsx b/src/screens/Home/Views/Mylist/MusicList/ListMenu.tsx index 072db95..d6d0818 100644 --- a/src/screens/Home/Views/Mylist/MusicList/ListMenu.tsx +++ b/src/screens/Home/Views/Mylist/MusicList/ListMenu.tsx @@ -1,6 +1,7 @@ import { useMemo, useRef, useImperativeHandle, forwardRef, useState } from 'react' import { useI18n } from '@/lang' import Menu, { type MenuType, type Position } from '@/components/common/Menu' +import { hasDislike } from '@/core/dislikeList' export interface SelectInfo { musicInfo: LX.Music.MusicInfo @@ -18,6 +19,7 @@ export interface ListMenuProps { onMove: (selectInfo: SelectInfo) => void onCopyName: (selectInfo: SelectInfo) => void onChangePosition: (selectInfo: SelectInfo) => void + onDislikeMusic: (selectInfo: SelectInfo) => void onRemove: (selectInfo: SelectInfo) => void } export interface ListMenuType { @@ -33,10 +35,12 @@ export default forwardRef((props, ref) => { const [visible, setVisible] = useState(false) const menuRef = useRef(null) const selectInfoRef = useRef(initSelectInfo as SelectInfo) + const [isDislikeMusic, setDislikeMusic] = useState(false) useImperativeHandle(ref, () => ({ show(selectInfo, position) { selectInfoRef.current = selectInfo + setDislikeMusic(hasDislike(selectInfo.musicInfo)) if (visible) menuRef.current?.show(position) else { setVisible(true) @@ -56,9 +60,10 @@ export default forwardRef((props, ref) => { { action: 'move', label: t('move_to') }, { action: 'copyName', label: t('copy_name') }, { action: 'changePosition', label: t('change_position') }, + { action: 'dislike', label: t('dislike'), disabled: isDislikeMusic }, { action: 'remove', label: t('delete') }, ] as const - }, [t]) + }, [t, isDislikeMusic]) const handleMenuPress = ({ action }: typeof menus[number]) => { const selectInfo = selectInfoRef.current @@ -91,6 +96,9 @@ export default forwardRef((props, ref) => { props.onChangePosition(selectInfo) // setVIsibleMusicPosition(true) break + case 'dislike': + props.onDislikeMusic(selectInfo) + break case 'remove': props.onRemove(selectInfo) break diff --git a/src/screens/Home/Views/Mylist/MusicList/index.tsx b/src/screens/Home/Views/Mylist/MusicList/index.tsx index 25292bf..5ec6360 100644 --- a/src/screens/Home/Views/Mylist/MusicList/index.tsx +++ b/src/screens/Home/Views/Mylist/MusicList/index.tsx @@ -2,7 +2,7 @@ import { useRef } from 'react' import listState from '@/store/list/state' import ListMenu, { type ListMenuType, type Position, type SelectInfo } from './ListMenu' -import { handlePlay, handlePlayLater, handleRemove, handleShare, handleUpdateMusicPosition } from './listAction' +import { handleDislikeMusic, handlePlay, handlePlayLater, handleRemove, handleShare, handleUpdateMusicPosition } from './listAction' import List, { type ListType } from './List' import ListMusicAdd, { type MusicAddModalType as ListMusicAddType } from '@/components/MusicAddModal' import ListMusicMultiAdd, { type MusicMultiAddModalType as ListAddMultiType } from '@/components/MusicMultiAddModal' @@ -139,6 +139,7 @@ export default () => { onPlay={info => { handlePlay(info.listId, info.index) }} onPlayLater={info => { hancelExitSelect(); handlePlayLater(info.listId, info.musicInfo, info.selectedList, hancelExitSelect) }} onRemove={info => { hancelExitSelect(); handleRemove(info.listId, info.musicInfo, info.selectedList, hancelExitSelect) }} + onDislikeMusic={info => { void handleDislikeMusic(info.musicInfo) }} onCopyName={info => { handleShare(info.musicInfo) }} onAdd={handleAddMusic} onMove={handleMoveMusic} diff --git a/src/screens/Home/Views/Mylist/MusicList/listAction.ts b/src/screens/Home/Views/Mylist/MusicList/listAction.ts index 4357baf..e3fdff1 100644 --- a/src/screens/Home/Views/Mylist/MusicList/listAction.ts +++ b/src/screens/Home/Views/Mylist/MusicList/listAction.ts @@ -1,9 +1,11 @@ import { removeListMusics, updateListMusicPosition } from '@/core/list' -import { playList } from '@/core/player/player' +import { playList, playNext } from '@/core/player/player' import { addTempPlayList } from '@/core/player/tempPlayList' import settingState from '@/store/setting/state' import { similar, sortInsert } from '@/utils' import { confirmDialog, shareMusic } from '@/utils/tools' +import { addDislikeInfo, hasDislike } from '@/core/dislikeList' +import playerState from '@/store/player/state' import type { SelectInfo } from './ListMenu' @@ -69,3 +71,9 @@ export const searchListMusic = (list: LX.Music.MusicInfo[], text: string) => { return sortedList.map(item => item.data).reverse() } +export const handleDislikeMusic = async(musicInfo: SelectInfo['musicInfo']) => { + await addDislikeInfo([{ name: musicInfo.name, singer: musicInfo.singer }]) + if (hasDislike(playerState.playMusicInfo.musicInfo)) { + void playNext(true) + } +} diff --git a/src/screens/Home/Views/Setting/settings/Other/DislikeEditModal.tsx b/src/screens/Home/Views/Setting/settings/Other/DislikeEditModal.tsx new file mode 100644 index 0000000..5254438 --- /dev/null +++ b/src/screens/Home/Views/Setting/settings/Other/DislikeEditModal.tsx @@ -0,0 +1,180 @@ +import { useRef, useImperativeHandle, forwardRef, useState, useCallback } from 'react' +import Text from '@/components/common/Text' +import { type LayoutChangeEvent, View } from 'react-native' +import Input, { type InputType } from '@/components/common/Input' +import { createStyle } from '@/utils/tools' +import { useTheme } from '@/store/theme/hook' +import { useI18n } from '@/lang' +import Dialog, { type DialogType } from '@/components/common/Dialog' +import Button from '@/components/common/Button' + +interface RuleInputType { + setText: (text: string) => void + getText: () => string + focus: () => void +} +const RuleInput = forwardRef((props, ref) => { + const theme = useTheme() + const t = useI18n() + const [text, setText] = useState('') + const inputRef = useRef(null) + const [height, setHeight] = useState(100) + + useImperativeHandle(ref, () => ({ + getText() { + return text.trim() + }, + setText(text) { + setText(text) + }, + focus() { + inputRef.current?.focus() + }, + })) + + const handleLayout = useCallback(({ nativeEvent }: LayoutChangeEvent) => { + setHeight(nativeEvent.layout.height) + }, []) + + return ( + + + + ) +}) + + +export interface DislikeEditModalProps { + onSave: (rules: string) => void + // onSourceChange: SourceSelectorProps['onSourceChange'] +} +export interface DislikeEditModalType { + show: (rules: string) => void +} + +export default forwardRef(({ onSave }, ref) => { + const dialogRef = useRef(null) + // const sourceSelectorRef = useRef(null) + const inputRef = useRef(null) + const [visible, setVisible] = useState(false) + const theme = useTheme() + const t = useI18n() + + const handleShow = (rules: string) => { + dialogRef.current?.setVisible(true) + requestAnimationFrame(() => { + inputRef.current?.setText(rules) + // sourceSelectorRef.current?.setSource(source) + // setTimeout(() => { + // inputRef.current?.focus() + // }, 300) + }) + } + useImperativeHandle(ref, () => ({ + show(rules) { + if (visible) handleShow(rules) + else { + setVisible(true) + requestAnimationFrame(() => { + handleShow(rules) + }) + } + }, + })) + + const handleCancel = () => { + dialogRef.current?.setVisible(false) + } + const handleConfirm = () => { + let rules = inputRef.current?.getText() ?? '' + handleCancel() + onSave(rules) + } + + return ( + visible + ? ( + + + + {t('setting_dislike_list_tips')} + + + + + + + ) : null + ) +}) + + +const styles = createStyle({ + content: { + flexGrow: 1, + flexShrink: 1, + paddingHorizontal: 15, + paddingTop: 15, + paddingBottom: 10, + flexDirection: 'column', + }, + col: { + flexDirection: 'row', + height: 38, + }, + // selector: { + // borderTopLeftRadius: 4, + // borderBottomLeftRadius: 4, + // }, + inputContent: { + flexGrow: 1, + flexShrink: 1, + // backgroundColor: 'rgba(0, 0, 0, 0.2)', + }, + input: { + minWidth: 290, + // borderRadius: 4, + // borderTopRightRadius: 4, + // borderBottomRightRadius: 4, + paddingTop: 5, + paddingBottom: 5, + }, + inputTipText: { + marginTop: 8, + // lineHeight: 18, + // backgroundColor: 'rgba(0, 0, 0, 0.2)', + }, + + btns: { + flexDirection: 'row', + justifyContent: 'center', + paddingBottom: 15, + paddingLeft: 15, + // paddingRight: 15, + }, + btn: { + flex: 1, + paddingTop: 8, + paddingBottom: 8, + paddingLeft: 10, + paddingRight: 10, + alignItems: 'center', + borderRadius: 4, + marginRight: 15, + }, +}) + + diff --git a/src/screens/Home/Views/Setting/settings/Other/DislikeList.tsx b/src/screens/Home/Views/Setting/settings/Other/DislikeList.tsx new file mode 100644 index 0000000..01bbcc9 --- /dev/null +++ b/src/screens/Home/Views/Setting/settings/Other/DislikeList.tsx @@ -0,0 +1,51 @@ +import { memo, useRef } from 'react' +import { StyleSheet, View } from 'react-native' + +// import { gzip, ungzip } from 'pako' + +import SubTitle from '../../components/SubTitle' +import Button from '../../components/Button' +import { toast } from '@/utils/tools' +import { useI18n } from '@/lang' +// import Text from '@/components/common/Text' +import { state } from '@/store/dislikeList' +import DislikeEditModal, { type DislikeEditModalType } from './DislikeEditModal' +import { overwirteDislikeInfo } from '@/core/dislikeList' + +export default memo(() => { + const t = useI18n() + const modalRef = useRef(null) + // const [ruleNum, setRuleNum] = useState(state.dislikeInfo.musicNames.size + state.dislikeInfo.singerNames.size + state.dislikeInfo.names.size) + + const handleShow = () => { + modalRef.current?.show(state.dislikeInfo.rules) + } + + const handleSave = async(rules: string) => { + if (state.dislikeInfo.rules.trim() == rules.trim()) return + await overwirteDislikeInfo(rules) + toast(t('setting__other_dislike_list_saved_tip')) + // setRuleNum(state.dislikeInfo.musicNames.size + state.dislikeInfo.singerNames.size + state.dislikeInfo.names.size) + } + + return ( + + {/* + {t('setting__other_dislike_list_label', { num: ruleNum })} + */} + + + + + + ) +}) + +const styles = StyleSheet.create({ + ruleNum: { + marginBottom: 5, + }, + btn: { + flexDirection: 'row', + }, +}) diff --git a/src/screens/Home/Views/Setting/settings/Other/index.tsx b/src/screens/Home/Views/Setting/settings/Other/index.tsx index 4609e63..ce45092 100644 --- a/src/screens/Home/Views/Setting/settings/Other/index.tsx +++ b/src/screens/Home/Views/Setting/settings/Other/index.tsx @@ -3,6 +3,7 @@ import { memo } from 'react' import Section from '../../components/Section' import ResourceCache from './ResourceCache' import MetaCache from './MetaCache' +import DislikeList from './DislikeList' import Log from './Log' // import MaxCache from './MaxCache' import { useI18n } from '@/lang' @@ -14,6 +15,7 @@ export default memo(() => {
+ {/* */}
diff --git a/src/store/dislikeList/action.ts b/src/store/dislikeList/action.ts new file mode 100644 index 0000000..7b7d397 --- /dev/null +++ b/src/store/dislikeList/action.ts @@ -0,0 +1,60 @@ +import { state } from './state' +import { SPLIT_CHAR } from '@/config/constant' + + +export const hasDislike = (info: LX.Music.MusicInfo | LX.Download.ListItem) => { + if ('progress' in info) info = info.metadata.musicInfo + const name = info.name?.replaceAll(SPLIT_CHAR.DISLIKE_NAME, SPLIT_CHAR.DISLIKE_NAME_ALIAS).toLocaleLowerCase().trim() ?? '' + const singer = info.singer?.replaceAll(SPLIT_CHAR.DISLIKE_NAME, SPLIT_CHAR.DISLIKE_NAME_ALIAS).toLocaleLowerCase().trim() ?? '' + + return state.dislikeInfo.musicNames.has(name) || state.dislikeInfo.singerNames.has(singer) || + state.dislikeInfo.names.has(`${name}${SPLIT_CHAR.DISLIKE_NAME}${singer}`) +} + +export const initDislikeInfo = (rules: string) => { + // state.dislikeInfo.names = names + // state.dislikeInfo.singerNames = singerNames + // state.dislikeInfo.musicNames = musicNames + state.dislikeInfo.rules = rules + initNameSet() +} + +const initNameSet = () => { + state.dislikeInfo.names.clear() + state.dislikeInfo.musicNames.clear() + state.dislikeInfo.singerNames.clear() + const list: string[] = [] + for (const item of state.dislikeInfo.rules.split('\n')) { + if (!item) continue + let [name, singer] = item.split(SPLIT_CHAR.DISLIKE_NAME) + if (name) { + name = name.replaceAll(SPLIT_CHAR.DISLIKE_NAME, SPLIT_CHAR.DISLIKE_NAME_ALIAS).toLocaleLowerCase().trim() + if (singer) { + singer = singer.replaceAll(SPLIT_CHAR.DISLIKE_NAME, SPLIT_CHAR.DISLIKE_NAME_ALIAS).toLocaleLowerCase().trim() + const rule = `${name}${SPLIT_CHAR.DISLIKE_NAME}${singer}` + state.dislikeInfo.names.add(rule) + list.push(rule) + } else { + state.dislikeInfo.musicNames.add(name) + list.push(name) + } + } else if (singer) { + singer = singer.replaceAll(SPLIT_CHAR.DISLIKE_NAME, SPLIT_CHAR.DISLIKE_NAME_ALIAS).toLocaleLowerCase().trim() + state.dislikeInfo.singerNames.add(singer) + list.push(`${SPLIT_CHAR.DISLIKE_NAME}${singer}`) + } + } + state.dislikeInfo.rules = Array.from(new Set(list)).join('\n') + '\n' +} + +// export const addDislikeInfo = (infos: LX.Dislike.DislikeMusicInfo[]) => { +// state.dislikeInfo.rules += '\n' + infos.map(info => `${info.name ?? ''}${SPLIT_CHAR.DISLIKE_NAME}${info.singer ?? ''}`).join('\n') +// initNameSet() +// return state.dislikeInfo.rules +// } + +export const overwirteDislikeInfo = (rules: string) => { + state.dislikeInfo.rules = rules + initNameSet() + return state.dislikeInfo.rules +} diff --git a/src/store/dislikeList/index.ts b/src/store/dislikeList/index.ts new file mode 100644 index 0000000..b71e70d --- /dev/null +++ b/src/store/dislikeList/index.ts @@ -0,0 +1,3 @@ + +export * as action from './action' +export * from './state' diff --git a/src/store/dislikeList/state.ts b/src/store/dislikeList/state.ts new file mode 100644 index 0000000..4dcf9c7 --- /dev/null +++ b/src/store/dislikeList/state.ts @@ -0,0 +1,19 @@ + +interface InitState { + dislikeInfo: LX.Dislike.DislikeInfo +} +const state: InitState = { + dislikeInfo: { + names: new Set(), + musicNames: new Set(), + singerNames: new Set(), + rules: '', + }, +} + + +export { + state, +} + + diff --git a/src/types/dislike_list.d.ts b/src/types/dislike_list.d.ts new file mode 100644 index 0000000..50361da --- /dev/null +++ b/src/types/dislike_list.d.ts @@ -0,0 +1,35 @@ + + +declare namespace LX { + namespace Dislike { + // interface ListItemMusicText { + // id?: string + // // type: 'music' + // name: string | null + // singer: string | null + // } + // interface ListItemMusic { + // id?: number + // type: 'musicId' + // musicId: string + // meta: LX.Music.MusicInfo + // } + // type ListItem = ListItemMusicText + // type ListItem = string + // type ListItem = ListItemMusic | ListItemMusicText + + interface DislikeMusicInfo { + name: string + singer: string + } + + interface DislikeInfo { + // musicIds: Set + names: Set + musicNames: Set + singerNames: Set + // list: LX.Dislike.ListItem[] + rules: string + } + } +} diff --git a/src/utils/data.ts b/src/utils/data.ts index cdd1469..ff98fb4 100644 --- a/src/utils/data.ts +++ b/src/utils/data.ts @@ -25,6 +25,7 @@ const syncAuthKeyPrefix = storageDataPrefix.syncAuthKey const syncHostPrefix = storageDataPrefix.syncHost const syncHostHistoryPrefix = storageDataPrefix.syncHostHistory const listPrefix = storageDataPrefix.list +const dislikeListPrefix = storageDataPrefix.dislikeList // const defaultListKey = listPrefix + 'default' // const loveListKey = listPrefix + 'love' @@ -363,6 +364,21 @@ export const clearOtherSource = async(keys?: string[]) => { await removeDataMultiple(keys) } +/** + * 获取不喜欢列表信息 + * @returns 不喜欢列表信息 + */ +export const getDislikeListRules = async() => { + return await getData(dislikeListPrefix) ?? '' +} +/** + * 保存列表信息 + * @param rules 规则信息 + */ +export const saveDislikeListRules = async(rules: string) => { + await saveData(dislikeListPrefix, rules) +} + // export const clearMusicUrlAndLyric = async() => { // let keys = (await getAllKeys()).filter(key => key.startsWith(storageDataPrefix.musicUrl) || key.startsWith(storageDataPrefix.lyric)) // await removeDataMultiple(keys)