1import React from "react"; 2import { StyleSheet, View } from "react-native"; 3import rpx from "@/utils/rpx"; 4import ListItem from "@/components/base/listItem"; 5import ThemeText from "@/components/base/themeText"; 6import { ImgAsset } from "@/constants/assetsConst"; 7import Clipboard from "@react-native-clipboard/clipboard"; 8import { getMediaKey } from "@/utils/mediaItem"; 9import FastImage from "@/components/base/fastImage"; 10import Toast from "@/utils/toast"; 11import toast from "@/utils/toast"; 12 13import { useSafeAreaInsets } from "react-native-safe-area-context"; 14import PanelBase from "../base/panelBase"; 15import { FlatList } from "react-native-gesture-handler"; 16import Divider from "@/components/base/divider"; 17import { iconSizeConst } from "@/constants/uiConst"; 18import Config from "@/core/config.ts"; 19import mediaCache from "@/core/mediaCache"; 20import LyricManager from "@/core/lyricManager"; 21import { IIconName } from "@/components/base/icon.tsx"; 22import LyricUtil from "@/native/lyricUtil"; 23import { hidePanel } from "@/components/panels/usePanel.ts"; 24import { getDocumentAsync } from "expo-document-picker"; 25import { readAsStringAsync } from "expo-file-system"; 26import { checkAndCreateDir } from "@/utils/fileUtils.ts"; 27import pathConst from "@/constants/pathConst.ts"; 28import CryptoJs from "crypto-js"; 29import RNFS from "react-native-fs"; 30 31interface IMusicItemLyricOptionsProps { 32 /** 歌曲信息 */ 33 musicItem: IMusic.IMusicItem; 34} 35 36const ITEM_HEIGHT = rpx(96); 37 38interface IOption { 39 icon: IIconName; 40 title: string; 41 onPress?: () => void; 42 show?: boolean; 43} 44 45export default function MusicItemLyricOptions( 46 props: IMusicItemLyricOptionsProps, 47) { 48 const {musicItem} = props ?? {}; 49 50 const platformHash = CryptoJs.MD5(musicItem.platform).toString( 51 CryptoJs.enc.Hex, 52 ); 53 const idHash: string = CryptoJs.MD5(musicItem.id).toString( 54 CryptoJs.enc.Hex, 55 ); 56 57 const safeAreaInsets = useSafeAreaInsets(); 58 59 const options: IOption[] = [ 60 { 61 icon: 'identification', 62 title: `ID: ${getMediaKey(musicItem)}`, 63 onPress: () => { 64 mediaCache.setMediaCache(musicItem); 65 Clipboard.setString( 66 JSON.stringify( 67 { 68 platform: musicItem.platform, 69 id: musicItem.id, 70 }, 71 null, 72 '', 73 ), 74 ); 75 Toast.success('已复制到剪切板'); 76 }, 77 }, 78 { 79 icon: 'user', 80 title: `作者: ${musicItem.artist}`, 81 onPress: () => { 82 try { 83 Clipboard.setString(musicItem.artist.toString()); 84 Toast.success('已复制到剪切板'); 85 } catch { 86 Toast.warn('复制失败'); 87 } 88 }, 89 }, 90 { 91 icon: 'album-outline', 92 show: !!musicItem.album, 93 title: `专辑: ${musicItem.album}`, 94 onPress: () => { 95 try { 96 Clipboard.setString(musicItem.album.toString()); 97 Toast.success('已复制到剪切板'); 98 } catch { 99 Toast.warn('复制失败'); 100 } 101 }, 102 }, 103 { 104 icon: 'lyric', 105 title: `${ 106 Config.getConfig('lyric.showStatusBarLyric') ? '关闭' : '开启' 107 }桌面歌词`, 108 async onPress() { 109 const showStatusBarLyric = Config.getConfig('lyric.showStatusBarLyric'); 110 if (!showStatusBarLyric) { 111 const hasPermission = 112 await LyricUtil.checkSystemAlertPermission(); 113 114 if (hasPermission) { 115 const statusBarLyricConfig = { 116 topPercent: Config.getConfig("lyric.topPercent"), 117 leftPercent: Config.getConfig("lyric.leftPercent"), 118 align: Config.getConfig("lyric.align"), 119 color: Config.getConfig("lyric.color"), 120 backgroundColor: Config.getConfig("lyric.backgroundColor"), 121 widthPercent: Config.getConfig("lyric.widthPercent"), 122 fontSize: Config.getConfig("lyric.fontSize") 123 }; 124 LyricUtil.showStatusBarLyric( 125 "MusicFree", 126 statusBarLyricConfig ?? {} 127 ); 128 Config.setConfig('lyric.showStatusBarLyric', true); 129 } else { 130 LyricUtil.requestSystemAlertPermission().finally(() => { 131 Toast.warn('开启桌面歌词失败,无悬浮窗权限'); 132 }); 133 } 134 } else { 135 LyricUtil.hideStatusBarLyric(); 136 Config.setConfig('lyric.showStatusBarLyric', false); 137 } 138 hidePanel(); 139 }, 140 }, 141 { 142 icon: 'arrow-up-tray', 143 title: '上传本地歌词', 144 async onPress() { 145 try { 146 const result = await getDocumentAsync({ 147 copyToCacheDirectory: true, 148 }); 149 if (result.canceled) { 150 return; 151 } 152 const pickedDoc = result.assets[0].uri; 153 const lyricContent = await readAsStringAsync(pickedDoc, { 154 encoding: 'utf8', 155 }); 156 157 // 调用rnfs写到external storage 158 await checkAndCreateDir( 159 pathConst.localLrcPath + platformHash, 160 ); 161 162 await RNFS.writeFile( 163 pathConst.localLrcPath + 164 platformHash + 165 '/' + 166 idHash + 167 '.lrc', 168 lyricContent, 169 'utf8', 170 ); 171 toast.success('设置成功'); 172 LyricManager.refreshLyric(false, true); 173 hidePanel(); 174 } catch (e: any) { 175 console.log(e); 176 toast.warn('设置失败' + e.message); 177 } 178 }, 179 }, 180 { 181 icon: 'arrow-up-tray', 182 title: '上传本地歌词翻译', 183 async onPress() { 184 try { 185 const result = await getDocumentAsync({ 186 copyToCacheDirectory: true, 187 }); 188 if (result.canceled) { 189 return; 190 } 191 const pickedDoc = result.assets[0].uri; 192 const lyricContent = await readAsStringAsync(pickedDoc, { 193 encoding: 'utf8', 194 }); 195 196 // 调用rnfs写到external storage 197 await checkAndCreateDir( 198 pathConst.localLrcPath + platformHash, 199 ); 200 201 await RNFS.writeFile( 202 pathConst.localLrcPath + 203 platformHash + 204 '/' + 205 idHash + 206 '.tran.lrc', 207 lyricContent, 208 'utf8', 209 ); 210 toast.success('设置成功'); 211 LyricManager.refreshLyric(false, true); 212 hidePanel(); 213 } catch (e: any) { 214 console.log(e); 215 toast.warn('设置失败' + e.message); 216 } 217 }, 218 }, 219 { 220 icon: 'trash-outline', 221 title: '删除本地歌词', 222 async onPress() { 223 try { 224 const basePath = 225 pathConst.localLrcPath + platformHash + '/' + idHash; 226 227 await RNFS.unlink(basePath + '.lrc').catch(() => {}); 228 await RNFS.unlink(basePath + '.tran.lrc').catch(() => {}); 229 230 toast.success('删除成功'); 231 LyricManager.refreshLyric(false, true); 232 hidePanel(); 233 } catch (e: any) { 234 console.log(e); 235 toast.warn('删除失败' + e.message); 236 } 237 }, 238 }, 239 ]; 240 241 return ( 242 <PanelBase 243 renderBody={() => ( 244 <> 245 <View style={style.header}> 246 <FastImage 247 style={style.artwork} 248 uri={musicItem?.artwork} 249 emptySrc={ImgAsset.albumDefault} 250 /> 251 <View style={style.content}> 252 <ThemeText numberOfLines={2} style={style.title}> 253 {musicItem?.title} 254 </ThemeText> 255 <ThemeText 256 fontColor="textSecondary" 257 fontSize="description" 258 numberOfLines={2}> 259 {musicItem?.artist}{' '} 260 {musicItem?.album ? `- ${musicItem.album}` : ''} 261 </ThemeText> 262 </View> 263 </View> 264 <Divider /> 265 <View style={style.wrapper}> 266 <FlatList 267 data={options} 268 getItemLayout={(_, index) => ({ 269 length: ITEM_HEIGHT, 270 offset: ITEM_HEIGHT * index, 271 index, 272 })} 273 ListFooterComponent={<View style={style.footer} />} 274 style={[ 275 style.listWrapper, 276 { 277 marginBottom: safeAreaInsets.bottom, 278 }, 279 ]} 280 keyExtractor={_ => _.title} 281 renderItem={({item}) => 282 item.show !== false ? ( 283 <ListItem 284 withHorizontalPadding 285 heightType="small" 286 onPress={item.onPress}> 287 <ListItem.ListItemIcon 288 width={rpx(48)} 289 icon={item.icon} 290 iconSize={iconSizeConst.light} 291 /> 292 <ListItem.Content title={item.title} /> 293 </ListItem> 294 ) : null 295 } 296 /> 297 </View> 298 </> 299 )} 300 /> 301 ); 302} 303 304const style = StyleSheet.create({ 305 wrapper: { 306 width: rpx(750), 307 flex: 1, 308 }, 309 header: { 310 width: rpx(750), 311 height: rpx(200), 312 flexDirection: 'row', 313 padding: rpx(24), 314 }, 315 listWrapper: { 316 paddingTop: rpx(12), 317 }, 318 artwork: { 319 width: rpx(140), 320 height: rpx(140), 321 borderRadius: rpx(16), 322 }, 323 content: { 324 marginLeft: rpx(36), 325 width: rpx(526), 326 height: rpx(140), 327 justifyContent: 'space-around', 328 }, 329 title: { 330 paddingRight: rpx(24), 331 }, 332 footer: { 333 width: rpx(750), 334 height: rpx(30), 335 }, 336}); 337