| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326 |
- import { BreadcrumbItem, RemoteDriveGlobalSearchResult, RemoteDriveManager } from '../common/util/RemoteDriveManager';
- import { WebDavAccount } from '../viewmodel/WebDavAccount';
- import { Song } from '../viewmodel/Song';
- import { RemoteDriveManagerStates } from '../common/enums/RemoteDriveManagerStates';
- import Logger from '../common/util/Logger';
- import { SymbolGlyphModifier, router, curves } from '@kit.ArkUI';
- import { CommonConstants } from '../common/constants/CommonConstants';
- import { VideoItem } from '../viewmodel/VideoItem';
- import { GlobalContext } from '../common/util/GlobalContext';
- import { FileInfo } from '../viewmodel/FileInfo';
- import { emitter } from '@kit.BasicServicesKit';
- import { EventConstants } from '../common/constants/EventConstants';
- import { LazyDataSource } from '../common/util/LazyDataSource';
- import { ArrayUtil, FileUtil, MD5, PreferencesUtil, StrUtil, ToastUtil } from '@pura/harmony-utils';
- import { DialogHelper } from '@pura/harmony-dialog';
- import { ButtonFancyModifier,
- MenuModifier,
- ShadowModifier, SymbolGlyphFancyModifier } from '../common/util/AttributeModifierUtil';
- import { getRemoteDriveAccountLabel, getRemoteDriveDisplayLabel, getRemoteDrivePlaylistPrefix } from '../common/util/RemoteDriveLabel';
- import { RemoteDriveType } from '../common/enums/RemoteDriveType';
- import { Utility } from '../common/util/Utility';
- import { SettingPage } from './SettingPage';
- import { getCloudDiskIcon } from '../dialog/RemoteDriveAccountDialog';
- import { CreateFolderDialog } from '../dialog/CreateFolderDialog';
- import { UploadMusicPage } from './UploadMusicPage';
- import { FFmpeg } from '@sj/ffmpeg';
- import FileManager from '../common/util/FileManager';
- import { fileIo, fileUri, picker } from '@kit.CoreFileKit';
- import { setVideoUrlForSong } from '../common/util/RemotePlayerUtil';
- import { showAddToPlaylistDialog } from '../dialog/AddToPlaylistDialog';
- import { showCreatePlaylistDialog } from '../dialog/PlaylistDialog';
- import { Playlist } from '../viewmodel/Playlist';
- import PlaylistTable from '../common/util/PlaylistTable';
- import MediaTable from '../common/util/MediaTable';
- import { DownloadCenterManager } from '../common/util/DownloadCenterManager';
- import { DownloadCenter } from '../view/DownloadCenter';
- /**
- * 歌单播放事件数据
- */
- interface PlaylistEventData {
- playlistId: string;
- playlistName: string;
- songCount: number;
- startIndex: number;
- isJump: boolean;
- songFilePaths: string[];
- // 移除webDavAuthInfo,现在直接使用videoItems中的webdav_account_id
- }
- interface WebDavMetadataUpdatePayload {
- filePath: string;
- pixelMapPath?: string;
- name?: string;
- artist?: string;
- }
- interface RemoteThumbSource {
- playUrl: string;
- headers: Map<string, string>;
- }
- const TAG = 'heanup WebDavMainPage';
- const REMOTE_THUMB_CACHE_DIR: string = 'remote_thumbs';
- const REMOTE_THUMB_MAX_TASK_COUNT: number = 36;
- const REMOTE_THUMB_CAPTURE_SECONDS: string = '1.2';
- // WebDAV歌曲数据全局内存存储
- let globalWebdavVideoItems: VideoItem[] = [];
- let globalWebdavCurrentPlayIndex: number = 0;
- let globalRemoteThumbFfmpegQueue: Promise<void> = Promise.resolve();
- // 导出函数供LocalMusic访问
- export function getWebdavVideoItems(): VideoItem[] {
- return globalWebdavVideoItems;
- }
- export function getWebdavCurrentPlayIndex(): number {
- return globalWebdavCurrentPlayIndex;
- }
- export function clearWebdavVideoItems(): void {
- globalWebdavVideoItems = [];
- globalWebdavCurrentPlayIndex = 0;
- }
- // URL解码函数
- function decodeUrlEncodedString(encodedStr: string): string {
- try {
- return decodeURIComponent(encodedStr);
- } catch (error) {
- // 如果解码失败,返回原始字符串
- return encodedStr;
- }
- }
- @Preview
- @Entry
- @Component
- export struct WebDavMainPage {
- @State appName: string = ''
- @State isShowUploadFile: boolean = false
- @State isShowTitleBar: boolean = true //是否显示分类导航条
- private prevOffsetY: number = 0; // 记录上一次的Y轴偏移量
- @State autoHideTitle: boolean = true //滚动自动隐藏标题栏
- @State isCustomizeBg: boolean = false //自定义背景界面
- @StorageProp('isLandscape') isLandscape: boolean = false;
- @State blurValue: number = 0 //背景模糊
- @State bgBrightness: number = 0 //背景亮度
- @State customizeBgPath: string | undefined = '';
- private listScroller: ListScroller = new ListScroller()
- @StorageProp('bottomSafeHeight') bottomSafeHeight: number = 0;
- searchController: SearchController = new SearchController()
- private searchTicket: number = 0;
- @StorageProp('animationState') animationState: AnimationStatus= AnimationStatus.Running
- @State isNoJumpToHome: boolean = false //网盘播放不跳转首页
- @State isSearchMode: boolean = false
- @StorageLink('currentSong') currentSong: VideoItem | undefined = undefined;
- @StorageProp('topSafeHeight') topSafeHeight: number = 0;
- @State webdavManager: RemoteDriveManager = RemoteDriveManager.getInstance();
- @State accounts: WebDavAccount[] = [];
- @Prop @Watch('onSwitchAccount') selectedAccount: WebDavAccount;
- @State songs: VideoItem[] = [];
- @State dataSource:LazyDataSource<VideoItem> = new LazyDataSource(this.songs)
- @Link mType: number;
- @Link offsetX: number;
- @Link isShowDrawer: boolean;
- @State isLoading: boolean = false;
- @State webDavFiles: FileInfo[] = []; // 直接在页面中保存文件列表副本
- @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
- @StorageProp('isDarkMode') isDarkMode: boolean = false;
- @State breadcrumbs:BreadcrumbItem[] = []//面包屑导航
- @State visibleFoldersState: FileInfo[] = []; // 改为普通状态变量
- @State isShowFileName: boolean = false//是否显示文件名
- @State isLongNameRoLL: boolean = true//长歌名滚动
- @State sortType: number = 0 //默认排序方式
- @State listRefreshKey: number = 0 // 列表刷新标识
- @Consume isMultiSelect: boolean
- @State selectedSongs: VideoItem[] = []
- @State isAllSelected: boolean = false
- @State isDeletingSelection: boolean = false
- @State ignoreTapAfterExitMultiSelect: boolean = false
- @State isShowSongPropertySheet: boolean = false
- @State songForPropertySheet: VideoItem | undefined = undefined
- @State rootPath: string = ''
- @State isShowDownloadCenter: boolean = false
- @State downloadCenterTabIndex: number[] = [0]
- @State isSearchLoading: boolean = false
- private readonly downloadFolderName: string = '下载'
- private readonly metadataUpdateEvent: emitter.InnerEvent = { eventId: EventConstants.EVENT_WEBDAV_METADATA_UPDATED };
- private thumbnailTaskToken: number = 0;
- private thumbnailRunningKeys: Set<string> = new Set<string>();
- private readonly downloadCenterManager: DownloadCenterManager = DownloadCenterManager.getInstance();
- private async runSerializedThumbnailFfmpeg(task: () => Promise<void>): Promise<void> {
- const nextTask: Promise<void> = globalRemoteThumbFfmpegQueue.then(async (): Promise<void> => {
- await task();
- }, async (): Promise<void> => {
- await task();
- });
- globalRemoteThumbFfmpegQueue = nextTask.catch((): void => {});
- await nextTask;
- }
- async onSwitchAccount(){
- console.log('heanup 切换账户:', this.selectedAccount.name);
- this.thumbnailTaskToken += 1;
- this.thumbnailRunningKeys.clear();
- this.searchTicket++;
- this.searchText = '';
- this.filteredList = [];
- this.filteredFolderList = [];
- this.isSearchMode = false;
- this.isSearchLoading = false;
- this.songs = [];
- this.visibleFoldersState = [];
- this.updateListData(this.songs)
- this.exitMultiSelect();
- // 注意:不再需要清空全局上下文,因为LocalMusic已改用实例变量缓存
- // 当新账户加载时,新的认证信息会自动覆盖旧的
- Logger.info(TAG, 'heanup 切换账户,准备加载新账户文件');
-
- this.isLoading = true;
- await this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount)
- .catch((error: Error) => {
- Logger.error(TAG, '加载文件失败: ' + error.message);
- this.isLoading = false;
- });
- this.breadcrumbs = this.webdavManager.getBreadcrumbs();
- }
- updateListData(mList:Array<VideoItem>, noSort?: boolean){
- if (!noSort) {
- this.sortType = PreferencesUtil.getNumberSync('webDavSortType', 0)
- this.doSortType(this.sortType)
- return
- }
- this.refreshDisplaySongs(mList)
- }
- private refreshDisplaySongs(mList: Array<VideoItem>): void {
- this.dataSource.pushArrayData(mList)
- if(mList.length > 0){
- setTimeout(() => {
- this.listScroller.scrollToIndex(0)
- },200)
- }
- }
- private buildThumbnailIdentity(item: VideoItem): string {
- const accountId: string = item.webdav_account_id ? item.webdav_account_id : '';
- const relPath: string = item.remote_rel_path ? item.remote_rel_path : '';
- return `${item.type}:${accountId}:${relPath}:${item.filePath}`;
- }
- private async ensureThumbnailCacheDir(): Promise<string> {
- const context = getContext(this);
- const cacheDir: string = `${context.filesDir}/${REMOTE_THUMB_CACHE_DIR}`;
- await FileManager.createDir(cacheDir);
- return cacheDir;
- }
- private async resolveRemoteThumbPath(item: VideoItem): Promise<string> {
- const cacheDir: string = await this.ensureThumbnailCacheDir();
- const identity: string = this.buildThumbnailIdentity(item);
- const hashName: string = MD5.digestSync(identity);
- return `${cacheDir}/${hashName}.jpg`;
- }
- private buildFfmpegHeadersArg(headers: Map<string, string>): string {
- if (!headers || headers.size <= 0) {
- return '';
- }
- const lines: string[] = [];
- headers.forEach((value: string, key: string): void => {
- if (StrUtil.isEmpty(key) || StrUtil.isEmpty(value)) {
- return;
- }
- lines.push(`${key}: ${value}`);
- });
- if (lines.length <= 0) {
- return '';
- }
- return `${lines.join('\r\n')}\r\n`;
- }
- private buildRemoteThumbFfmpegCmd(playUrl: string, headersArg: string, outputPath: string): string[] {
- const commands: string[] = ['ffmpeg', '-y', '-ss', REMOTE_THUMB_CAPTURE_SECONDS];
- const networkArgs: string[] = this.buildRemoteFfmpegNetworkArgs(playUrl);
- for (let index: number = 0; index < networkArgs.length; index += 1) {
- commands.push(networkArgs[index]);
- }
- if (StrUtil.isNotEmpty(headersArg)) {
- commands.push('-headers', headersArg);
- }
- commands.push(
- '-i', playUrl,
- '-frames:v', '1',
- '-vf', 'scale=320:-2',
- '-q:v', '4',
- outputPath
- );
- return commands;
- }
- private buildRemoteAudioCoverFfmpegCmd(playUrl: string, headersArg: string, outputPath: string): string[] {
- const commands: string[] = ['ffmpeg', '-y'];
- const networkArgs: string[] = this.buildRemoteFfmpegNetworkArgs(playUrl);
- for (let index: number = 0; index < networkArgs.length; index += 1) {
- commands.push(networkArgs[index]);
- }
- if (StrUtil.isNotEmpty(headersArg)) {
- commands.push('-headers', headersArg);
- }
- commands.push(
- '-i', playUrl,
- '-map', '0:v:0',
- '-frames:v', '1',
- '-q:v', '4',
- outputPath
- );
- return commands;
- }
- private buildRemoteFfmpegNetworkArgs(playUrl: string): string[] {
- if (StrUtil.isEmpty(playUrl)) {
- return [];
- }
- const lowerPlayUrl: string = playUrl.toLowerCase();
- if (!lowerPlayUrl.startsWith('http://') && !lowerPlayUrl.startsWith('https://')) {
- return [];
- }
- return ['-rw_timeout', '15000000', '-probesize', '1048576', '-analyzeduration', '2000000'];
- }
- private sanitizeFfmpegCommands(commands: string[]): string[] {
- const safeCommands: string[] = [];
- for (let index: number = 0; index < commands.length; index++) {
- const value: string = commands[index];
- if (value === undefined || value === null) {
- Logger.warn(TAG, `缩略图命令存在空参数 index=${index}`);
- continue;
- }
- const text: string = `${value}`;
- if (text.length <= 0) {
- Logger.warn(TAG, `缩略图命令存在空字符串参数 index=${index}`);
- continue;
- }
- safeCommands.push(text);
- }
- return safeCommands;
- }
- private async executeSafeFfmpeg(commands: string[]): Promise<void> {
- // 兼容部分版本 @sj/ffmpeg 对第二个参数空值处理不安全的问题
- await FFmpeg.execute(commands, {});
- }
- private normalizeThumbImageSource(path: string): string {
- if (StrUtil.isEmpty(path)) {
- return '';
- }
- if (path.startsWith('http://') || path.startsWith('https://') || path.startsWith('file://')) {
- return path;
- }
- if (path.startsWith('/')) {
- return fileUri.getUriFromPath(path);
- }
- return path;
- }
- private normalizeThumbLocalPath(path: string): string {
- if (StrUtil.isEmpty(path)) {
- return '';
- }
- if (path.startsWith('file://')) {
- try {
- return new fileUri.FileUri(path).path;
- } catch (error) {
- const err: Error = error as Error;
- Logger.warn(TAG, `缩略图URI转路径失败 uri=${path}, error=${err.message}`);
- return '';
- }
- }
- return path;
- }
- private isThumbCacheValid(localPath: string): boolean {
- if (StrUtil.isEmpty(localPath)) {
- return false;
- }
- if (!FileUtil.accessSync(localPath)) {
- return false;
- }
- try {
- const stat: fileIo.Stat = fileIo.statSync(localPath);
- if (stat.size <= 1024) {
- Logger.warn(TAG, `缩略图缓存文件过小,判定无效 path=${localPath}, size=${stat.size}`);
- return false;
- }
- if (this.isImageMagicValid(localPath)) {
- return true;
- }
- Logger.warn(TAG, `缩略图缓存文件头无效,判定无效 path=${localPath}, size=${stat.size}`);
- } catch (error) {
- const err: Error = error as Error;
- Logger.warn(TAG, `缩略图缓存校验失败 path=${localPath}, error=${err.message}`);
- }
- return false;
- }
- private isImageMagicValid(localPath: string): boolean {
- let file: fileIo.File | undefined = undefined;
- try {
- file = fileIo.openSync(localPath, fileIo.OpenMode.READ_ONLY);
- const headerBuffer: ArrayBuffer = new ArrayBuffer(12);
- const readLen: number = fileIo.readSync(file.fd, headerBuffer);
- if (readLen < 4) {
- return false;
- }
- const bytes: Uint8Array = new Uint8Array(headerBuffer);
- const isJpg: boolean = bytes[0] === 0xFF && bytes[1] === 0xD8 && bytes[2] === 0xFF;
- const isPng: boolean = readLen >= 8 &&
- bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4E && bytes[3] === 0x47 &&
- bytes[4] === 0x0D && bytes[5] === 0x0A && bytes[6] === 0x1A && bytes[7] === 0x0A;
- const isGif: boolean = readLen >= 4 &&
- bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x38;
- const isBmp: boolean = bytes[0] === 0x42 && bytes[1] === 0x4D;
- const isWebp: boolean = readLen >= 12 &&
- bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46 &&
- bytes[8] === 0x57 && bytes[9] === 0x45 && bytes[10] === 0x42 && bytes[11] === 0x50;
- return isJpg || isPng || isGif || isBmp || isWebp;
- } catch (error) {
- const err: Error = error as Error;
- Logger.warn(TAG, `缩略图文件头校验失败 path=${localPath}, error=${err.message}`);
- return false;
- } finally {
- if (file !== undefined) {
- try {
- fileIo.closeSync(file);
- } catch (error) {
- const err: Error = error as Error;
- Logger.warn(TAG, `关闭缩略图文件失败 path=${localPath}, error=${err.message}`);
- }
- }
- }
- }
- private removeInvalidThumbCache(localPath: string): void {
- if (StrUtil.isEmpty(localPath) || !FileUtil.accessSync(localPath)) {
- return;
- }
- try {
- fileIo.unlinkSync(localPath);
- Logger.info(TAG, `已删除无效缩略图缓存 path=${localPath}`);
- } catch (error) {
- const err: Error = error as Error;
- Logger.warn(TAG, `删除无效缩略图缓存失败 path=${localPath}, error=${err.message}`);
- }
- }
- private resolveFileNameForTypeCheck(item: VideoItem): string {
- const rawName: string = item.fileName ?? item.remote_rel_path ?? item.filePath;
- if (StrUtil.isEmpty(rawName)) {
- return '';
- }
- const hashIndex = rawName.indexOf('#');
- const queryIndex = rawName.indexOf('?');
- let endIndex = rawName.length;
- if (queryIndex >= 0) {
- endIndex = queryIndex;
- }
- if (hashIndex >= 0 && hashIndex < endIndex) {
- endIndex = hashIndex;
- }
- return rawName.substring(0, endIndex);
- }
- private isVideoMediaItem(item: VideoItem): boolean {
- if (item.mimeType && item.mimeType.toLowerCase().startsWith('video/')) {
- return true;
- }
- const fileNameForCheck: string = this.resolveFileNameForTypeCheck(item);
- return Utility.isVideoByExtension(fileNameForCheck);
- }
- private refreshThumbItem(item: VideoItem): void {
- const dataIndex: number = this.dataSource.dataArray.indexOf(item);
- if (dataIndex >= 0) {
- this.dataSource.notifyDataChange(dataIndex);
- }
- this.listRefreshKey += 1;
- }
- private async prepareRemoteThumbSource(item: VideoItem): Promise<RemoteThumbSource> {
- let playUrl: string = item.filePath;
- if (StrUtil.isNotEmpty(playUrl)) {
- const lowerPath = playUrl.toLowerCase();
- const isDirectPath = lowerPath.startsWith('http://') || lowerPath.startsWith('https://') ||
- lowerPath.startsWith('/') || lowerPath.startsWith('file://');
- if (!isDirectPath) {
- try {
- playUrl = await setVideoUrlForSong(item, {
- extractCover: false,
- extractLyric: false,
- extractAudioInfo: false
- });
- } catch (error) {
- Logger.warn(TAG, `解析缩略图播放地址失败 name=${item.name}, error=${(error as Error).message}`);
- }
- }
- }
- const headers: Map<string, string> = await this.webdavManager.buildHttpHeadersWithAccountId(item, playUrl);
- const source: RemoteThumbSource = {
- playUrl: playUrl,
- headers: headers
- };
- return source;
- }
- private async tryResolveRemoteMusicCover(item: VideoItem, token: number): Promise<void> {
- const coverPath: string = await this.resolveRemoteThumbPath(item);
- if (this.isThumbCacheValid(coverPath)) {
- item.pixelMapPath = this.normalizeThumbImageSource(coverPath);
- this.refreshThumbItem(item);
- return;
- }
- this.removeInvalidThumbCache(coverPath);
- const source: RemoteThumbSource = await this.prepareRemoteThumbSource(item);
- if (StrUtil.isEmpty(source.playUrl)) {
- return;
- }
- const headersArg: string = this.buildFfmpegHeadersArg(source.headers);
- const rawCommands: string[] = this.buildRemoteAudioCoverFfmpegCmd(source.playUrl, headersArg, coverPath);
- const commands: string[] = this.sanitizeFfmpegCommands(rawCommands);
- if (commands.length < 2) {
- return;
- }
- await this.runSerializedThumbnailFfmpeg(async (): Promise<void> => {
- if (token !== this.thumbnailTaskToken) {
- return;
- }
- await this.executeSafeFfmpeg(commands);
- });
- if (token !== this.thumbnailTaskToken) {
- return;
- }
- if (this.isThumbCacheValid(coverPath)) {
- item.pixelMapPath = this.normalizeThumbImageSource(coverPath);
- this.refreshThumbItem(item);
- }
- }
- private async generateRemoteVideoThumb(item: VideoItem, token: number): Promise<void> {
- if (token !== this.thumbnailTaskToken) {
- return;
- }
- const identity: string = this.buildThumbnailIdentity(item);
- if (this.thumbnailRunningKeys.has(identity)) {
- return;
- }
- this.thumbnailRunningKeys.add(identity);
- try {
- const isVideoFile: boolean = this.isVideoMediaItem(item);
- if (!isVideoFile) {
- await this.tryResolveRemoteMusicCover(item, token);
- return;
- }
- const thumbPath: string = await this.resolveRemoteThumbPath(item);
- if (this.isThumbCacheValid(thumbPath)) {
- item.pixelMapPath = this.normalizeThumbImageSource(thumbPath);
- this.refreshThumbItem(item);
- return;
- }
- this.removeInvalidThumbCache(thumbPath);
- const source = await this.prepareRemoteThumbSource(item);
- if (StrUtil.isEmpty(source.playUrl)) {
- return;
- }
- const headersArg: string = this.buildFfmpegHeadersArg(source.headers);
- const rawCommands: string[] = this.buildRemoteThumbFfmpegCmd(source.playUrl, headersArg, thumbPath);
- const commands: string[] = this.sanitizeFfmpegCommands(rawCommands);
- if (commands.length < 2) {
- return;
- }
- await this.runSerializedThumbnailFfmpeg(async (): Promise<void> => {
- if (token !== this.thumbnailTaskToken) {
- return;
- }
- await this.executeSafeFfmpeg(commands);
- });
- if (token !== this.thumbnailTaskToken) {
- return;
- }
- if (this.isThumbCacheValid(thumbPath)) {
- item.pixelMapPath = this.normalizeThumbImageSource(thumbPath);
- this.refreshThumbItem(item);
- }
- } catch (error) {
- const err: Error = error as Error;
- Logger.info(TAG, `远程缩略图/封面生成失败 name=${item.name}, error=${err.message}`);
- } finally {
- this.thumbnailRunningKeys.delete(identity);
- }
- }
- private scheduleRemoteThumbPrefetch(): void {
- const token: number = this.thumbnailTaskToken + 1;
- this.thumbnailTaskToken = token;
- this.thumbnailRunningKeys.clear();
- const mediaItems: VideoItem[] = this.songs.slice();
- if (mediaItems.length <= 0) {
- return;
- }
- const limitCount: number = Math.min(mediaItems.length, REMOTE_THUMB_MAX_TASK_COUNT);
- const targetItems: VideoItem[] = mediaItems.slice(0, limitCount);
- setTimeout((): void => {
- void this.runRemoteThumbPrefetchQueue(targetItems, token);
- }, 120);
- }
- private async runRemoteThumbPrefetchQueue(items: VideoItem[], token: number): Promise<void> {
- for (let index: number = 0; index < items.length; index += 1) {
- if (token !== this.thumbnailTaskToken) {
- return;
- }
- const item: VideoItem = items[index];
- if (StrUtil.isNotEmpty(item.pixelMapPath)) {
- const currentThumbRef: string = item.pixelMapPath ? item.pixelMapPath : '';
- if (currentThumbRef.startsWith('http')) {
- continue;
- }
- const currentThumbPath: string = this.normalizeThumbLocalPath(currentThumbRef);
- if (StrUtil.isNotEmpty(currentThumbPath) && this.isThumbCacheValid(currentThumbPath)) {
- continue;
- }
- }
- await this.generateRemoteVideoThumb(item, token);
- }
- }
- // 更新可见文件夹列表
- private updateVisibleFolders(): void {
- try {
- // 安全检查webDavFiles
- if (!this.webDavFiles || !Array.isArray(this.webDavFiles)) {
- this.visibleFoldersState = [];
- return;
- }
- const allFolders = this.webDavFiles.filter(f => f.isDirectory);
- const isFlatFolderAccount = this.selectedAccount?.webType === RemoteDriveType.Navidrome
- || this.selectedAccount?.webType === RemoteDriveType.Jellyfin
- || this.selectedAccount?.webType === RemoteDriveType.Emby;
- if (isFlatFolderAccount) {
- const sortedNavFolders = allFolders.sort((a, b) => a.fileName.localeCompare(b.fileName));
- this.visibleFoldersState = sortedNavFolders;
- return;
- }
- const visible: FileInfo[] = [];
- for (let i = 0; i < allFolders.length; i++) {
- const folder = allFolders[i];
- // 安全检查folder对象
- if (!folder || typeof folder.fileName !== 'string') {
- continue;
- }
- let shouldShow = this.isDirectChildOfCurrentPath(folder);
- if (shouldShow) {
- visible.push(folder);
- }
- }
- console.log('更新文件夹列表:', visible);
- this.visibleFoldersState = visible;
- } catch (error) {
- Logger.error(TAG, '更新文件夹列表失败:', error.toString());
- this.visibleFoldersState = [];
- }
- this.isShowTitleBar = true
- }
- private isSongSelected(song: VideoItem): boolean {
- return this.selectedSongs.some(item => item.filePath === song.filePath);
- }
- private enterMultiSelect(song: VideoItem): void {
- if (!this.isMultiSelect) {
- this.isMultiSelect = true;
- this.selectedSongs = [song];
- this.isAllSelected = this.selectedSongs.length === this.songs.length && this.songs.length > 0;
- return;
- }
- if (!this.isSongSelected(song)) {
- this.toggleSongSelection(song);
- }
- }
- private toggleSongSelection(song: VideoItem): void {
- if (!this.isMultiSelect) {
- this.isMultiSelect = true;
- }
- const exists = this.isSongSelected(song);
- if (exists) {
- const next = this.selectedSongs.filter(item => item.filePath !== song.filePath);
- this.selectedSongs = next;
- this.isAllSelected = next.length > 0 && next.length === this.songs.length;
- if (next.length === 0) {
- this.exitMultiSelect();
- }
- } else {
- const next = [...this.selectedSongs, song];
- this.selectedSongs = next;
- this.isAllSelected = next.length === this.songs.length && this.songs.length > 0;
- }
- }
- private handleCheckboxSelection(song: VideoItem, checked: boolean): void {
- if (this.ignoreTapAfterExitMultiSelect) {
- return;
- }
- if (!this.isMultiSelect) {
- this.isMultiSelect = true;
- }
- const exists = this.isSongSelected(song);
- if (checked && !exists) {
- this.toggleSongSelection(song);
- } else if (!checked && exists) {
- this.toggleSongSelection(song);
- }
- }
- private toggleSelectAll(): void {
- if (this.isAllSelected) {
- this.selectedSongs = [];
- this.isAllSelected = false;
- return;
- }
- if (!this.isMultiSelect) {
- this.isMultiSelect = true;
- }
- this.selectedSongs = this.songs.slice();
- this.isAllSelected = this.selectedSongs.length > 0 && this.selectedSongs.length === this.songs.length;
- }
- private exitMultiSelect(): void {
- this.ignoreTapAfterExitMultiSelect = true;
- this.isMultiSelect = false;
- this.selectedSongs = [];
- this.isAllSelected = false;
- setTimeout(() => {
- this.isMultiSelect = false;
- this.ignoreTapAfterExitMultiSelect = false;
- }, 180);
- console.info('onecold this.isMultiSelect ' + this.isMultiSelect)
- }
- private syncSelectionAfterRefresh(): void {
- if (!this.isMultiSelect) {
- return;
- }
- const currentKeys: Set<string> = new Set(this.songs.map(item => item.filePath));
- const filtered = this.selectedSongs.filter(item => currentKeys.has(item.filePath));
- if (filtered.length !== this.selectedSongs.length) {
- this.selectedSongs = filtered;
- }
- this.isAllSelected = filtered.length > 0 && filtered.length === this.songs.length;
- if (filtered.length === 0) {
- this.exitMultiSelect();
- }
- }
- private confirmDeleteSelected(): void {
- if (!this.selectedAccount) {
- this.getUIContext().getPromptAction().showToast({ message: `请先选择${getRemoteDriveAccountLabel()}` });
- return;
- }
- if (!this.isMultiSelect || this.selectedSongs.length === 0) {
- this.getUIContext().getPromptAction().showToast({ message: '请先选择要删除的文件' });
- return;
- }
- this.getUIContext().showAlertDialog({
- title: '删除文件',
- message: `确定删除选中的${this.selectedSongs.length}个文件吗?`,
- primaryButton: {
- value: '取消',
- action: () => {}
- },
- secondaryButton: {
- value: '删除',
- fontColor: Color.Red,
- action: () => {
- void this.performDeleteSelected();
- }
- }
- });
- }
- private async performDeleteSelected(): Promise<void> {
- if (!this.selectedAccount || this.selectedSongs.length === 0) {
- return;
- }
- this.isDeletingSelection = true;
- try {
- await this.webdavManager.deleteRemoteSongs(this.selectedAccount, this.selectedSongs.slice());
- await this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount, this.webdavManager.currentPath);
- this.exitMultiSelect();
- this.getUIContext().getPromptAction().showToast({ message: '删除成功' });
- this.exitMultiSelect()
- } catch (error) {
- const err = error as Error;
- this.getUIContext().getPromptAction().showToast({ message: `删除失败: ${err.message}` });
- } finally {
- this.isDeletingSelection = false;
- }
- }
- private confirmDeleteSingleSong(song: VideoItem): void {
- if (!this.selectedAccount) {
- this.getUIContext().getPromptAction().showToast({ message: `请先选择${getRemoteDriveAccountLabel()}` });
- return;
- }
- this.getUIContext().showAlertDialog({
- title: '删除文件',
- message: `确定删除 "${song.name}" 吗?`,
- primaryButton: {
- value: '取消',
- action: () => {}
- },
- secondaryButton: {
- value: '删除',
- fontColor: Color.Red,
- action: () => {
- void this.performDeleteSingleSong(song);
- }
- }
- });
- }
- private async performDeleteSingleSong(song: VideoItem): Promise<void> {
- if (!this.selectedAccount) {
- return;
- }
- this.isDeletingSelection = true;
- try {
- await this.webdavManager.deleteRemoteSongs(this.selectedAccount, [song]);
- await this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount, this.webdavManager.currentPath);
- this.getUIContext().getPromptAction().showToast({ message: '删除成功' });
- } catch (error) {
- const err = error as Error;
- this.getUIContext().getPromptAction().showToast({ message: `删除失败: ${err.message}` });
- } finally {
- this.isDeletingSelection = false;
- }
- }
- private addSongToNextPlay(song: VideoItem): void {
- if (!song) {
- return;
- }
- const queue: VideoItem[] = globalWebdavVideoItems && globalWebdavVideoItems.length > 0 ?
- globalWebdavVideoItems : this.songs;
- if (!queue || queue.length === 0) {
- this.getUIContext().getPromptAction().showToast({ message: '当前列表为空' });
- return;
- }
- const currentPath = this.currentSong?.filePath;
- if (StrUtil.isEmpty(currentPath)) {
- const targetIndex = this.songs.findIndex((item: VideoItem) => item.filePath === song.filePath);
- if (targetIndex >= 0) {
- this.playSong(song, targetIndex, false);
- this.getUIContext().getPromptAction().showToast({ message: '已开始播放' });
- }
- return;
- }
- if (currentPath === song.filePath) {
- this.getUIContext().getPromptAction().showToast({ message: '当前正在播放这首歌' });
- return;
- }
- let currentIndex = queue.findIndex((item: VideoItem) => item.filePath === currentPath);
- if (currentIndex < 0 && globalWebdavCurrentPlayIndex >= 0 && globalWebdavCurrentPlayIndex < queue.length) {
- currentIndex = globalWebdavCurrentPlayIndex;
- }
- if (currentIndex < 0) {
- currentIndex = 0;
- }
- const existIndex = queue.findIndex((item: VideoItem) => item.filePath === song.filePath);
- if (existIndex >= 0) {
- const movedSong = queue.splice(existIndex, 1)[0];
- if (existIndex < currentIndex) {
- currentIndex -= 1;
- }
- queue.splice(currentIndex + 1, 0, movedSong);
- } else {
- queue.splice(currentIndex + 1, 0, song);
- }
- globalWebdavCurrentPlayIndex = currentIndex;
- globalWebdavVideoItems = queue;
- this.songs = [...queue];
- this.dataSource.pushArrayData(this.songs);
- const eventQueueRefresh: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAY_QUEUE_REFRESH };
- emitter.emit(eventQueueRefresh, { data: { source: 'webdav' } });
- this.getUIContext().getPromptAction().showToast({ message: '已添加到下一首播放' });
- }
- private cloneSongForDb(song: VideoItem): VideoItem {
- const dbSong = new VideoItem(
- song.name || '',
- song.id || song.filePath,
- song.filePath,
- song.type,
- song.videoSize || 0,
- song.cTime || Utility.getFormatDateStr(new Date(), 'yyyy-MM-dd HH:mm')
- );
- dbSong.parentPath = song.parentPath;
- dbSong.isFav = song.isFav;
- dbSong.size = song.size;
- dbSong.pixelMapPath = song.pixelMapPath;
- dbSong.artist = song.artist;
- dbSong.album = song.album;
- dbSong.fileName = song.fileName;
- dbSong.duration = song.duration;
- dbSong.mimeType = song.mimeType;
- dbSong.sampleRate = song.sampleRate;
- dbSong.trackCount = song.trackCount;
- dbSong.lastPlayedStr = song.lastPlayedStr;
- dbSong.playCount = song.playCount;
- dbSong.lyricContent = song.lyricContent;
- dbSong.md5Str = song.md5Str;
- dbSong.extra_json = song.extra_json;
- dbSong.pyStr = song.pyStr;
- dbSong.bit_rate = song.bit_rate;
- dbSong.probe_score = song.probe_score;
- dbSong.year = song.year;
- dbSong.nb_streams = song.nb_streams;
- dbSong.nb_programs = song.nb_programs;
- dbSong.genre = song.genre;
- dbSong.track = song.track;
- dbSong.bits_per_raw_sample = song.bits_per_raw_sample;
- dbSong.channels = song.channels;
- dbSong.channel_layout = song.channel_layout;
- dbSong.start_time = song.start_time;
- dbSong.ALBUMARTIST = song.ALBUMARTIST;
- dbSong.COMPOSER = song.COMPOSER;
- dbSong.LYRICIST = song.LYRICIST;
- dbSong.COMMENT = song.COMMENT;
- dbSong.disc = song.disc;
- dbSong.webdav_account_id = song.webdav_account_id;
- dbSong.remote_rel_path = song.remote_rel_path;
- dbSong.navArtistId = song.navArtistId;
- dbSong.navAlbumId = song.navAlbumId;
- dbSong.baiduFsId = song.baiduFsId;
- dbSong.webdav_id = song.webdav_id;
- dbSong.lyricIndex = song.lyricIndex;
- return dbSong;
- }
- private emitPlaylistRefresh(): void {
- emitter.emit({ eventId: EventConstants.EVENT_PLAYLIST_REFRESH }, {});
- }
- private buildPlaylistSongCandidates(songs: VideoItem[]): VideoItem[] {
- if (!songs || songs.length <= 0) {
- return [];
- }
- const result: VideoItem[] = [];
- const existedPaths: Set<string> = new Set<string>();
- for (let index: number = 0; index < songs.length; index += 1) {
- const song = songs[index];
- if (!song || StrUtil.isEmpty(song.filePath)) {
- continue;
- }
- if (existedPaths.has(song.filePath)) {
- continue;
- }
- existedPaths.add(song.filePath);
- result.push(song);
- }
- return result;
- }
- private async resolvePlaylistSongPaths(mediaTable: MediaTable, songs: VideoItem[]): Promise<string[]> {
- const paths: string[] = [];
- for (let index: number = 0; index < songs.length; index += 1) {
- const song: VideoItem = songs[index];
- const songForDb: VideoItem = this.cloneSongForDb(song);
- if (!songForDb.webdav_account_id && this.selectedAccount?.id) {
- songForDb.webdav_account_id = this.selectedAccount.id.toString();
- }
- if (StrUtil.isEmpty(songForDb.id)) {
- songForDb.id = songForDb.remote_rel_path || songForDb.filePath;
- }
- if (StrUtil.isEmpty(songForDb.remote_rel_path)) {
- songForDb.remote_rel_path = songForDb.id || songForDb.filePath;
- }
- if (StrUtil.isEmpty(songForDb.parentPath) && songForDb.filePath.includes('/')) {
- const pathIndex = songForDb.filePath.lastIndexOf('/');
- if (pathIndex > 0) {
- songForDb.parentPath = songForDb.filePath.substring(0, pathIndex);
- }
- }
- const saved: boolean = await mediaTable.saveOrUpdateWebDavItem(songForDb);
- if (!saved) {
- Logger.warn(TAG, `歌曲入库失败,跳过加入歌单: ${songForDb.filePath}`);
- continue;
- }
- const storedSong: VideoItem | null = await mediaTable.queryVideoByFilePath(songForDb.filePath);
- if (storedSong && StrUtil.isNotEmpty(storedSong.filePath)) {
- paths.push(storedSong.filePath);
- }
- }
- const uniquePaths: string[] = [];
- const pathSet: Set<string> = new Set<string>();
- for (let index: number = 0; index < paths.length; index += 1) {
- const path = paths[index];
- if (StrUtil.isEmpty(path) || pathSet.has(path)) {
- continue;
- }
- pathSet.add(path);
- uniquePaths.push(path);
- }
- return uniquePaths;
- }
- private async openSongsAddToPlaylistDialog(songs: VideoItem[]): Promise<void> {
- const candidateSongs: VideoItem[] = this.buildPlaylistSongCandidates(songs);
- if (candidateSongs.length <= 0) {
- this.getUIContext().getPromptAction().showToast({ message: '请选择要加入歌单的歌曲' });
- return;
- }
- try {
- const uiContext = this.getUIContext();
- const hostCtx = uiContext ? uiContext.getHostContext() : undefined;
- if (!hostCtx) {
- this.getUIContext().getPromptAction().showToast({ message: '无法获取上下文' });
- return;
- }
- const playlistTable: PlaylistTable = new PlaylistTable(hostCtx);
- const mediaTable: MediaTable = new MediaTable(hostCtx);
- await new Promise<void>((resolve) => {
- mediaTable.getRdbStore(hostCtx, () => resolve());
- });
- const playlistSongPaths: string[] = await this.resolvePlaylistSongPaths(mediaTable, candidateSongs);
- if (playlistSongPaths.length <= 0) {
- this.getUIContext().getPromptAction().showToast({ message: '歌曲入库失败,无法添加到歌单' });
- return;
- }
- const resolveSuccessToast = (): string => {
- if (playlistSongPaths.length > 1) {
- return `已添加${playlistSongPaths.length}首到歌单`;
- }
- return '已添加到歌单';
- };
- const openDialog = async (): Promise<void> => {
- const playlists: Playlist[] = await playlistTable.queryAllPlaylists();
- showAddToPlaylistDialog(
- candidateSongs,
- playlists,
- async (playlistId: string) => {
- const success = await playlistTable.addSongsToPlaylist(playlistId, playlistSongPaths);
- if (success) {
- this.emitPlaylistRefresh();
- }
- this.getUIContext().getPromptAction().showToast({ message: success ? resolveSuccessToast() : '歌曲已在该歌单中' });
- },
- () => {},
- () => {
- showCreatePlaylistDialog(
- async (name: string, description: string, coverPath?: string) => {
- const created = await playlistTable.createPlaylist(name, description, coverPath);
- if (!created) {
- this.getUIContext().getPromptAction().showToast({ message: '歌单创建失败' });
- return;
- }
- const target = await playlistTable.queryPlaylistByName(name);
- if (!target) {
- this.getUIContext().getPromptAction().showToast({ message: '未找到新建歌单' });
- return;
- }
- const addSuccess = await playlistTable.addSongsToPlaylist(target.id, playlistSongPaths);
- if (addSuccess) {
- this.emitPlaylistRefresh();
- }
- this.getUIContext().getPromptAction().showToast({ message: addSuccess ? resolveSuccessToast() : '歌曲已在该歌单中' });
- },
- () => {}
- );
- }
- );
- };
- await openDialog();
- } catch (error) {
- const err = error as Error;
- Logger.error(TAG, `打开添加到歌单失败: ${err.message}`);
- this.getUIContext().getPromptAction().showToast({ message: '添加到歌单失败' });
- }
- }
- private async openSelectedSongsAddToPlaylistDialog(): Promise<void> {
- if (!this.isMultiSelect || this.selectedSongs.length <= 0) {
- this.getUIContext().getPromptAction().showToast({ message: '请先选择歌曲' });
- return;
- }
- await this.openSongsAddToPlaylistDialog(this.selectedSongs.slice());
- }
- private async openSongAddToPlaylistDialog(song: VideoItem): Promise<void> {
- if (!song) {
- return;
- }
- await this.openSongsAddToPlaylistDialog([song]);
- }
- private sanitizeDownloadFileName(fileName: string): string {
- const trimmed = (fileName || '').trim();
- const sanitized = trimmed.replace(/[\\/:*?"<>|]/g, '_');
- return sanitized.length > 0 ? sanitized : '未知歌曲.mp3';
- }
- private resolveDownloadFileName(song: VideoItem, sourceUrlOrPath: string): string {
- if (StrUtil.isNotEmpty(song.fileName)) {
- return this.sanitizeDownloadFileName(song.fileName as string);
- }
- if (StrUtil.isNotEmpty(song.name) && (song.name as string).includes('.')) {
- return this.sanitizeDownloadFileName(song.name as string);
- }
- if (StrUtil.isNotEmpty(sourceUrlOrPath)) {
- const purePath = sourceUrlOrPath.split('?')[0];
- const lastSlash = purePath.lastIndexOf('/');
- if (lastSlash >= 0 && lastSlash < purePath.length - 1) {
- const urlName = purePath.substring(lastSlash + 1);
- if (StrUtil.isNotEmpty(urlName)) {
- return this.sanitizeDownloadFileName(urlName);
- }
- }
- }
- const baseName = StrUtil.isNotEmpty(song.name) ? song.name as string : '未知歌曲';
- return this.sanitizeDownloadFileName(`${baseName}.mp3`);
- }
- private buildUniqueDownloadPath(downloadDir: string, fileName: string): string {
- const safeName = this.sanitizeDownloadFileName(fileName);
- const dotIndex = safeName.lastIndexOf('.');
- const hasExt = dotIndex > 0;
- const namePart = hasExt ? safeName.substring(0, dotIndex) : safeName;
- const extPart = hasExt ? safeName.substring(dotIndex) : '';
- let targetPath = `${downloadDir}/${safeName}`;
- let suffix = 1;
- while (FileUtil.accessSync(targetPath)) {
- targetPath = `${downloadDir}/${namePart}(${suffix})${extPart}`;
- suffix += 1;
- }
- return targetPath;
- }
- private async pickDownloadRootPath(): Promise<string> {
- try {
- const documentViewPicker = new picker.DocumentViewPicker();
- const documentSaveResult = await documentViewPicker.save({ pickerMode: picker.DocumentPickerMode.DOWNLOAD });
- if (!documentSaveResult || documentSaveResult.length === 0) {
- return '';
- }
- const resolvedPath = new fileUri.FileUri(documentSaveResult[0]).path;
- this.rootPath = resolvedPath;
- return resolvedPath;
- } catch (error) {
- const err = error as Error;
- Logger.error(TAG, `选择下载目录失败: ${err.message}`);
- return '';
- }
- }
- private async ensureDownloadDirectory(): Promise<string> {
- let basePath = this.rootPath;
- if (StrUtil.isEmpty(basePath) || !FileUtil.accessSync(basePath)) {
- basePath = await this.pickDownloadRootPath();
- }
- if (StrUtil.isEmpty(basePath)) {
- return '';
- }
- const downloadDir = `${basePath}/${this.downloadFolderName}`;
- if (!FileUtil.accessSync(downloadDir)) {
- await fileIo.mkdir(downloadDir);
- }
- return downloadDir;
- }
- private async upsertDownloadedSongToLibrary(targetPath: string, sourceSong: VideoItem): Promise<void> {
- if (StrUtil.isEmpty(targetPath) || !FileUtil.accessSync(targetPath)) {
- return;
- }
- try {
- const pageContext = getContext(this);
- const mediaModule = await import('../common/util/MediaTable');
- const mediaTable = new mediaModule.default(pageContext);
- await new Promise<void>((resolve) => {
- mediaTable.getRdbStore(pageContext, () => resolve());
- });
- const exists = await mediaTable.isRecordExists(targetPath);
- if (exists) {
- return;
- }
- let mediaItem: VideoItem;
- try {
- mediaItem = await Utility.readMetaInfoFFmpeg(pageContext, targetPath, CommonConstants.TYPE_LOCAL, false);
- } catch (error) {
- Logger.warn(TAG, `下载歌曲读取元数据失败,使用兜底入库: ${(error as Error).message}`);
- mediaItem = new VideoItem(
- sourceSong.name || FileUtil.getFileName(targetPath),
- targetPath,
- targetPath,
- CommonConstants.TYPE_LOCAL,
- 0,
- Utility.getFormatDateStr(new Date(), 'yyyy-MM-dd HH:mm')
- );
- mediaItem.fileName = FileUtil.getFileName(targetPath);
- mediaItem.artist = sourceSong.artist;
- mediaItem.album = sourceSong.album;
- }
- mediaItem.id = targetPath;
- mediaItem.filePath = targetPath;
- mediaItem.type = CommonConstants.TYPE_LOCAL;
- if (StrUtil.isEmpty(mediaItem.fileName)) {
- mediaItem.fileName = FileUtil.getFileName(targetPath);
- }
- if (StrUtil.isEmpty(mediaItem.name)) {
- mediaItem.name = mediaItem.fileName as string;
- }
- if (StrUtil.isEmpty(mediaItem.parentPath)) {
- mediaItem.parentPath = FileUtil.getParentPath(targetPath);
- }
- await new Promise<void>((resolve) => {
- mediaTable.insert(mediaItem, () => resolve());
- });
- emitter.emit({ eventId: EventConstants.EVENT_SCAN_UPDATE }, {});
- } catch (error) {
- Logger.error(TAG, `下载歌曲入库失败: ${(error as Error).message}`);
- }
- }
- private openDownloadCenter(): void {
- this.isShowDownloadCenter = true;
- }
- private resolveSongDownloadSizeText(song: VideoItem): string {
- if (StrUtil.isNotEmpty(song.size)) {
- return decodeUrlEncodedString(song.size as string);
- }
- if (song.videoSize > 0) {
- return this.formatDownloadBytes(song.videoSize);
- }
- return '';
- }
- private parseSizeTextToBytes(sizeText: string): number {
- if (StrUtil.isEmpty(sizeText)) {
- return 0;
- }
- const normalized = sizeText.trim();
- const match = normalized.match(/([0-9]+(?:\.[0-9]+)?)\s*(B|KB|MB|GB|TB)/i);
- if (!match) {
- return 0;
- }
- const value = parseFloat(match[1]);
- if (!Number.isFinite(value) || value <= 0) {
- return 0;
- }
- const unit = match[2].toUpperCase();
- const factor = this.resolveUnitFactor(unit);
- return Math.floor(value * factor);
- }
- private resolveUnitFactor(unit: string): number {
- switch (unit) {
- case 'KB':
- return 1024;
- case 'MB':
- return 1024 * 1024;
- case 'GB':
- return 1024 * 1024 * 1024;
- case 'TB':
- return 1024 * 1024 * 1024 * 1024;
- case 'B':
- default:
- return 1;
- }
- }
- private formatDownloadBytes(bytes: number): string {
- if (!Number.isFinite(bytes) || bytes <= 0) {
- return '';
- }
- const units: string[] = ['B', 'KB', 'MB', 'GB', 'TB'];
- let size: number = bytes;
- let index: number = 0;
- while (size >= 1024 && index < units.length - 1) {
- size /= 1024;
- index += 1;
- }
- const precision: number = index === 0 ? 0 : 2;
- return `${size.toFixed(precision)} ${units[index]}`;
- }
- private pauseDownloadTask(taskId: string): void {
- if (StrUtil.isEmpty(taskId)) {
- return;
- }
- this.downloadCenterManager.pauseTask(taskId);
- }
- private resumeDownloadTask(taskId: string): void {
- if (StrUtil.isEmpty(taskId)) {
- return;
- }
- this.downloadCenterManager.resumeTask(taskId);
- }
- private async resolveSongDownloadSourceUrl(song: VideoItem): Promise<string> {
- if (song.type === CommonConstants.TYPE_BAIDU && song.webdav_account_id) {
- try {
- const account = await this.webdavManager.getWebDavAccountById(song.webdav_account_id);
- if (account) {
- const dlink = await this.webdavManager.getBaiduDownloadUrl(account, song);
- if (StrUtil.isNotEmpty(dlink)) {
- Logger.info(TAG, `下载中心使用百度直链下载: ${song.name}`);
- return dlink;
- }
- }
- } catch (error) {
- Logger.warn(TAG, `百度下载直链获取失败,回退播放地址: ${(error as Error).message}`);
- }
- }
- return setVideoUrlForSong(song, {
- context: getContext(this),
- autoParseMusicName: false,
- extractCover: false,
- extractLyric: false,
- extractAudioInfo: false
- });
- }
- private async buildDownloadRequestHeaders(song: VideoItem, sourceUrl: string): Promise<Map<string, string>> {
- if (song.type === CommonConstants.TYPE_BAIDU) {
- const headers = new Map<string, string>();
- // 参考 BaiduFileCache 下载链路,避免复用播放头导致 Range 行为异常
- headers.set('User-Agent', 'pan.baidu.com');
- headers.set('Accept', '*/*');
- headers.set('Connection', 'Keep-Alive');
- return headers;
- }
- return this.webdavManager.buildHttpHeadersWithAccountId(song, sourceUrl);
- }
- private async enqueueSongDownloadWithDirectory(song: VideoItem, downloadDir: string): Promise<string> {
- if (!song) {
- throw new Error('歌曲不存在');
- }
- if (StrUtil.isEmpty(downloadDir)) {
- throw new Error('未选择下载目录');
- }
- const sourceUrl = await this.resolveSongDownloadSourceUrl(song);
- const normalizedSource = sourceUrl.startsWith('file://') ? new fileUri.FileUri(sourceUrl).path : sourceUrl;
- const fileName = this.resolveDownloadFileName(song, normalizedSource);
- const targetPath = this.buildUniqueDownloadPath(downloadDir, fileName);
- const sizeText = this.resolveSongDownloadSizeText(song);
- const expectedBytes = this.parseSizeTextToBytes(sizeText);
- const requestHeaders = await this.buildDownloadRequestHeaders(song, sourceUrl);
- const coverPath = song.pixelMapPath ?? '';
- this.downloadCenterManager.enqueueDownload({
- title: song.name || fileName,
- fileName: fileName,
- coverPath: coverPath,
- sizeText: sizeText,
- expectedBytes: expectedBytes,
- sourceUrl: sourceUrl,
- targetPath: targetPath,
- downloadDir: downloadDir,
- headers: requestHeaders,
- onCompleted: async () => {
- await this.upsertDownloadedSongToLibrary(targetPath, song);
- }
- });
- return fileName;
- }
- private async enqueueSongDownload(song: VideoItem): Promise<void> {
- if (!song) {
- return;
- }
- try {
- const downloadDir = await this.ensureDownloadDirectory();
- if (StrUtil.isEmpty(downloadDir)) {
- this.getUIContext().getPromptAction().showToast({ message: '未选择下载目录' });
- return;
- }
- const fileName: string = await this.enqueueSongDownloadWithDirectory(song, downloadDir);
- this.openDownloadCenter();
- this.getUIContext().getPromptAction().showToast({ message: `已加入下载队列: ${fileName}` });
- } catch (error) {
- const err = error as Error;
- Logger.error(TAG, `加入下载队列失败: ${err.message}`);
- this.getUIContext().getPromptAction().showToast({ message: `下载任务创建失败: ${err.message}` });
- }
- }
- private async enqueueSelectedSongsDownload(): Promise<void> {
- if (!this.isMultiSelect || this.selectedSongs.length <= 0) {
- this.getUIContext().getPromptAction().showToast({ message: '请先选择要下载的歌曲' });
- return;
- }
- const candidates: VideoItem[] = this.buildPlaylistSongCandidates(this.selectedSongs.slice());
- if (candidates.length <= 0) {
- this.getUIContext().getPromptAction().showToast({ message: '没有可下载的歌曲' });
- return;
- }
- try {
- const downloadDir = await this.ensureDownloadDirectory();
- if (StrUtil.isEmpty(downloadDir)) {
- this.getUIContext().getPromptAction().showToast({ message: '未选择下载目录' });
- return;
- }
- let successCount: number = 0;
- let failedCount: number = 0;
- for (let index: number = 0; index < candidates.length; index += 1) {
- const song: VideoItem = candidates[index];
- try {
- await this.enqueueSongDownloadWithDirectory(song, downloadDir);
- successCount += 1;
- } catch (error) {
- failedCount += 1;
- const err = error as Error;
- Logger.error(TAG, `批量下载入队失败: ${(song.name || song.fileName || song.filePath)}, ${err.message}`);
- }
- }
- if (successCount > 0) {
- this.openDownloadCenter();
- this.exitMultiSelect();
- }
- if (successCount > 0 && failedCount <= 0) {
- this.getUIContext().getPromptAction().showToast({ message: `已加入下载队列: ${successCount}首` });
- return;
- }
- if (successCount > 0) {
- this.getUIContext().getPromptAction().showToast({ message: `已加入下载队列${successCount}首,失败${failedCount}首` });
- return;
- }
- this.getUIContext().getPromptAction().showToast({ message: '下载任务创建失败' });
- } catch (error) {
- const err = error as Error;
- Logger.error(TAG, `批量下载失败: ${err.message}`);
- this.getUIContext().getPromptAction().showToast({ message: `批量下载失败: ${err.message}` });
- }
- }
- private buildSongMetaLine(song: VideoItem): string {
- const parts: string[] = [];
- if (StrUtil.isNotEmpty(song.duration)) {
- parts.push(song.duration as string);
- } else if (StrUtil.isNotEmpty(song.size)) {
- parts.push(decodeUrlEncodedString(song.size as string));
- }
- if (parts.length === 0 && StrUtil.isNotEmpty(song.cTime)) {
- parts.push(song.cTime as string);
- }
- return parts.join(' · ');
- }
- // 对话框控制器
- private accountDialogController: CustomDialogController | null = null;
- // 保存事件处理器引用,用于取消订阅
- private eventHandler: (event: string) => void = (event: string) => {
- this.handleWebdavEvent(event);
- };
- initSetting(){
- this.isCustomizeBg = PreferencesUtil.getBooleanSync(SettingPage.IS_CUSTOMIZE_BG, false)
- this.customizeBgPath = PreferencesUtil.getStringSync(SettingPage.IS_CUSTOMIZE_BG_PATH, '')
- this.blurValue = PreferencesUtil.getNumberSync(SettingPage.CUSTOMIZE_BG_BLUR, 0)
- this.bgBrightness = PreferencesUtil.getNumberSync(SettingPage.BG_BRIGHTNESS, 0)
- this.isShowFileName = PreferencesUtil.getBooleanSync('isShowFileName', false)
- this.isLongNameRoLL = PreferencesUtil.getBooleanSync('isLongNameRoLL', true)
- this.sortType = PreferencesUtil.getNumberSync('webDavSortType', 0)
- this.isNoJumpToHome = PreferencesUtil.getBooleanSync('isNoJumpToHome', true)
- this.isShowTitleBar = PreferencesUtil.getBooleanSync(SettingPage.IS_SHOW_TITLTBAR, true)
- this.autoHideTitle = PreferencesUtil.getBooleanSync('autoHideTitle', true)
- }
- aboutToAppear(): void {
- Utility.getAppName(getContext(this)).then((appName: string) => {
- this.appName = appName
- })
- this.initSetting()
- let eventSetting: emitter.InnerEvent = { eventId: 333 }
- // 监听广播事件(通用设置配置更新)
- emitter.on(eventSetting, (eventData: emitter.EventData) => {
- this.initSetting()
- });
- // 加载账户列表
- this.loadAccounts();
- this.loadFiles()
- this.breadcrumbs = this.webdavManager.getBreadcrumbs();
- // 订阅WebDAV状态变化
- this.webdavManager.subscribe(this.eventHandler);
- this.subscribeWebDavMetadataUpdates();
- // 监听手势返回事件
- let eventBackSwipeBack: emitter.InnerEvent = { eventId: EventConstants.EVENT_SWIPE_BACK_DISK }
- emitter.on(eventBackSwipeBack, (eventData: emitter.EventData) => {
- console.info('onecold', ' 收到 EVENT_SWIPE_BACK_DISK 事件');
- // 如果在详情视图模式,退出详情视图
- this.goBack()
- });
- }
- aboutToDisappear(): void {
- // 取消订阅
- this.thumbnailTaskToken += 1;
- this.thumbnailRunningKeys.clear();
- this.webdavManager.unsubscribe(this.eventHandler);
- emitter.off(EventConstants.EVENT_WEBDAV_METADATA_UPDATED);
- }
- // 处理WebDAV事件
- private handleWebdavEvent(event: string): void {
- switch (event) {
- case RemoteDriveManagerStates.LoadFilesInfoSucceed:
- this.songs = this.webdavManager.webDavSongs;
- this.updateListData(this.songs)
- // 直接引用webdavManager的数组,避免@Observed序列化问题
- this.webDavFiles = this.webdavManager.webDavFiles;
- this.isLoading = false;
- // 更新可见文件夹列表
- this.updateVisibleFolders();
- this.breadcrumbs = this.webdavManager.getBreadcrumbs();
- this.syncSelectionAfterRefresh();
- this.scheduleRemoteThumbPrefetch();
- if (this.isSearchMode && this.searchText.length > 0) {
- void this.applyGlobalSearch(this.searchText, ++this.searchTicket);
- } else {
- this.restoreCurrentDirectorySearchView();
- }
- // promptAction.showToast({
- // message: '加载成功: ' + this.webDavFiles.filter(f => f.isDirectory).length + '个文件夹, ' + this.songs.length + '首歌曲'
- // });
- break;
- case RemoteDriveManagerStates.LoadFilesInfoFailed:
- this.isLoading = false;
- // this.getUIContext().getPromptAction().showToast({ message: '加载失败' });
- break;
- case RemoteDriveManagerStates.InsertAccountSucceed:
- case RemoteDriveManagerStates.EditAccountSucceed:
- case RemoteDriveManagerStates.RemoveAccountSucceed:
- this.loadAccounts();
- break;
- case RemoteDriveManagerStates.GlobalSearchIndexBuildStart:
- case RemoteDriveManagerStates.GlobalSearchIndexUpdated:
- case RemoteDriveManagerStates.GlobalSearchIndexReady:
- if (this.isSearchMode && this.searchText.length > 0) {
- void this.applyGlobalSearch(this.searchText, ++this.searchTicket);
- }
- break;
- case RemoteDriveManagerStates.GlobalSearchIndexFailed:
- this.isSearchLoading = false;
- break;
- }
- }
- private subscribeWebDavMetadataUpdates(): void {
- emitter.on(this.metadataUpdateEvent, (eventData: emitter.EventData) => {
- const payloads = eventData?.data as WebDavMetadataUpdatePayload[] | undefined;
- if (!payloads || payloads.length === 0) {
- Logger.info(TAG, 'WebDav metadata event received but payload empty');
- return;
- }
- Logger.info(TAG, `WebDav metadata event payload count: ${payloads.length}`);
- this.handleWebDavMetadataUpdates(payloads);
- });
- }
- private handleWebDavMetadataUpdates(payloads: WebDavMetadataUpdatePayload[]): void {
- if (!payloads || payloads.length === 0) {
- return;
- }
- Logger.info(TAG, 'handleWebDavMetadataUpdates start');
- let hasChanges = false;
- for (let i = 0; i < payloads.length; i++) {
- const payload = payloads[i];
- if (!payload || !payload.filePath) {
- continue;
- }
- Logger.info(TAG, `WebDav metadata detail path=${payload.pixelMapPath ?? 'null'}`);
- const songUpdated = this.updateSongMetadata(payload);
- if (songUpdated) {
- hasChanges = true;
- Logger.info(TAG, `metadata updated for ${payload.filePath}`);
- }
- }
- if (!hasChanges) {
- Logger.info(TAG, 'handleWebDavMetadataUpdates no changes detected');
- return;
- }
- // 更新dataSource的数据,不滚动,只触发刷新
- this.dataSource.pushArrayData(this.songs);
- this.listRefreshKey++;
- Logger.info(TAG, 'handleWebDavMetadataUpdates trigger refresh');
- }
- private updateSongMetadata(payload: WebDavMetadataUpdatePayload): boolean {
- if (!payload.filePath) {
- return false;
- }
- const targetIndex = this.findSongIndex(payload.filePath);
- if (targetIndex < 0) {
- return false;
- }
- const targetSong = this.songs[targetIndex];
- let mutated = false;
- if (payload.pixelMapPath && payload.pixelMapPath.length > 0) {
- targetSong.pixelMapPath = payload.pixelMapPath;
- mutated = true;
- }
- if (payload.name && payload.name.length > 0) {
- targetSong.name = payload.name;
- mutated = true;
- }
- if (payload.artist && payload.artist.length > 0) {
- targetSong.artist = payload.artist;
- mutated = true;
- }
- return mutated;
- }
- private findSongIndex(filePath: string): number {
- for (let i = 0; i < this.songs.length; i++) {
- if (this.songs[i].filePath === filePath) {
- return i;
- }
- }
- return -1;
- }
- private cloneSong(item: VideoItem): VideoItem {
- const clone = new VideoItem(
- item.name,
- item.id,
- item.filePath,
- item.type,
- item.videoSize,
- item.cTime,
- item.size,
- item.pixelMapPath,
- item.artist,
- item.album,
- item.fileName,
- item.lastPlayed
- );
- clone.duration = item.duration;
- clone.mimeType = item.mimeType;
- clone.trackCount = item.trackCount;
- clone.sampleRate = item.sampleRate;
- clone.size = item.size;
- clone.webdav_account_id = item.webdav_account_id;
- clone.remote_rel_path = item.remote_rel_path;
- clone.artist = item.artist;
- clone.album = item.album;
- clone.lyricContent = item.lyricContent;
- clone.pixelMapPath = item.pixelMapPath;
- clone.cTime = item.cTime;
- clone.fileName = item.fileName;
- clone.md5Str = item.md5Str;
- clone.bit_rate = item.bit_rate;
- clone.probe_score = item.probe_score;
- clone.year = item.year;
- clone.nb_streams = item.nb_streams;
- clone.nb_programs = item.nb_programs;
- clone.genre = item.genre;
- clone.track = item.track;
- clone.disc = item.disc;
- clone.channels = item.channels;
- clone.channel_layout = item.channel_layout;
- clone.start_time = item.start_time;
- clone.ALBUMARTIST = item.ALBUMARTIST;
- clone.COMPOSER = item.COMPOSER;
- clone.COMMENT = item.COMMENT;
- clone.LYRICIST = item.LYRICIST;
- clone.pyStr = item.pyStr;
- clone.extra_json = item.extra_json;
- clone.parentPath = item.parentPath;
- clone.isFav = item.isFav;
- clone.playCount = item.playCount;
- clone.lastPlayed = item.lastPlayed;
- clone.videoSize = item.videoSize;
- clone.isFav = item.isFav;
- return clone;
- }
- // 加载账户列表
- private loadAccounts(): void {
- this.accounts = this.webdavManager.getAllWebDavAccounts();
- }
- // 加载文件列表
- private loadFiles(): void {
- if (!this.selectedAccount) {
- this.getUIContext().getPromptAction().showToast({ message: '请先选择账户' });
- return;
- }
- this.isLoading = true;
- console.info('heanup '+this.selectedAccount.name+'type '+this.selectedAccount.webType)
- this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount)
- .catch((error: Error) => {
- Logger.error(TAG, '加载文件失败: ' + error.message);
- this.isLoading = false;
- });
- }
- // 进入文件夹
- private enterFolder(folder: FileInfo): void {
- this.isLoading = true;
- this.webdavManager.enterFolder(folder)
- .catch((error: Error) => {
- Logger.error(TAG, '进入文件夹失败: ' + error.message);
- this.isLoading = false;
- });
- }
- // 检查是否为当前目录的直接子项
- private isDirectChildOfCurrentPath(folder: FileInfo): boolean {
- const currentPath = this.webdavManager.currentPath || '';
- // 如果在根目录(currentPath为空或'/'),显示所有第一级文件夹
- if (currentPath === '' || currentPath === '/') {
- const folderPath = folder.href.replace(/\/$/, ''); // 去掉尾部斜杠
- return folder.href.startsWith('/') &&
- folder.href !== '/' &&
- !folderPath.substring(1).includes('/');
- }
- // 非根目录情况,计算相对路径
- let relativePath = folder.href;
- if (currentPath !== '/') {
- relativePath = folder.href.replace(currentPath, '');
- }
- relativePath = relativePath.replace(/^\//, '').replace(/\/$/, '');
- // 只有相对路径不为空且不包含/时才认为是直接子项
- return relativePath !== '' && !relativePath.includes('/');
- }
- // 返回上级目录
- private goBack(): void {
- if(this.webdavManager.canGoBack()){
- this.isLoading = true;
- this.webdavManager.goBack()
- .catch((error: Error) => {
- Logger.error(TAG, '返回失败: ' + error.message);
- this.isLoading = false;
- });
- this.breadcrumbs = this.webdavManager.getBreadcrumbs();
- }else{
- this.getUIContext().animateTo({ duration: 555 }, () => {
- // 动画闭包内控制Image组件的出现和消失
- this.isShowDrawer = !this.isShowDrawer
- this.offsetX = 0
- })
- }
- }
- // 切换账户
- private switchAccount(account: WebDavAccount): void {
- this.selectedAccount = account;
- this.songs = [];
- this.updateListData(this.songs)
- }
- private resolveSongPlayIndex(song: VideoItem, fallbackIndex: number): number {
- if (this.songs.length <= 0) {
- return -1;
- }
- if (song && StrUtil.isNotEmpty(song.filePath)) {
- for (let i = 0; i < this.songs.length; i++) {
- if (this.songs[i].filePath === song.filePath) {
- return i;
- }
- }
- }
- if (fallbackIndex >= 0 && fallbackIndex < this.songs.length) {
- return fallbackIndex;
- }
- return 0;
- }
- // 播放WebDAV歌曲
- private playSong(song: VideoItem, index: number,isJump:boolean=false): void {
- try {
- Logger.info(TAG, `heanup === WebDAV playSong 方法被调用 ===`);
- Logger.info(TAG, 'heanup 播放指定歌曲: ' + song.name + ', 索引: ' + index);
- Logger.info(TAG, 'heanup 歌曲列表长度: ' + this.songs.length);
- const playIndex = this.resolveSongPlayIndex(song, index);
- if (playIndex < 0 || playIndex >= this.songs.length) {
- this.getUIContext().getPromptAction().showToast({ message: '播放失败,未找到歌曲' });
- Logger.warn(TAG, `heanup 无法定位播放索引: fallback=${index}, songPath=${song.filePath}`);
- return;
- }
- if (playIndex !== index) {
- Logger.info(TAG, `heanup 修正播放索引: fallback=${index}, real=${playIndex}, song=${song.name}`);
- }
- // 检查歌曲是否有webdav_account_id
- Logger.info(TAG, `heanup 播放歌曲的webdav_account_id: ${song.webdav_account_id || '未设置'}`);
- // 确保所有歌曲都设置了正确的webdav_account_id
- if (this.selectedAccount && this.selectedAccount.id) {
- const accountIds = this.songs.map(s => s.webdav_account_id).filter(id => id);
- Logger.info(TAG, `heanup 当前歌曲列表中有 ${accountIds.length}/${this.songs.length} 首歌曲设置了webdav_account_id`);
- // 如果发现歌曲缺少webdav_account_id,立即设置
- this.songs.forEach((item, idx) => {
- if (!item.webdav_account_id) {
- item.webdav_account_id = this.selectedAccount!.id.toString();
- Logger.info(TAG, `heanup 为歌曲 "${item.name}" 设置webdav_account_id: ${item.webdav_account_id}`);
- }
- });
- }
- // 直接使用当前的VideoItem数组
- const videoItems: VideoItem[] = this.songs;
- const songFilePaths: string[] = [];
- for (let i = 0; i < this.songs.length; i++) {
- const item = this.songs[i];
- songFilePaths.push(item.filePath); // 使用filePath作为文件路径
- }
- // 直接通过事件传递videoItems数据,不使用GlobalContext
- const eventPlaylistPlay: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYLIST_PLAY };
- const playlistData: PlaylistEventData = {
- playlistId: 'webdav-playlist', // 使用特殊的ID标识网盘播放列表
- playlistName: `${getRemoteDriveDisplayLabel(this.selectedAccount?.webType)} - ${this.selectedAccount?.name || '未知账户'}`,
- songCount: this.songs.length,
- startIndex: playIndex,
- isJump: isJump,//设置true会弹出播放页
- songFilePaths: songFilePaths
- };
- // 保存videoItems到全局内存
- globalWebdavVideoItems = videoItems;
- globalWebdavCurrentPlayIndex = playIndex;
- Logger.info(TAG, `heanup 保存WebDAV歌曲到全局内存,长度: ${videoItems.length}, 索引: ${playIndex}`);
- // 发送播放请求事件,只传递索引信息
- emitter.emit(eventPlaylistPlay, { data: playlistData });
- Logger.info(TAG, `heanup 发送WebDAV播放事件,只传递索引: ${playIndex}`);
- if(!this.isNoJumpToHome){
- // 跳转到首页播放器
- this.getUIContext()?.animateTo({ duration: 555 }, () => {
- this.mType = 0
- })
- }
- } catch (error) {
- const err = error as Error;
- Logger.error(TAG, '播放歌曲失败: ' + err.message);
- this.getUIContext().getPromptAction().showToast({ message: '播放失败' });
- }
- }
- /**
- * 一键创建歌单:将当前WebDAV歌曲全部加入新歌单
- */
- private async createPlaylistFromCurrentWebDav(): Promise<void> {
- try {
- Logger.info(TAG, 'heanup 一键创建歌单开始');
- if (!this.selectedAccount || !this.selectedAccount.id) {
- this.getUIContext().getPromptAction().showToast({ message: `请先选择${getRemoteDriveAccountLabel()}` });
- return;
- }
- if (!this.songs || this.songs.length === 0) {
- // 回退到manager内的歌曲(可能还未复制到页面state)
- this.songs = this.webdavManager.webDavSongs;
- Logger.info(TAG, `heanup 页面songs为空,回退webdavManager.webDavSongs,长度=${this.songs.length}`);
- if (this.songs.length === 0) {
- this.getUIContext().getPromptAction().showToast({ message: '当前目录没有可添加的歌曲' });
- return;
- }
- }
- Logger.info(TAG, `heanup 歌单创建前歌曲数量: ${this.songs.length}`);
- // 确保每首歌都有webdav_account_id
- for (let i = 0; i < this.songs.length; i++) {
- if (!this.songs[i].webdav_account_id) {
- this.songs[i].webdav_account_id = this.selectedAccount.id.toString();
- }
- }
- // 构建歌单名称:账户名 + 当前路径(简化)
- const rawPath = this.webdavManager.currentPath || '/';
- const shortPath = rawPath === '/' ? '根目录' : rawPath.replace(/\/$/, '').split('/').slice(-1)[0];
- const playlistName = `${getRemoteDrivePlaylistPrefix(this.selectedAccount.webType)}-${this.selectedAccount.name}-${shortPath}`;
- // 创建歌单
- // 安全获取HostContext
- const uiContext = this.getUIContext();
- const hostCtx = uiContext ? uiContext.getHostContext() : undefined;
- if (!hostCtx) {
- this.getUIContext().getPromptAction().showToast({ message: '无法获取上下文' });
- return;
- }
- const playlistModule = await import('../common/util/PlaylistTable');
- const mediaModule = await import('../common/util/MediaTable');
- const playlistTable = new playlistModule.default(hostCtx);
- const mediaTable = new mediaModule.default(hostCtx);
- // 等待MediaTable底层RDB初始化完成
- await new Promise<void>((resolve) => {
- mediaTable.getRdbStore(hostCtx, () => {
- Logger.info(TAG, 'heanup mediaTable RDB 初始化完成');
- resolve();
- });
- });
- // 先将WebDAV歌曲入库(若不存在)
- let upsertSuccess = 0;
- for (let i = 0; i < this.songs.length; i++) {
- const v = this.songs[i];
- if (!v.id || v.id === '') {
- // 使用filePath作为唯一ID
- v.id = v.filePath;
- }
- if (!v.parentPath) {
- const idxp = v.filePath.lastIndexOf('/');
- if (idxp > 0) {
- v.parentPath = v.filePath.substring(0, idxp);
- }
- }
- const ok = await mediaTable.upsertWebDavVideoItem(v);
- Logger.info(TAG, `heanup upsert 第${i+1}/${this.songs.length}首: ${v.filePath} => ${ok}`);
- if (ok) {
- upsertSuccess++;
- }
- }
- Logger.info(TAG, `heanup WebDAV歌曲入库完成: 成功 ${upsertSuccess}/${this.songs.length}`);
- // 先查询是否已有同名歌单,避免重复创建导致混淆
- const existing = await playlistTable.queryPlaylistByName(playlistName);
- if (existing) {
- this.getUIContext().getPromptAction().showToast({ message: '歌单已存在,直接追加歌曲' });
- const filePathsExist: string[] = this.songs.map(s => s.filePath);
- await playlistTable.addSongsToPlaylist(existing.id, filePathsExist);
- router.pushUrl({ url: 'pages/PlaylistDetailPage', params: { playlist: existing } });
- return;
- }
- Logger.info(TAG, `heanup 准备创建歌单: ${playlistName}`);
- const created = await playlistTable.createPlaylist(playlistName, `来自${getRemoteDriveDisplayLabel(this.selectedAccount.webType)}: ${this.selectedAccount.name} 路径: ${rawPath}`);
- if (!created) {
- this.getUIContext().getPromptAction().showToast({ message: '歌单创建失败' });
- return;
- }
- // 查询刚创建的歌单ID
- const target = await playlistTable.queryPlaylistByName(playlistName);
- if (!target) {
- this.getUIContext().getPromptAction().showToast({ message: '无法找到新建歌单' });
- return;
- }
- // 批量添加歌曲
- const filePaths: string[] = this.songs.map(s => s.filePath);
- Logger.info(TAG, `heanup 开始批量添加歌曲到歌单: ${target.id}`);
- const addResult = await playlistTable.addSongsToPlaylist(target.id, filePaths);
- Logger.info(TAG, `heanup 批量添加结果: ${addResult}`);
- if (!addResult) {
- Logger.warn(TAG, '批量添加歌曲返回false,可能全部已存在或写入失败');
- }
- this.getUIContext().getPromptAction().showToast({ message: `歌单创建成功: ${playlistName}` });
- Logger.info(TAG, `heanup 一键创建歌单成功: ${playlistName}, 添加 ${filePaths.length} 首歌曲`);
- // 跳转到歌单详情页面
- router.pushUrl({ url: 'pages/PlaylistDetailPage', params: { playlist: target } });
- } catch (error) {
- Logger.error(TAG, '一键创建歌单失败: ' + (error as Error).message);
- this.getUIContext().getPromptAction().showToast({ message: '一键创建歌单失败' });
- }
- }
- // 导航到指定层级的面包屑路径
- private async navigateToBreadcrumb(crumb: BreadcrumbItem): Promise<void> {
- if (!crumb) {
- return;
- }
- try {
- this.isLoading = true;
- await this.webdavManager.enterFolderFromPath(crumb.path);
- } catch (error) {
- Logger.error(TAG, '导航到面包屑路径失败: ' + (error as Error).message);
- this.isLoading = false;
- }
- }
- @Builder
- MoreMenuBuilder() {
- Menu() {
- MenuItem({
- symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.music_note_list')),
- content: $r('app.string.onekey_add_playlist')
- })
- .onClick(async () => {
- this.createPlaylistFromCurrentWebDav();
- })
- MenuItem({
- symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.arrow_up_to_line')),
- content: $r('app.string.upload_music_file')
- })
- .onClick(async () => {
- // this.navigateToUploadPage();
- this.isShowUploadFile = !this.isShowUploadFile
- })
- MenuItem({
- symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.download')),
- content: '下载中心'
- })
- .onClick(() => {
- this.openDownloadCenter();
- })
- MenuItem({
- symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.folder_badge_plus')),
- content: $r('app.string.create_foder')
- })
- .onClick(async () => {
- this.showCreateFolderDialog();
- })
- }.attributeModifier(new MenuModifier())
- }
- @Builder
- SortMenuBuilder() {
- Menu() {
- MenuItem({
- symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.text_and_arrow_up')),
- content: $r('app.string.sort_by_name'),
- symbolEndIcon: this.sortType === 0 ? new SymbolGlyphModifier($r('sys.symbol.checkmark')) : undefined
- })
- .backgroundColor(this.getSortItemBackground(0))
- .onClick(async () => {
- this.doSortType(0)
- PreferencesUtil.put("webDavSortType", 0)
- })
- MenuItem({
- symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.text_and_arrow_down')),
- content: '按名称降序',
- symbolEndIcon: this.sortType === 1 ? new SymbolGlyphModifier($r('sys.symbol.checkmark')) : undefined
- })
- .backgroundColor(this.getSortItemBackground(1))
- .onClick(async () => {
- this.doSortType(1)
- PreferencesUtil.put("webDavSortType", 1)
- })
- MenuItem({
- symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.clock')),
- content:'按时间升序',
- symbolEndIcon: this.sortType === 2 ? new SymbolGlyphModifier($r('sys.symbol.checkmark')) : undefined
- })
- .backgroundColor(this.getSortItemBackground(2))
- .onClick(async () => {
- this.doSortType(2)
- PreferencesUtil.put("webDavSortType", 2)
- })
- MenuItem({
- symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.timer')),
- content: '按时间降序',
- symbolEndIcon: this.sortType === 3 ? new SymbolGlyphModifier($r('sys.symbol.checkmark')) : undefined
- })
- .backgroundColor(this.getSortItemBackground(3))
- .onClick(async () => {
- this.doSortType(3)
- PreferencesUtil.put("webDavSortType", 3)
- })
- MenuItem({
- symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.text_and_arrow_up')),
- content:'按大小升序',
- symbolEndIcon: this.sortType === 4 ? new SymbolGlyphModifier($r('sys.symbol.checkmark')) : undefined
- })
- .backgroundColor(this.getSortItemBackground(4))
- .onClick(async () => {
- this.doSortType(4)
- PreferencesUtil.put("webDavSortType", 4)
- })
- MenuItem({
- symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.text_and_arrow_down')),
- content: '按大小降序',
- symbolEndIcon: this.sortType === 5 ? new SymbolGlyphModifier($r('sys.symbol.checkmark')) : undefined
- })
- .backgroundColor(this.getSortItemBackground(5))
- .onClick(async () => {
- this.doSortType(5)
- PreferencesUtil.put("webDavSortType", 5)
- })
- }.attributeModifier(new MenuModifier())
- }
- private sortSongsForType(target: Array<VideoItem>, index: number): Array<VideoItem> {
- switch (index) {
- case 0:
- Utility.doSortListAscending(target, this.isShowFileName)
- break;
- case 1:
- Utility.doSortListDescending(target, this.isShowFileName)
- break;
- case 2:
- target.sort((a, b) => {
- return a.cTime.localeCompare(b.cTime);
- });
- break;
- case 3:
- target.sort((a, b) => {
- return b.cTime.localeCompare(a.cTime);
- });
- break;
- case 4:
- target.sort((a, b) => {
- return a.videoSize - b.videoSize;
- });
- break;
- case 5:
- target.sort((a, b) => {
- return b.videoSize - a.videoSize;
- });
- break;
- }
- return target;
- }
- private sortFoldersForType(target: Array<FileInfo>, index: number): Array<FileInfo> {
- switch (index) {
- case 0:
- target.sort((a, b) => {
- return a.fileName.localeCompare(b.fileName);
- });
- break;
- case 1:
- target.sort((a, b) => {
- return b.fileName.localeCompare(a.fileName);
- });
- break;
- case 2:
- target.sort((a, b) => {
- return a.time - b.time;
- });
- break;
- case 3:
- target.sort((a, b) => {
- return b.time - a.time;
- });
- break;
- }
- return target;
- }
- doSortType(index: number) {
- this.sortType = index;
- this.sortSongsForType(this.songs, index)
- this.sortFoldersForType(this.visibleFoldersState, index)
- if (this.filteredList.length > 0) {
- this.filteredList = this.sortSongsForType([...this.filteredList], index)
- }
- if (this.filteredFolderList.length > 0) {
- this.filteredFolderList = this.sortFoldersForType([...this.filteredFolderList], index)
- }
- if (this.isSearchMode && this.searchText.length > 0) {
- this.refreshDisplaySongs(this.filteredList)
- return
- }
- this.updateListData(this.songs,true)
- }
- private getSortItemBackground(sortType: number): ResourceColor {
- if (this.sortType !== sortType) {
- return Color.Transparent;
- }
- const isAsc: boolean = sortType % 2 === 0;
- if (isAsc) {
- return this.isDarkMode ? '#295B8A' : '#DCEEFF';
- }
- return this.isDarkMode ? '#7A4D22' : '#FFE9D5';
- }
- @Builder
- topTitleBar(){
- Column() {
- Row({ space: 15 }) {
- if (!this.isSearchMode &&!(this.webdavManager.canGoBack())) {
- //左侧滑动按钮
- Button({ type: ButtonType.Circle, stateEffect: true }) {
- SymbolGlyph($r('sys.symbol.sort'))
- .attributeModifier(new SymbolGlyphFancyModifier(25, '', ''))
- }
- .attributeModifier(new ButtonFancyModifier(40, 40))
- .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 })
- .animation({ duration: 300, curve: Curve.Ease })
- .onClick(() => {
- this.getUIContext().animateTo({ duration: 555 }, () => {
- // 动画闭包内控制Image组件的出现和消失
- this.isShowDrawer = !this.isShowDrawer
- this.offsetX = 0
- })
- })
- .attributeModifier(new ShadowModifier())
- .zIndex(0)
- }else{
- //左侧搜索返回按钮
- Button({ type: ButtonType.Circle, stateEffect: true }) {
- SymbolGlyph($r('sys.symbol.chevron_left'))
- .attributeModifier(new SymbolGlyphFancyModifier(24, '', ''))
- }
- .attributeModifier(new ButtonFancyModifier(40, 40))
- .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 })
- .animation({ duration: 300, curve: Curve.Ease })
- .onClick(() => {
- if(this.isSearchMode){
- this.isSearchMode = false
- this.searchController.stopEditing()
- this.onSearchInput('')
- }else{
- if(this.webdavManager.canGoBack()){
- this.goBack()
- }
- }
- })
- .attributeModifier(new ShadowModifier())
- .zIndex(0)
- }
- if(!this.isSearchMode){
- Text(this.selectedAccount.name)
- .margin({left:3,right:10})
- .fontColor($r('app.color.text_color'))
- .fontSize(19)
- .maxLines(1)
- .textOverflow({ overflow: TextOverflow.MARQUEE })//超长滚动
- .layoutWeight(1)
- .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 })
- }
- //搜索框
- Search({ controller: this.searchController,value: this.searchText, placeholder: '输入名称...' })
- .searchButton('搜索',{fontColor:this.themeColor})
- .searchIcon({
- src: $r('sys.media.ohos_ic_public_search_filled')
- })
- .cancelButton({
- style: CancelButtonStyle.CONSTANT,
- icon: {
- src: $r('sys.media.ohos_ic_public_cancel_filled')
- }
- })
- .layoutWeight(1)
- .height(35)
- .maxLength(20)
- .backgroundColor(this.isDarkMode?Color.Black:'#F5F5F5')
- .placeholderColor(Color.Grey)
- .placeholderFont({ size: 14, weight: 400 })
- .textFont({ size: 14, weight: 400 })
- .onSubmit((value: string) => {
- console.log('onecold onSubmit ='+value)
- this.searchController.stopEditing()
- this.onSearchInput(value);
- })
- .onChange((value: string) => {
- console.log('onecold onChange ='+value)
- this.onSearchInput(value);
- })
- .visibility(this.isSearchMode?Visibility.Visible:Visibility.None)
- .animation({ duration: 300, curve: Curve.Ease })
- //搜索按钮
- if (!this.isSearchMode) {
- Button({ type: ButtonType.Circle, stateEffect: true }) {
- SymbolGlyph($r('sys.symbol.magnifyingglass'))
- .attributeModifier(new SymbolGlyphFancyModifier(25, '', ''))
- }
- .attributeModifier(new ButtonFancyModifier(40, 40))
- .animation({ duration: 300, curve: Curve.Ease })
- .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 })
- .attributeModifier(new ShadowModifier())
- .zIndex(0)
- .onClick(()=>{
- this.isSearchMode = true
- this.restoreCurrentDirectorySearchView()
- void this.webdavManager.ensureGlobalSearchIndex(this.selectedAccount)
- })
- //排序按钮
- Button({ type: ButtonType.Circle, stateEffect: true }) {
- SymbolGlyph($r('sys.symbol.list_number'))
- .attributeModifier(new SymbolGlyphFancyModifier(25, '', ''))
- }
- .attributeModifier(new ButtonFancyModifier(40, 40))
- .animation({ duration: 300, curve: Curve.Ease })
- .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 })
- .bindMenu(this.SortMenuBuilder)
- .attributeModifier(new ShadowModifier())
- .zIndex(0)
- //添加/上传综合按钮
- Button({ type: ButtonType.Circle, stateEffect: true }) {
- SymbolGlyph($r('sys.symbol.plus'))
- .attributeModifier(new SymbolGlyphFancyModifier(25, '', ''))
- }
- .attributeModifier(new ButtonFancyModifier(40, 40))
- .animation({ duration: 300, curve: Curve.Ease })
- .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 })
- .bindMenu(this.MoreMenuBuilder)
- .attributeModifier(new ShadowModifier())
- .zIndex(0)
- .bindContentCover($$this.isShowUploadFile, this.UploadFielBuilder(), {
- transition: TransitionEffect.translate({ x: 500 }).animation({ curve: curves.springMotion(0.6, 0.8) })
- })
- }
- }
- }
- .padding({ top: this.topSafeHeight + 10, left: 10, right: 10, bottom: 2 })
- .width('100%')
- .bindContentCover($$this.isShowDownloadCenter, this.DownloadCenterBuilder(),
- {
- modalTransition:ModalTransition.DEFAULT,
- onWillDisappear: () => {
- this.isShowDownloadCenter = false
- },
- })
- }
- @Builder
- UploadFielBuilder() {
- Scroll() {
- Column() {
- UploadMusicPage({
- accountId: this.selectedAccount.id,
- isShowUploadFile:this.isShowUploadFile,
- onResult:(result:boolean)=>{
- console.info("heanup onResult,上传成功,刷新列表")
- if(result){
- // 上传文件成功后刷新当前路径的列表
- if(this.selectedAccount.webType==RemoteDriveType.Baidu){
- ToastUtil.showToast(`由于百度网盘限制,百度网盘的上传路径是apps/${this.appName}`)
- }
- this.refreshFileListAfterUpload()
- }
- },
- })
- }
- }
- .width('100%')
- .height('100%')
- }
- @Builder
- DownloadCenterBuilder() {
- DownloadCenter({
- themeColor: this.themeColor,
- isDarkMode: this.isDarkMode,
- appName: this.appName,
- topSafeHeight: this.topSafeHeight,
- bottomSafeHeight: this.bottomSafeHeight,
- selectedIndexes: $downloadCenterTabIndex,
- onPauseTask: (taskId: string) => {
- this.pauseDownloadTask(taskId);
- },
- onResumeTask: (taskId: string) => {
- this.resumeDownloadTask(taskId);
- },
- onClose: () => {
- this.isShowDownloadCenter = false;
- }
- })
- .width('100%')
- .height('100%')
- }
- //搜索功能的实现
- @State searchText: string = ''; // 用户输入内容
- @State filteredList: Array<VideoItem> = []; // 过滤后的歌曲结果
- @State filteredFolderList: Array<FileInfo> = []; // 过滤后的文件夹结果
- private restoreCurrentDirectorySearchView(): void {
- this.filteredList = this.sortSongsForType([...this.songs], this.sortType)
- this.filteredFolderList = this.sortFoldersForType([...this.visibleFoldersState], this.sortType)
- this.isSearchLoading = false
- this.refreshDisplaySongs(this.filteredList)
- }
- private shouldShowSearchLocation(): boolean {
- return this.isSearchMode && this.searchText.length > 0
- }
- private getSongSearchLocation(song: VideoItem): string {
- if (!this.shouldShowSearchLocation() || !this.selectedAccount) {
- return ''
- }
- return this.webdavManager.getSongSearchLocationLabel(this.selectedAccount, song)
- }
- private getFolderSearchLocation(folder: FileInfo): string {
- if (!this.shouldShowSearchLocation() || !this.selectedAccount) {
- return ''
- }
- return this.webdavManager.getFolderSearchLocationLabel(this.selectedAccount, folder)
- }
- private async applyGlobalSearch(keyword: string, ticket: number): Promise<void> {
- if (!this.selectedAccount) {
- if (ticket === this.searchTicket) {
- this.isSearchLoading = false
- this.filteredList = []
- this.filteredFolderList = []
- this.refreshDisplaySongs([])
- }
- return
- }
- try {
- const result: RemoteDriveGlobalSearchResult = await this.webdavManager.searchGlobalIndex(this.selectedAccount, keyword);
- if (ticket !== this.searchTicket) {
- return
- }
- this.filteredList = this.sortSongsForType([...result.songs], this.sortType)
- this.filteredFolderList = this.sortFoldersForType([...result.folders], this.sortType)
- this.isSearchLoading = result.isIndexing
- this.refreshDisplaySongs(this.filteredList)
- } catch (error) {
- if (ticket !== this.searchTicket) {
- return
- }
- this.isSearchLoading = false
- this.filteredList = []
- this.filteredFolderList = []
- this.refreshDisplaySongs([])
- ToastUtil.showToast(`搜索失败: ${(error as Error).message}`)
- }
- }
- private onSearchInput(value: string) {
- const keyword = value.trim();
- this.searchText = keyword;
- const ticket = ++this.searchTicket;
- if (keyword.length === 0) {
- this.restoreCurrentDirectorySearchView();
- return;
- }
- if (!this.isSearchMode) {
- this.isSearchMode = true;
- }
- this.isSearchLoading = true;
- void this.applyGlobalSearch(keyword, ticket);
- }
- private openSongPropertySheet(song: VideoItem): void {
- this.songForPropertySheet = song;
- this.isShowSongPropertySheet = true;
- }
- private getSongPropertyValue(value: string | number | undefined): string {
- if (value === undefined) {
- return '--';
- }
- if (typeof value === 'number') {
- return `${value}`;
- }
- if (StrUtil.isEmpty(value)) {
- return '--';
- }
- return value;
- }
- private getSongTypeLabel(song: VideoItem | undefined): string {
- if (!song) {
- return '--';
- }
- if (song.type === CommonConstants.TYPE_LOCAL) {
- return '本地';
- }
- if (song.type === CommonConstants.TYPE_WEBDAV) {
- return 'WebDAV';
- }
- if (song.type === CommonConstants.TYPE_JELLYFIN) {
- return 'Jellyfin';
- }
- if (song.type === CommonConstants.TYPE_EMBY) {
- return 'Emby';
- }
- if (song.type === CommonConstants.TYPE_AUDIOSTATION) {
- return 'AudioStation';
- }
- if (song.type === CommonConstants.TYPE_BAIDU) {
- return '百度网盘';
- }
- if (song.type === CommonConstants.TYPE_NAVIDROME) {
- return 'Navidrome';
- }
- if (song.type === CommonConstants.TYPE_PLEX) {
- return 'Plex';
- }
- return `${song.type}`;
- }
- @Builder
- private SongPropertyRow(label: string, value: string) {
- Column({ space: 4 }) {
- Text(label)
- .fontSize(12)
- .fontColor($r('app.color.index_tab_font_color'))
- .opacity(0.7)
- Text(value)
- .fontSize(14)
- .fontColor($r('app.color.text_color'))
- .maxLines(6)
- .textOverflow({ overflow: TextOverflow.Ellipsis })
- }
- .width('100%')
- .alignItems(HorizontalAlign.Start)
- .padding({ left: 2, right: 2, top: 4, bottom: 4 })
- }
- @Builder
- private SongPropertyCoverRow(label: string, coverPath: string | undefined) {
- Column({ space: 8 }) {
- Text(label)
- .fontSize(12)
- .fontColor($r('app.color.index_tab_font_color'))
- .opacity(0.7)
- if (StrUtil.isNotEmpty(coverPath)) {
- Image(coverPath as string)
- .width(136)
- .height(136)
- .borderRadius(10)
- .sourceSize({ width: 136, height: 136 })
- .objectFit(ImageFit.Cover)
- .alt($r('app.media.alt'))
- .fillColor(this.themeColor)
- } else {
- Text('--')
- .fontSize(14)
- .fontColor($r('app.color.text_color'))
- }
- }
- .width('100%')
- .alignItems(HorizontalAlign.Start)
- .padding({ left: 2, right: 2, top: 4, bottom: 4 })
- }
- @Builder
- private SongPropertyLyricRow(label: string, lyricContent: string | undefined) {
- Column({ space: 4 }) {
- Text(label)
- .fontSize(12)
- .fontColor($r('app.color.index_tab_font_color'))
- .opacity(0.7)
- Text(this.getSongPropertyValue(lyricContent))
- .fontSize(14)
- .fontColor($r('app.color.text_color'))
- .textAlign(TextAlign.Start)
- }
- .width('100%')
- .alignItems(HorizontalAlign.Start)
- .padding({ left: 2, right: 2, top: 4, bottom: 4 })
- }
- @Builder
- private SongPropertySheetBuilder() {
- if (this.songForPropertySheet) {
- Scroll() {
- Column({ space: 10 }) {
- this.SongPropertyCoverRow('音乐封面', this.songForPropertySheet?.pixelMapPath)
- this.SongPropertyRow('歌曲名称', this.getSongPropertyValue(this.songForPropertySheet?.name))
- this.SongPropertyRow('文件名称', this.getSongPropertyValue(this.songForPropertySheet?.fileName))
- this.SongPropertyRow('艺术家', this.getSongPropertyValue(this.songForPropertySheet?.artist))
- this.SongPropertyRow('专辑', this.getSongPropertyValue(this.songForPropertySheet?.album))
- this.SongPropertyRow('时长', this.getSongPropertyValue(this.songForPropertySheet?.duration))
- this.SongPropertyRow('文件大小', this.getSongPropertyValue(this.songForPropertySheet?.size))
- this.SongPropertyRow('比特率', this.getSongPropertyValue(this.songForPropertySheet?.bit_rate))
- this.SongPropertyRow('采样率', this.getSongPropertyValue(this.songForPropertySheet?.sampleRate))
- this.SongPropertyLyricRow('内嵌歌词', this.songForPropertySheet?.lyricContent)
- this.SongPropertyRow('创建时间', this.getSongPropertyValue(this.songForPropertySheet?.cTime))
- this.SongPropertyRow('来源类型', this.getSongTypeLabel(this.songForPropertySheet))
- this.SongPropertyRow('文件路径', this.getSongPropertyValue(this.songForPropertySheet?.filePath))
- }
- .width('100%')
- .padding({ left: 12, right: 12, top: 8, bottom: 12 })
- }
- .width('100%')
- .height('100%')
- .scrollBar(BarState.Auto)
- } else {
- Column() {
- Text('暂无歌曲属性信息')
- .fontSize(14)
- .fontColor($r('app.color.text_color'))
- .opacity(0.65)
- }
- .width('100%')
- .height('100%')
- .justifyContent(FlexAlign.Center)
- .alignItems(HorizontalAlign.Center)
- }
- }
- build() {
- Stack() {
- if (this.accounts.length === 0) {
- this.buildEmptyView();
- } else {
- this.buildContentView();
- }
- // 顶部区域:标题栏在上,面包屑在下
- Column() {
- this.topTitleBar()
- this.breaker()
- }
- .width('100%')
- .visibility(this.isShowTitleBar?Visibility.Visible:
- this.autoHideTitle?Visibility.None:Visibility.Visible)
- .animation({
- duration: 500,
- curve: Curve.Friction // 可选动画曲线
- })
- .position({ top: 0, left: 0 })
- // .backgroundColor($r('app.color.start_window_background'))
- // 多选操作条,位于播放条之上(仅在多选模式下显示)
- if (this.isMultiSelect || this.isDeletingSelection) {
- this.buildSelectionOverlay()
- }
- }
- .width('100%')
- .height('100%')
- .alignContent(Alignment.Bottom)
- .backgroundImage(this.isCustomizeBg?this.customizeBgPath:$r('app.color.start_window_background'))
- .backgroundImageSize(this.isLandscape?{width:'100%'}:{ height: '100%'})
- .backgroundImagePosition(Alignment.Center)
- .backdropBlur(this.blurValue)
- .backgroundBrightness({rate:this.isCustomizeBg?0.1:0,lightUpDegree:this.bgBrightness})
- .bindSheet($$this.isShowSongPropertySheet, this.SongPropertySheetBuilder(), {
- height: '99%',
- dragBar: true,
- showClose: true,
- preferType: SheetType.CENTER,
- blurStyle: BlurStyle.Thin,
- title: { title: '歌曲属性' }
- })
- .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
- }
- @Builder
- breaker() {
- // 面包屑导航
- Column({ space: 8 }) {
- // 面包屑导航
- if (this.webdavManager.currentPath !== '') {
- Row({ space: 8 }) {
- Button({ type: ButtonType.Circle }) {
- // Image(this.webdavManager.canGoBack()?$r('app.media.back'):
- // this.selectedAccount.coverPath?this.selectedAccount.coverPath:getCloudDiskIcon(this.selectedAccount.webType))
- Image(this.selectedAccount.coverPath)
- .width(15)
- .height(15)
- .borderRadius(10)
- .alt($r('app.media.cloudDisk'))
- .fillColor(Color.White)
- }
- .width(20)
- .height(20)
- .margin({left:5})
- .backgroundColor(this.themeColor)
- .onClick(() => this.goBack())
- Row({ space: 4 }) {
- ForEach(this.breadcrumbs, (crumb: BreadcrumbItem, index: number) => {
- Row() {
- Text(crumb.label)
- .fontSize(15)
- .fontColor($r('app.color.text_color'))
- .maxLines(1)
- .textOverflow({ overflow: TextOverflow.Ellipsis })
- }
- .onClick(() => {
- this.navigateToBreadcrumb(crumb);
- })
- // 添加分隔符(除了最后一个元素)
- if (index < this.breadcrumbs.length - 1) {
- Text('/')
- .fontSize(15)
- .fontColor($r('app.color.index_tab_font_color'))
- .opacity(0.6)
- }
- })
- }
- .layoutWeight(1)
- }
- .width('100%')
- .padding({ left: 4, right: 4 })
- }
- }
- .width('100%')
- .padding({ left: 12, right: 12, top: 8, bottom: 5 })
- }
- // 空状态视图
- @Builder
- buildEmptyView() {
- Column({ space: 20 }) {
- Image($r('app.media.cloudDisk'))
- .width(120)
- .height(120)
- .opacity(0.3)
- Text(`暂无${getRemoteDriveAccountLabel()}`)
- .fontSize(16)
- .fontColor($r('app.color.index_tab_font_color'))
- .opacity(0.6)
- }
- .justifyContent(FlexAlign.Center)
- .width('100%')
- .height('100%')
- }
- // 内容视图
- @Builder
- buildContentView() {
- Column() {
- // 加载状态
- Row() {
- LoadingProgress()
- .width(30)
- .height(30)
- .color(this.themeColor)
- Text('加载中...')
- .fontSize(14)
- .fontColor($r('app.color.index_tab_font_color'))
- .margin({ left: 12 })
- }
- .padding({top: this.topSafeHeight + 90, left: 20, right: 20})
- .visibility(this.isLoading?Visibility.Visible:Visibility.None)
- .opacity(this.isLoading ? 1 : 0)
- .animation({
- duration: 800,
- curve: Curve.Smooth // 可选动画曲线
- })
- if (this.isSearchMode && this.searchText.length > 0 && this.isSearchLoading) {
- Row({ space: 8 }) {
- LoadingProgress()
- .width(18)
- .height(18)
- .color(this.themeColor)
- Text('正在构建全局索引,结果会持续补全')
- .fontSize(12)
- .fontColor($r('app.color.index_tab_font_color'))
- .opacity(0.65)
- }
- .padding({ left: 20, right: 20, top: this.topSafeHeight + 74, bottom: 6 })
- }
- // 文件列表(文件夹 + 歌曲)
- if (this.webDavFiles.length > 0 || this.songs.length > 0) {
- List({ scroller: this.listScroller ,space: 0 }) {
- // 显示文件夹 - 只显示当前目录下的直接子文件夹
- ForEach(this.isSearchMode ? this.filteredFolderList : this.visibleFoldersState, (folder: FileInfo) => {
- ListItem() {
- this.buildFolderItem(folder)
- }
- .clickEffect({ level: ClickEffectLevel.MIDDLE })
- }, (folder: FileInfo) => folder.name+folder.fileName)
- // 显示歌曲
- LazyForEach(this.dataSource, (song: VideoItem, index: number) => {
- ListItem() {
- this.buildSongItem(song, index)
- }
- .clickEffect({ level: ClickEffectLevel.MIDDLE })
- }, (item: VideoItem, index: number) => item.filePath + '_' + index+this.listRefreshKey)
- }
- .scrollBar(BarState.Off)
- .onScrollFrameBegin((offset: number) => {
- // 获取当前滚动偏移量
- if(this.autoHideTitle){
- const currentOffsetY = this.listScroller.currentOffset().yOffset;
- // 判断滚动方向
- if (currentOffsetY > this.prevOffsetY) {
- this.isShowTitleBar = false
- } else if (currentOffsetY < this.prevOffsetY) {
- this.isShowTitleBar = true
- }
- // 更新前一次偏移量
- this.prevOffsetY = currentOffsetY;
- }
- return { offsetRemain: offset };
- })
- .contentStartOffset(this.topSafeHeight + 85)
- .contentEndOffset(this.bottomSafeHeight+70)
- .layoutWeight(1)
- .margin({ top: 4 })
- } else if (!this.isLoading) {
- Column() {
- Text(this.isSearchMode && this.searchText.length > 0 ? '没有找到匹配结果' : '暂无内容')
- .fontSize(14)
- .fontColor($r('app.color.index_tab_font_color'))
- .opacity(0.6)
- Text(this.isSearchMode && this.searchText.length > 0 ?
- (this.isSearchLoading ? '正在继续扫描更多目录...' : '可以换个关键词再试') :
- '点击"左侧菜单"网盘加载')
- .fontSize(12)
- .fontColor($r('app.color.index_tab_font_color'))
- .opacity(0.4)
- .margin({ top: 8 })
- }
- .justifyContent(FlexAlign.Center)
- .layoutWeight(1)
- }
- }
- .layoutWeight(1)
- }
- @Builder
- private buildMultiSelectBar() {
- Row({ space: 12 }) {
- Button(this.isAllSelected ? '反选' : '全选', { type: ButtonType.Circle, stateEffect: true })
- .width(55)
- .height(55)
- .fontSize(13)
- .backgroundColor(this.themeColor)
- .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 })
- .onClick(() => this.toggleSelectAll())
- Button('删除', { type: ButtonType.Circle, stateEffect: true })
- .width(55)
- .height(55)
- .fontSize(13)
- .backgroundColor(this.themeColor)
- .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 })
- .enabled(this.selectedSongs.length > 0 && !this.isDeletingSelection)
- .onClick(() => this.confirmDeleteSelected())
- Button('+歌单', { type: ButtonType.Circle, stateEffect: true })
- .width(55)
- .height(55)
- .fontSize(13)
- .backgroundColor(this.themeColor)
- .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 })
- .enabled(this.selectedSongs.length > 0 && !this.isDeletingSelection)
- .onClick(() => {
- void this.openSelectedSongsAddToPlaylistDialog();
- })
- Button('下载', { type: ButtonType.Circle, stateEffect: true })
- .width(55)
- .height(55)
- .fontSize(13)
- .backgroundColor(this.themeColor)
- .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 })
- .enabled(this.selectedSongs.length > 0 && !this.isDeletingSelection)
- .onClick(() => {
- void this.enqueueSelectedSongsDownload();
- })
- Button('取消', { type: ButtonType.Circle, stateEffect: true })
- .width(55)
- .height(55)
- .fontSize(13)
- .backgroundColor(this.themeColor)
- .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 })
- .onClick(() => this.exitMultiSelect())
- }
- .width('100%')
- .padding({ bottom: 10, top: 12 })
- .justifyContent(FlexAlign.Center)
- .visibility(this.isMultiSelect ? Visibility.Visible : Visibility.None)
- .opacity(this.isMultiSelect ? 1 : 0)
- .animation({ duration: 300, curve: 'ease-in-out' })
- }
- @Builder
- private buildSelectionOverlay() {
- Column() {
- Row({ space: 10 }) {
- LoadingProgress()
- .width(22)
- .height(22)
- .color(this.themeColor)
- Text('正在删除...')
- .fontSize(14)
- .fontColor($r('app.color.index_tab_font_color'))
- }
- .padding({ top: 12, bottom: 6 })
- .justifyContent(FlexAlign.Center)
- .visibility(this.isDeletingSelection ? Visibility.Visible : Visibility.None)
- this.buildMultiSelectBar()
- }
- .width('100%')
- .padding({ left: 12, right: 12, bottom: this.bottomSafeHeight })
- }
- // 文件夹列表项
- @Builder
- buildFolderItem(folder: FileInfo) {
- Button({ type: ButtonType.Capsule, stateEffect: true }) {
- Row({ space: 2 }) {
- SymbolGlyph($r('sys.symbol.folder'))
- .fontSize(48)
- .fontColor([this.themeColor])
- .alignSelf(ItemAlign.Center)
- .margin({ left: 8, right: 6 })
- // 文件夹信息
- Column({ space: 4 }) {
- Text(decodeUrlEncodedString(folder.fileName.replace('/', '')))
- .fontSize(15)
- .fontColor($r('app.color.index_tab_font_color'))
- .maxLines(1)
- .textOverflow({ overflow: TextOverflow.Ellipsis })
- Text(this.getFolderSearchLocation(folder))
- .fontSize(12)
- .fontColor($r('app.color.index_tab_font_color'))
- .opacity(0.48)
- .maxLines(1)
- .textOverflow({ overflow: TextOverflow.Ellipsis })
- .visibility(this.getFolderSearchLocation(folder).length > 0 ? Visibility.Visible : Visibility.None)
- Text('文件夹')
- .fontSize(13)
- .fontColor($r('app.color.index_tab_font_color'))
- .opacity(0.6)
- }
- .alignItems(HorizontalAlign.Start)
- .layoutWeight(1)
- }
- }
- .reuseId('dir_item')
- .width('100%')
- .padding(12)
- .backgroundColor(Color.Transparent)
- // .backgroundColor($r('app.color.start_window_background'))
- .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.8 })
- .onClick(() => {
- if (this.isSearchMode && this.searchText.length > 0) {
- this.isSearchMode = false
- this.searchController.stopEditing()
- this.onSearchInput('')
- }
- this.enterFolder(folder);
- this.breadcrumbs = this.webdavManager.getBreadcrumbs();
- })
- }
- // 歌曲列表项
- @Builder
- buildSongItem(song: VideoItem, index: number) {
- Button({ type: ButtonType.Capsule, stateEffect: true }) {
- Row({ space: 12 }) {
- // 序号
- // 歌曲封面
- Image(song.pixelMapPath ? song.pixelMapPath : $r('app.media.alt'))
- .width(52)
- .height(52)
- .borderRadius(9)
- .sourceSize({width:38, height:38})
- .alt($r('app.media.alt'))
- .fillColor(this.themeColor)
- .objectFit(ImageFit.Cover)
- .shadow({
- radius: StrUtil.isEmpty(song.pixelMapPath) ?6:14,
- type: ShadowType.BLUR,
- color: 'on_primary'
- })
- .margin({ left: 8 })
- .onClick(() => {
- if (this.isMultiSelect) {
- this.toggleSongSelection(song);
- } else {
- this.playSong(song, index, true);
- }
- })
- // 歌曲信息
- Column({ space: 4 }) {
- Text(this.isShowFileName?song.fileName :song.name)
- .fontSize(15)
- .textOverflow({ overflow: this.isLongNameRoLL?TextOverflow.MARQUEE:TextOverflow.Ellipsis })//超长滚动
- .fontColor(this.currentSong?.filePath==song.filePath?this.themeColor:$r('app.color.index_tab_font_color'))
- .maxLines(1)
- .textOverflow({ overflow: TextOverflow.Ellipsis })
- Row(){
- Text((song.artist ?? '') + " ")
- .fontSize(13)
- .fontColor(this.currentSong?.filePath==song.filePath?this.themeColor:$r('app.color.index_tab_font_color'))
- .opacity(0.6)
- .maxLines(1)
- .visibility(song.artist?Visibility.Visible:Visibility.None)
- .textOverflow({ overflow: TextOverflow.Ellipsis })
- Text(this.buildSongMetaLine(song))
- .fontSize(13)
- .fontColor(this.currentSong?.filePath==song.filePath?this.themeColor:
- $r('app.color.index_tab_font_color'))
- .opacity(0.6)
- .maxLines(1)
- .textOverflow({ overflow: TextOverflow.Ellipsis })
- }
- .width('90%')
- Text(this.getSongSearchLocation(song))
- .fontSize(12)
- .fontColor(this.currentSong?.filePath==song.filePath?this.themeColor:$r('app.color.index_tab_font_color'))
- .opacity(0.42)
- .maxLines(1)
- .textOverflow({ overflow: TextOverflow.Ellipsis })
- .visibility(this.getSongSearchLocation(song).length > 0 ? Visibility.Visible : Visibility.None)
- }
- .alignItems(HorizontalAlign.Start)
- .layoutWeight(1)
- .padding({ right: 20 })
- Column() {
- Checkbox({ name: 'checkbox_' + index })
- .select(this.isSongSelected(song))
- .selectedColor(this.themeColor)
- .opacity(this.isMultiSelect ? 1 : 0)
- .animation({
- duration: 300,
- curve: 'Smooth'
- })
- .visibility(this.isMultiSelect ? Visibility.Visible : Visibility.None)
- .onChange((checked: boolean) => this.handleCheckboxSelection(song, checked))
- .margin({ left: 10, top: 8, bottom: 8, right: 12 })
- .width(22)
- .height(22)
- ImageAnimator()
- .images(CommonConstants.IMAGE_FRAME_INFO)// 动画数组
- .duration(1000)// 持续
- //.state(this.animationState)// 动画状态
- .state(AnimationStatus.Running)// 动画状态
- .fillMode(FillMode.Forwards)
- .width(18)
- .margin({ right: 12, top: 8, bottom: 8 })
- .visibility(this.currentSong?.filePath==song.filePath ? (this.isMultiSelect ? Visibility.None : Visibility.Visible) :
- Visibility.None)
- .height(18)
- .iterations(-1) // 播放次数
- }
- }
- }
- .width('100%')
- .padding(12)
- .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.8 })
- .backgroundColor(Color.Transparent)
- .onClick(() => {
- if (this.ignoreTapAfterExitMultiSelect) {
- return;
- }
- if (this.isMultiSelect) {
- this.toggleSongSelection(song);
- } else {
- this.playSong(song, index);
- }
- })
- // .gesture(LongPressGesture().onAction(() => {
- // this.enterMultiSelect(song);
- // }))
- .bindContextMenu(this.LongPressMenuBuilder(song), ResponseType.LongPress,
- {
- preview: MenuPreviewMode.IMAGE
- })
- .bindContextMenu(this.LongPressMenuBuilder(song), ResponseType.RightClick,
- {
- preview: MenuPreviewMode.IMAGE
- })
- }
- @Builder
- LongPressMenuBuilder(song: VideoItem) {
- Scroll() {
- Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) {
- Menu() {
- MenuItem({
- symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.forward_end_fill')),
- content: '下一首播放'
- })
- .onClick(() => {
- this.addSongToNextPlay(song);
- })
- MenuItem({
- symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.music_note_list')),
- content: '添加到歌单'
- })
- .onClick(async () => {
- await this.openSongAddToPlaylistDialog(song);
- })
- //多选
- MenuItem({
- symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.checkmark_square_on_square')),
- content: $r('app.string.select_all')
- })
- .onClick(() => {
- this.enterMultiSelect(song);
- })
- //下载
- MenuItem({
- symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.download')),
- content: '下载'
- })
- .onClick(async () => {
- await this.enqueueSongDownload(song);
- })
- //属性
- MenuItem({
- symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.music_note_list')),
- content: '属性'
- })
- .onClick(() => {
- this.openSongPropertySheet(song);
- })
- //删除
- MenuItem({
- symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.trash')),
- content: $r('app.string.delete')
- })
- .onClick(() => {
- this.confirmDeleteSingleSong(song);
- })
- }
- .font({ size: 14, weight: FontWeight.Normal })
- }
- .width(180)
- }
- .height('auto')
- .enableScrollInteraction(true)
- .scrollBar(BarState.Off)
- }
-
-
- // 对话框控制器
- private createFolderDialogController: CustomDialogController | null = null;
- /**
- * 显示创建文件夹对话框
- */
- private showCreateFolderDialog(): void {
- if (!this.selectedAccount) {
- this.getUIContext().getPromptAction().showToast({ message: `请先选择${getRemoteDriveAccountLabel()}` });
- return;
- }
- // 获取当前主题色
- const currentThemeColor = this.themeColor;
- // 创建自定义对话框
- this.createFolderDialogController = new CustomDialogController({
- builder: CreateFolderDialog({
- onConfirm: (folderName: string) => {
- if (folderName.trim().length > 0) {
- void this.createFolder(folderName.trim());
- } else {
- this.getUIContext().getPromptAction().showToast({ message: '文件夹名称不能为空' });
- }
- },
- onCancel: () => {
- // 用户取消创建
- },
- initialName: '',
- themeColor: currentThemeColor
- }),
- autoCancel: true,
- alignment: DialogAlignment.Center,
- customStyle: false
- });
- this.createFolderDialogController.open();
- }
- /**
- * 上传成功后刷新文件列表
- */
- private async refreshFileListAfterUpload(): Promise<void> {
- if (!this.selectedAccount) {
- Logger.warn(TAG, '刷新失败:未选择账户');
- return;
- }
- try {
- Logger.info(TAG, 'heanup 开始刷新上传后的文件列表');
- // 显示加载状态
- this.isLoading = true;
- // 重新加载当前账户的文件信息
- await this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount, this.webdavManager.currentPath);
- Logger.info(TAG, 'heanup 文件列表刷新完成');
- } catch (error) {
- const err = error as Error;
- Logger.error(TAG, `heanup 刷新文件列表失败: ${err.message}`);
- // 显示错误提示
- this.getUIContext().getPromptAction().showToast({
- message: `刷新失败: ${err.message}`
- });
- } finally {
- this.isLoading = false;
- }
- }
- /**
- * 创建文件夹的实际方法
- * @param folderName 文件夹名称
- */
- private async createFolder(folderName: string): Promise<void> {
- if (!this.selectedAccount) {
- this.getUIContext().getPromptAction().showToast({ message: `请先选择${getRemoteDriveAccountLabel()}` });
- return;
- }
- if (folderName.length === 0) {
- this.getUIContext().getPromptAction().showToast({ message: '文件夹名称不能为空' });
- return;
- }
- try {
- Logger.info(TAG, `heanup 开始创建文件夹: ${folderName}`);
- // 显示加载状态
- this.isLoading = true;
- if(this.selectedAccount.webType==RemoteDriveType.Baidu){
- // 调用RemoteDriveManager的createBaiduFolder方法
- await this.webdavManager.createBaiduFolder(this.selectedAccount, folderName);
- }else if(this.selectedAccount.webType==RemoteDriveType.WebDav){
- // 调用RemoteDriveManager的createWebDavFolder方法
- await this.webdavManager.createWebDavFolder(this.selectedAccount, folderName);
- }else if(this.selectedAccount.webType==RemoteDriveType.Smb){
- // 调用RemoteDriveManager的createSMBFolder方法
- await this.webdavManager.createSMBFolder(this.selectedAccount, folderName);
- }else if(this.selectedAccount.webType==RemoteDriveType.Ftp){
- // 调用RemoteDriveManager的createFTPFolder方法
- this.getUIContext().getPromptAction().showToast({ message: 'FTP目前不支持创建文件夹' });
- }else{
- this.getUIContext().getPromptAction().showToast({ message: '当前账户类型不支持创建文件夹' });
- }
- // 创建成功后刷新当前目录
- await this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount, this.webdavManager.currentPath);
- this.getUIContext().getPromptAction().showToast({
- message: `文件夹 "${folderName}" 创建成功`
- });
- Logger.info(TAG, `heanup 文件夹创建成功`);
- } catch (error) {
- const err = error as Error;
- Logger.error(TAG, `heanup 创建文件夹失败: ${err.message}`);
- // 显示具体的错误信息
- this.getUIContext().getPromptAction().showToast({
- message: `创建失败: ${err.message}`
- });
- } finally {
- this.isLoading = false;
- }
- }
- }
|