mirror of
https://github.com/ikun0014/lx-music-mobile.git
synced 2025-07-02 18:32:10 +08:00
新增“不喜欢歌曲”功能
This commit is contained in:
parent
30765d551e
commit
050949f183
@ -13,6 +13,7 @@
|
||||
- 新增列表设置-是否显示歌曲专辑名,默认关闭
|
||||
- 新增列表设置-是否显示歌曲时长,默认开启
|
||||
- 新增是否允许通过歌词调整播放进度功能,默认关闭,可到播放详情页右上角设置开启
|
||||
- 新增“不喜欢歌曲”功能,可以在我的列表或者在线列表内歌曲的右击菜单使用,还可以去“设置-其他”手动编辑不喜欢规则,注:“上一曲”、“下一曲”功能将跳过符合“不喜欢歌曲”规则的歌曲,但你仍可以手动播放这些歌曲
|
||||
- 新增设置-播放设置-是否启用音频卸载,该设置之前默认是启用的,现在添加开关允许将其关闭,若出现播放器问题可尝试将其关闭
|
||||
|
||||
### 优化
|
||||
|
@ -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<ListMenuType, ListMenuProps>((props: ListMenuProps, re
|
||||
const [visible, setVisible] = useState(false)
|
||||
const menuRef = useRef<MenuType>(null)
|
||||
const selectInfoRef = useRef<SelectInfo>(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<ListMenuType, ListMenuProps>((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<ListMenuType, ListMenuProps>((props: ListMenuProps, re
|
||||
case 'copyName':
|
||||
props.onCopyName(selectInfo)
|
||||
break
|
||||
case 'dislike':
|
||||
props.onDislikeMusic(selectInfo)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
@ -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<OnlineListType, OnlineListProps>(({
|
||||
onPlayLater={info => { hancelExitSelect(); handlePlayLater(info.musicInfo, info.selectedList, hancelExitSelect) }}
|
||||
onCopyName={info => { handleShare(info.musicInfo) }}
|
||||
onAdd={handleAddMusic}
|
||||
onDislikeMusic={info => { void handleDislikeMusic(info.musicInfo) }}
|
||||
/>
|
||||
{/* <LoadingMask ref={loadingMaskRef} /> */}
|
||||
</View>
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<DialogType, DialogProps>(({
|
||||
closeBtn = true,
|
||||
title = '',
|
||||
children,
|
||||
height,
|
||||
}: DialogProps, ref) => {
|
||||
const theme = useTheme()
|
||||
const { keyboardShown, keyboardHeight } = useKeyboard()
|
||||
@ -98,7 +100,7 @@ export default forwardRef<DialogType, DialogProps>(({
|
||||
return (
|
||||
<Modal onHide={onHide} keyHide={keyHide} bgHide={bgHide} bgColor="rgba(50,50,50,.3)" ref={modalRef}>
|
||||
<View style={{ ...styles.centeredView, paddingBottom: keyboardShown ? keyboardHeight : 0 }}>
|
||||
<View style={{ ...styles.modalView, backgroundColor: theme['c-content-background'] }} onStartShouldSetResponder={() => true}>
|
||||
<View style={{ ...styles.modalView, height, backgroundColor: theme['c-content-background'] }} onStartShouldSetResponder={() => true}>
|
||||
<View style={{ ...styles.header, backgroundColor: theme['c-primary-light-100-alpha-100'] }}>
|
||||
<Text style={styles.title} size={13} color={theme['c-primary-light-1000']} numberOfLines={1}>{title}</Text>
|
||||
{closeBtnComponent}
|
||||
|
@ -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<InputType, InputProps>(({ onChangeText, onClearText, clearBtn, style, ...props }, ref) => {
|
||||
export default forwardRef<InputType, InputProps>(({ onChangeText, onClearText, clearBtn, style, size = 14, ...props }, ref) => {
|
||||
const inputRef = useRef<TextInput>(null)
|
||||
const theme = useTheme()
|
||||
// const scaleClearBtn = useRef(new Animated.Value(0)).current
|
||||
@ -113,7 +115,7 @@ export default forwardRef<InputType, InputProps>(({ 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} />
|
||||
|
@ -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
|
||||
|
28
src/core/dislikeList.ts
Normal file
28
src/core/dislikeList.ts
Normal file
@ -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)
|
||||
}
|
||||
|
@ -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)) // 初始化上次的歌曲播放信息
|
||||
|
@ -317,6 +317,7 @@ export const playNext = async(isAutoToggle = false): Promise<void> => {
|
||||
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<void> => {
|
||||
list: currentList,
|
||||
playedList,
|
||||
playerMusicInfo: currentList[playInfo.playerPlayIndex],
|
||||
isNext: false,
|
||||
})
|
||||
if (!filteredList.length) return handleToggleStop()
|
||||
|
||||
|
@ -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<LX.Dislike.DislikeInfo, 'rules'>
|
||||
|
||||
isNext: boolean
|
||||
}) => {
|
||||
let playerIndex = -1
|
||||
|
||||
let canPlayList: Array<LX.Music.MusicInfo | LX.Download.ListItem> = []
|
||||
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<LX.Music.MusicInfo | LX.Download.ListItem> = 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<LX.Music.MusicInfo | LX.Download.ListItem>
|
||||
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) {
|
||||
|
@ -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: <Name>@<Singer>\n3. Specify a song: <Name>\n4. Specify a certain singer:@<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",
|
||||
|
@ -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": "关闭",
|
||||
|
@ -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<ListMenuType, ListMenuProps>((props, ref) => {
|
||||
const [visible, setVisible] = useState(false)
|
||||
const menuRef = useRef<MenuType>(null)
|
||||
const selectInfoRef = useRef<SelectInfo>(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<ListMenuType, ListMenuProps>((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<ListMenuType, ListMenuProps>((props, ref) => {
|
||||
props.onChangePosition(selectInfo)
|
||||
// setVIsibleMusicPosition(true)
|
||||
break
|
||||
case 'dislike':
|
||||
props.onDislikeMusic(selectInfo)
|
||||
break
|
||||
case 'remove':
|
||||
props.onRemove(selectInfo)
|
||||
break
|
||||
|
@ -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}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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<RuleInputType, {}>((props, ref) => {
|
||||
const theme = useTheme()
|
||||
const t = useI18n()
|
||||
const [text, setText] = useState('')
|
||||
const inputRef = useRef<InputType>(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 (
|
||||
<View style={styles.inputContent} onLayout={handleLayout}>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
placeholder={t('songlist_open_input_placeholder')}
|
||||
value={text}
|
||||
onChangeText={setText}
|
||||
multiline
|
||||
textAlignVertical="top"
|
||||
size={12}
|
||||
style={{ ...styles.input, height, backgroundColor: theme['c-primary-input-background'] }}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
export interface DislikeEditModalProps {
|
||||
onSave: (rules: string) => void
|
||||
// onSourceChange: SourceSelectorProps['onSourceChange']
|
||||
}
|
||||
export interface DislikeEditModalType {
|
||||
show: (rules: string) => void
|
||||
}
|
||||
|
||||
export default forwardRef<DislikeEditModalType, DislikeEditModalProps>(({ onSave }, ref) => {
|
||||
const dialogRef = useRef<DialogType>(null)
|
||||
// const sourceSelectorRef = useRef<SourceSelectorType>(null)
|
||||
const inputRef = useRef<RuleInputType>(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
|
||||
? (
|
||||
<Dialog height='80%' ref={dialogRef} bgHide={false}>
|
||||
<View style={styles.content}>
|
||||
<RuleInput ref={inputRef} />
|
||||
<Text style={styles.inputTipText} size={12} color={theme['c-600']}>{t('setting_dislike_list_tips')}</Text>
|
||||
</View>
|
||||
<View style={styles.btns}>
|
||||
<Button style={{ ...styles.btn, backgroundColor: theme['c-button-background'] }} onPress={handleCancel}>
|
||||
<Text size={14} color={theme['c-button-font']}>{t('cancel')}</Text>
|
||||
</Button>
|
||||
<Button style={{ ...styles.btn, backgroundColor: theme['c-button-background'] }} onPress={handleConfirm}>
|
||||
<Text size={14} color={theme['c-button-font']}>{t('confirm')}</Text>
|
||||
</Button>
|
||||
</View>
|
||||
</Dialog>
|
||||
) : 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,
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -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<DislikeEditModalType>(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 (
|
||||
<SubTitle title={t('setting__other_dislike_list')}>
|
||||
{/* <View style={styles.ruleNum}>
|
||||
<Text>{t('setting__other_dislike_list_label', { num: ruleNum })}</Text>
|
||||
</View> */}
|
||||
<View style={styles.btn}>
|
||||
<Button onPress={handleShow}>{t('setting_other_dislike_list_show_btn')}</Button>
|
||||
</View>
|
||||
<DislikeEditModal ref={modalRef} onSave={handleSave} />
|
||||
</SubTitle>
|
||||
)
|
||||
})
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
ruleNum: {
|
||||
marginBottom: 5,
|
||||
},
|
||||
btn: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
})
|
@ -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(() => {
|
||||
<Section title={t('setting_other')}>
|
||||
<ResourceCache />
|
||||
<MetaCache />
|
||||
<DislikeList />
|
||||
<Log />
|
||||
{/* <MaxCache /> */}
|
||||
</Section>
|
||||
|
60
src/store/dislikeList/action.ts
Normal file
60
src/store/dislikeList/action.ts
Normal file
@ -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
|
||||
}
|
3
src/store/dislikeList/index.ts
Normal file
3
src/store/dislikeList/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
|
||||
export * as action from './action'
|
||||
export * from './state'
|
19
src/store/dislikeList/state.ts
Normal file
19
src/store/dislikeList/state.ts
Normal file
@ -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,
|
||||
}
|
||||
|
||||
|
35
src/types/dislike_list.d.ts
vendored
Normal file
35
src/types/dislike_list.d.ts
vendored
Normal file
@ -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<string>
|
||||
names: Set<string>
|
||||
musicNames: Set<string>
|
||||
singerNames: Set<string>
|
||||
// list: LX.Dislike.ListItem[]
|
||||
rules: string
|
||||
}
|
||||
}
|
||||
}
|
@ -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<string>(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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user