新增“不喜欢歌曲”功能

This commit is contained in:
lyswhut 2023-09-05 12:14:57 +08:00
parent 30765d551e
commit 050949f183
24 changed files with 522 additions and 13 deletions

View File

@ -13,6 +13,7 @@
- 新增列表设置-是否显示歌曲专辑名,默认关闭
- 新增列表设置-是否显示歌曲时长,默认开启
- 新增是否允许通过歌词调整播放进度功能,默认关闭,可到播放详情页右上角设置开启
- 新增“不喜欢歌曲”功能,可以在我的列表或者在线列表内歌曲的右击菜单使用,还可以去“设置-其他”手动编辑不喜欢规则,注:“上一曲”、“下一曲”功能将跳过符合“不喜欢歌曲”规则的歌曲,但你仍可以手动播放这些歌曲
- 新增设置-播放设置-是否启用音频卸载,该设置之前默认是启用的,现在添加开关允许将其关闭,若出现播放器问题可尝试将其关闭
### 优化

View File

@ -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
}

View File

@ -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>

View File

@ -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)
}
}

View File

@ -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}

View File

@ -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} />

View File

@ -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
View 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)
}

View File

@ -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)) // 初始化上次的歌曲播放信息

View File

@ -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()

View File

@ -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) {

View File

@ -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",

View File

@ -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": "关闭",

View File

@ -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

View File

@ -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}

View File

@ -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)
}
}

View File

@ -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,
},
})

View File

@ -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',
},
})

View File

@ -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>

View 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
}

View File

@ -0,0 +1,3 @@
export * as action from './action'
export * from './state'

View 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
View 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
}
}
}

View File

@ -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)