| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275 |
- 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, deviceInfo } from '@kit.BasicServicesKit';
- import { hdsEffect } from '@kit.UIDesignKit';
- import { EventConstants } from '../common/constants/EventConstants';
- import { LazyDataSource } from '../common/util/LazyDataSource';
- import { ArrayUtil, FileUtil, MD5, PreferencesUtil, StrUtil, ToastUtil } from '@pura/harmony-utils';
- import { MenuModifier } 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 { mergeCoverWithPriority, 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, DownloadCenterTask } from '../common/util/DownloadCenterManager';
- import { TitleBarPointLightButton } from '../view/PointLight/TitleBarPointLightButton';
- import { DownloadCenter } from '../view/DownloadCenter';
- import { WebDavUrlUtil } from '../common/util/WebDavUrlUtil';
- import { SearchHistoryUtil } from '../common/util/SearchHistoryUtil';
- import { PlayingIndicator } from '../view/PlayingIndicator';
- import { buildWebDavVisibleThumbPrefetchWindow, WebDavThumbPrefetchRange } from '../common/util/WebDavThumbPrefetchHelper';
- import { buildWebDavSongItemRenderKey } from '../common/util/WebDavListRenderHelper';
- import { MusicPlaybackController } from '../controller/MusicPlaybackController';
- interface WebDavMetadataUpdatePayload {
- filePath: string;
- pixelMapPath?: string;
- isCustomCover?: number;
- md5Str?: 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_CAPTURE_SECONDS: string = '1.2';
- const WEBDAV_PROGRESSIVE_RELOAD_THRESHOLD: number = 180;
- const WEBDAV_PROGRESSIVE_RELOAD_INITIAL_COUNT: number = 20;
- const WEBDAV_PROGRESSIVE_RELOAD_BATCH_SIZE: number = 20;
- const WEBDAV_PROGRESSIVE_RELOAD_DELAY_MS: number = 32;
- const WEBDAV_VISIBLE_THUMB_PREFETCH_DELAY_MS: number = 120;
- // 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 setWebdavPlaylist(items: VideoItem[], startIndex: number): void {
- globalWebdavVideoItems = items.slice();
- globalWebdavCurrentPlayIndex = startIndex;
- }
- 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 //自定义背景界面
- @State isHomeBgFollowMusicCover: boolean = true //主页背景随当前播放封面
- @StorageProp('isLandscape') isLandscape: boolean = false;
- @State blurValue: number = 0 //背景模糊
- @State bgBrightness: number = 0 //背景亮度
- @State customizeBgPath: string | undefined = '';
- @StorageProp('customizeMusicBgPath') customizeMusicBgPath: string = '';
- 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 isSearchMode: boolean = false
- @StorageLink('currentSong') @Watch('onHomeBgCurrentSongChange') currentSong: VideoItem | undefined = undefined;
- @StorageProp('topSafeHeight') topSafeHeight: number = 0;
- @State webdavManager: RemoteDriveManager = RemoteDriveManager.getInstance();
- @State accounts: WebDavAccount[] = [];
- @Prop @Watch('onSwitchAccount') selectedAccount: WebDavAccount;
- private songs: VideoItem[] = [];
- @State dataSource:LazyDataSource<VideoItem> = new LazyDataSource(this.songs)
- @Link mType: number;
- @Link offsetX: number;
- @Link isShowDrawer: boolean;
- @State isLoading: boolean = false;
- @State isRefreshingCache: boolean = false;
- @State isDirectoryNavigationLoading: boolean = false;
- private webDavFiles: FileInfo[] = []; // 原始目录数据保持为普通字段,避免大数组响应式代理卡顿
- @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
- @StorageProp('isDarkMode') isDarkMode: boolean = false;
- @State currentDirectoryPath: string = '';
- @State breadcrumbs:BreadcrumbItem[] = []//面包屑导航
- @State private pointLightOptions: hdsEffect.PointLightOptions | undefined = undefined
- @State private activePointLightItemKey: string = ''
- @State visibleFoldersState: FileInfo[] = []; // 改为普通状态变量
- @State isShowFileName: boolean = false//是否显示文件名
- @State isLongNameRoLL: boolean = true//长歌名滚动
- @State sortType: number = 0 //默认排序方式
- @Consume isMultiSelect: boolean
- @State selectedSongs: VideoItem[] = []
- @State selectedFolders: FileInfo[] = []
- @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 downloadCenterRefreshVersion: number = 0
- @State downloadCenterActiveTasks: DownloadCenterTask[] = []
- @State downloadCenterCompletedTasks: DownloadCenterTask[] = []
- @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 visibleListStartIndex: number = -1;
- private visibleListEndIndex: number = -1;
- private scheduledThumbPrefetchTimer: number = -1;
- private lastVisibleThumbStart: number = -1;
- private lastVisibleThumbEndExclusive: number = -1;
- private readonly downloadCenterManager: DownloadCenterManager = DownloadCenterManager.getInstance();
- private readonly downloadCenterListener: () => void = (): void => {
- this.scheduleDownloadCenterRefresh();
- };
- private downloadCenterRefreshTimer: number = -1;
- private displayReloadToken: number = 0;
- private pendingDirectoryRefreshToken: number = 0;
- private playbackController: MusicPlaybackController = MusicPlaybackController.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.cancelPendingDirectoryRefresh();
- this.resetRemoteThumbPrefetchState();
- 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.currentDirectoryPath = this.selectedAccount.filepath || '/';
- this.scheduleDirectoryRefresh(this.selectedAccount.filepath,
- () => this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount), '加载文件失败');
- this.breadcrumbs = this.webdavManager.getBreadcrumbs();
- }
- updateListData(mList:Array<VideoItem>, noSort?: boolean, shouldScrollToTop: boolean = true){
- if (!noSort) {
- this.sortType = PreferencesUtil.getNumberSync('webDavSortType', 0)
- this.doSortType(this.sortType, shouldScrollToTop)
- return
- }
- this.refreshDisplaySongs(mList, shouldScrollToTop)
- }
- private refreshDisplaySongs(mList: Array<VideoItem>, shouldScrollToTop: boolean = true): void {
- const reloadToken: number = ++this.displayReloadToken;
- this.invalidateRemoteThumbPrefetchRange()
- const displayItems: Array<VideoItem> = mList ? [...mList] : [];
- if (displayItems.length <= WEBDAV_PROGRESSIVE_RELOAD_THRESHOLD) {
- this.dataSource.pushArrayData(displayItems)
- if (shouldScrollToTop && displayItems.length > 0) {
- setTimeout(() => {
- if (reloadToken !== this.displayReloadToken) {
- return
- }
- this.listScroller.scrollToIndex(0)
- }, 120)
- }
- return
- }
- const initialItems: Array<VideoItem> = displayItems.slice(0, WEBDAV_PROGRESSIVE_RELOAD_INITIAL_COUNT)
- this.dataSource.pushArrayData(initialItems)
- if (shouldScrollToTop && initialItems.length > 0) {
- setTimeout(() => {
- if (reloadToken !== this.displayReloadToken) {
- return
- }
- this.listScroller.scrollToIndex(0)
- }, 80)
- }
- let cursor: number = initialItems.length
- const appendNextBatch = (): void => {
- if (reloadToken !== this.displayReloadToken) {
- return
- }
- if (cursor >= displayItems.length) {
- return
- }
- const nextBatch: Array<VideoItem> = displayItems.slice(cursor, cursor + WEBDAV_PROGRESSIVE_RELOAD_BATCH_SIZE)
- this.dataSource.appendArrayData(nextBatch)
- cursor += nextBatch.length
- if (cursor < displayItems.length) {
- setTimeout(() => {
- appendNextBatch()
- }, WEBDAV_PROGRESSIVE_RELOAD_DELAY_MS)
- }
- }
- setTimeout(() => {
- appendNextBatch()
- }, WEBDAV_PROGRESSIVE_RELOAD_DELAY_MS)
- }
- private cancelPendingDirectoryRefresh(): void {
- this.pendingDirectoryRefreshToken += 1
- }
- private startDirectoryNavigationLoading(): void {
- this.isDirectoryNavigationLoading = true
- }
- private stopDirectoryNavigationLoading(): void {
- this.isDirectoryNavigationLoading = false
- }
- private scheduleDirectoryRefresh(_targetPath: string | undefined, loadAction: () => Promise<void>,
- errorPrefix: string): void {
- this.cancelPendingDirectoryRefresh()
- this.startDirectoryNavigationLoading()
- const refreshToken: number = this.pendingDirectoryRefreshToken
- this.isLoading = true
- this.isRefreshingCache = false
- const executeLoad = (): void => {
- if (refreshToken !== this.pendingDirectoryRefreshToken) {
- return
- }
- void loadAction().catch((error: Error) => {
- if (refreshToken !== this.pendingDirectoryRefreshToken) {
- return
- }
- Logger.error(TAG, `${errorPrefix}: ${error.message}`)
- this.isLoading = false
- this.isRefreshingCache = false
- this.stopDirectoryNavigationLoading()
- })
- }
- executeLoad()
- }
- 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);
- }
- }
- private getDisplayedFolderCount(): number {
- return this.isSearchMode ? this.filteredFolderList.length : this.visibleFoldersState.length
- }
- private stopScheduledRemoteThumbPrefetch(): void {
- if (this.scheduledThumbPrefetchTimer >= 0) {
- clearTimeout(this.scheduledThumbPrefetchTimer)
- this.scheduledThumbPrefetchTimer = -1
- }
- }
- // 保留当前视口索引,但让新的列表内容可以重新触发同一可见区间的封面补全。
- private invalidateRemoteThumbPrefetchRange(): void {
- this.stopScheduledRemoteThumbPrefetch()
- this.thumbnailTaskToken += 1
- this.thumbnailRunningKeys.clear()
- this.lastVisibleThumbStart = -1
- this.lastVisibleThumbEndExclusive = -1
- }
- // 列表数据源切换时需要显式取消旧任务,避免后台继续为已经离开的可见区生成封面。
- private resetRemoteThumbPrefetchState(): void {
- this.invalidateRemoteThumbPrefetchRange()
- this.visibleListStartIndex = -1
- this.visibleListEndIndex = -1
- }
- private updateVisibleRemoteThumbRange(start: number, end: number): void {
- this.visibleListStartIndex = start
- this.visibleListEndIndex = end
- this.scheduleRemoteThumbPrefetch()
- }
- 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> {
- if (item.isCustomCover === 1 && StrUtil.isNotEmpty(item.pixelMapPath)) {
- this.refreshThumbItem(item);
- return;
- }
- const coverPath: string = await this.resolveRemoteThumbPath(item);
- if (this.isThumbCacheValid(coverPath)) {
- mergeCoverWithPriority(item, 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)) {
- mergeCoverWithPriority(item, 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)) {
- mergeCoverWithPriority(item, 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)) {
- mergeCoverWithPriority(item, 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 {
- if (this.isLoading || this.isRefreshingCache) {
- this.stopScheduledRemoteThumbPrefetch()
- this.thumbnailTaskToken += 1
- this.thumbnailRunningKeys.clear()
- return
- }
- const totalSongs: number = this.dataSource.totalCount()
- const visibleWindow = buildWebDavVisibleThumbPrefetchWindow(totalSongs, this.getDisplayedFolderCount(),
- this.visibleListStartIndex, this.visibleListEndIndex)
- if (!visibleWindow.hasVisibleSongs) {
- this.stopScheduledRemoteThumbPrefetch()
- this.thumbnailTaskToken += 1
- this.thumbnailRunningKeys.clear()
- return
- }
- const range: WebDavThumbPrefetchRange = visibleWindow.range
- const isSameVisibleRange: boolean = range.start === this.lastVisibleThumbStart &&
- range.endExclusive === this.lastVisibleThumbEndExclusive
- if (isSameVisibleRange) {
- return
- }
- this.lastVisibleThumbStart = range.start
- this.lastVisibleThumbEndExclusive = range.endExclusive
- this.stopScheduledRemoteThumbPrefetch()
- const token: number = this.thumbnailTaskToken + 1
- this.thumbnailTaskToken = token
- this.thumbnailRunningKeys.clear()
- this.scheduledThumbPrefetchTimer = setTimeout((): void => {
- this.scheduledThumbPrefetchTimer = -1
- if (token !== this.thumbnailTaskToken) {
- return
- }
- const mediaItems: VideoItem[] = this.dataSource.dataArray.slice(range.start, range.endExclusive)
- if (mediaItems.length <= 0) {
- return
- }
- void this.runRemoteThumbPrefetchQueue(mediaItems, token)
- }, WEBDAV_VISIBLE_THUMB_PREFETCH_DELAY_MS)
- }
- 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 resolveFolderSelectionKey(folder: FileInfo): string {
- return folder.href;
- }
- private isFolderSelected(folder: FileInfo): boolean {
- const key = this.resolveFolderSelectionKey(folder);
- return this.selectedFolders.some(item => this.resolveFolderSelectionKey(item) === key);
- }
- private getSelectableSongs(): VideoItem[] {
- return this.isSearchMode ? this.filteredList : this.songs;
- }
- private getSelectableFolders(): FileInfo[] {
- return this.isSearchMode ? this.filteredFolderList : this.visibleFoldersState;
- }
- private getSelectedEntryCount(): number {
- return this.selectedSongs.length + this.selectedFolders.length;
- }
- private hasSelectedFolders(): boolean {
- return this.selectedFolders.length > 0;
- }
- private canOperateOnSelectedSongs(): boolean {
- return this.selectedSongs.length > 0 && !this.hasSelectedFolders() && !this.isDeletingSelection;
- }
- private updateAllSelectedState(): void {
- const totalSelectable = this.getSelectableSongs().length + this.getSelectableFolders().length;
- const totalSelected = this.getSelectedEntryCount();
- this.isAllSelected = totalSelected > 0 && totalSelected === totalSelectable;
- }
- private enterMultiSelect(song: VideoItem): void {
- if (!this.isMultiSelect) {
- this.isMultiSelect = true;
- this.selectedSongs = [song];
- this.selectedFolders = [];
- this.updateAllSelectedState();
- 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.updateAllSelectedState();
- if (next.length === 0) {
- if (this.selectedFolders.length === 0) {
- this.exitMultiSelect();
- }
- }
- } else {
- const next = [...this.selectedSongs, song];
- this.selectedSongs = next;
- this.updateAllSelectedState();
- }
- }
- 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 enterFolderMultiSelect(folder: FileInfo): void {
- if (!this.isMultiSelect) {
- this.isMultiSelect = true;
- this.selectedFolders = [folder];
- this.selectedSongs = [];
- this.updateAllSelectedState();
- return;
- }
- if (!this.isFolderSelected(folder)) {
- this.toggleFolderSelection(folder);
- }
- }
- private toggleFolderSelection(folder: FileInfo): void {
- if (!this.isMultiSelect) {
- this.isMultiSelect = true;
- }
- const key = this.resolveFolderSelectionKey(folder);
- const exists = this.selectedFolders.some(item => this.resolveFolderSelectionKey(item) === key);
- if (exists) {
- const next = this.selectedFolders.filter(item => this.resolveFolderSelectionKey(item) !== key);
- this.selectedFolders = next;
- this.updateAllSelectedState();
- if (next.length === 0 && this.selectedSongs.length === 0) {
- this.exitMultiSelect();
- }
- } else {
- this.selectedFolders = [...this.selectedFolders, folder];
- this.updateAllSelectedState();
- }
- }
- private handleFolderCheckboxSelection(folder: FileInfo, checked: boolean): void {
- if (this.ignoreTapAfterExitMultiSelect) {
- return;
- }
- if (!this.isMultiSelect) {
- this.isMultiSelect = true;
- }
- const exists = this.isFolderSelected(folder);
- if (checked && !exists) {
- this.toggleFolderSelection(folder);
- } else if (!checked && exists) {
- this.toggleFolderSelection(folder);
- }
- }
- private toggleSelectAll(): void {
- if (this.isAllSelected) {
- this.selectedSongs = [];
- this.selectedFolders = [];
- this.isAllSelected = false;
- return;
- }
- if (!this.isMultiSelect) {
- this.isMultiSelect = true;
- }
- this.selectedSongs = this.getSelectableSongs().slice();
- this.selectedFolders = this.getSelectableFolders().slice();
- this.updateAllSelectedState();
- }
- private exitMultiSelect(): void {
- this.ignoreTapAfterExitMultiSelect = true;
- this.isMultiSelect = false;
- this.selectedSongs = [];
- this.selectedFolders = [];
- 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 currentSongKeys: Set<string> = new Set(this.getSelectableSongs().map(item => item.filePath));
- const filteredSongs = this.selectedSongs.filter(item => currentSongKeys.has(item.filePath));
- if (filteredSongs.length !== this.selectedSongs.length) {
- this.selectedSongs = filteredSongs;
- }
- const currentFolderKeys: Set<string> = new Set(
- this.getSelectableFolders().map(item => this.resolveFolderSelectionKey(item))
- );
- const filteredFolders = this.selectedFolders.filter(item =>
- currentFolderKeys.has(this.resolveFolderSelectionKey(item))
- );
- if (filteredFolders.length !== this.selectedFolders.length) {
- this.selectedFolders = filteredFolders;
- }
- this.updateAllSelectedState();
- if (filteredSongs.length === 0 && filteredFolders.length === 0) {
- this.exitMultiSelect();
- }
- }
- private confirmDeleteSelected(): void {
- if (!this.selectedAccount) {
- this.getUIContext().getPromptAction().showToast({ message: `请先选择${getRemoteDriveAccountLabel()}` });
- return;
- }
- const totalSelected = this.getSelectedEntryCount();
- if (!this.isMultiSelect || totalSelected === 0) {
- this.getUIContext().getPromptAction().showToast({ message: '请先选择要删除的文件或文件夹' });
- return;
- }
- this.getUIContext().showAlertDialog({
- title: '删除项目',
- message: `确定删除选中的${totalSelected}项吗?`,
- primaryButton: {
- value: '取消',
- action: () => {}
- },
- secondaryButton: {
- value: '删除',
- fontColor: Color.Red,
- action: () => {
- void this.performDeleteSelected();
- }
- }
- });
- }
- private async performDeleteSelected(): Promise<void> {
- if (!this.selectedAccount || this.getSelectedEntryCount() === 0) {
- return;
- }
- this.isDeletingSelection = true;
- try {
- if (this.selectedSongs.length > 0) {
- await this.webdavManager.deleteRemoteSongs(this.selectedAccount, this.selectedSongs.slice());
- }
- if (this.selectedFolders.length > 0) {
- await this.webdavManager.deleteRemoteFolders(this.selectedAccount, this.selectedFolders.slice());
- }
- this.removeSongsLocally(this.selectedSongs.slice())
- this.removeFoldersLocally(this.selectedFolders.slice())
- this.exitMultiSelect();
- this.getUIContext().getPromptAction().showToast({ message: '删除成功' });
- } 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]);
- this.removeSongsLocally([song])
- this.getUIContext().getPromptAction().showToast({ message: '删除成功' });
- } catch (error) {
- const err = error as Error;
- this.getUIContext().getPromptAction().showToast({ message: `删除失败: ${err.message}` });
- } finally {
- this.isDeletingSelection = false;
- }
- }
- private getRemoteFolderDisplayName(folder: FileInfo): string {
- return decodeUrlEncodedString((folder.fileName ?? folder.name ?? '').replace(/\/+$/, ''));
- }
- private confirmDeleteSingleFolder(folder: FileInfo): void {
- if (!this.selectedAccount) {
- this.getUIContext().getPromptAction().showToast({ message: `请先选择${getRemoteDriveAccountLabel()}` });
- return;
- }
- this.getUIContext().showAlertDialog({
- title: '删除文件夹',
- message: `确定删除 "${this.getRemoteFolderDisplayName(folder)}" 吗?`,
- primaryButton: {
- value: '取消',
- action: () => {}
- },
- secondaryButton: {
- value: '删除',
- fontColor: Color.Red,
- action: () => {
- void this.performDeleteSingleFolder(folder);
- }
- }
- });
- }
- private async performDeleteSingleFolder(folder: FileInfo): Promise<void> {
- if (!this.selectedAccount) {
- return;
- }
- this.isDeletingSelection = true;
- try {
- await this.webdavManager.deleteRemoteFolders(this.selectedAccount, [folder]);
- this.removeFoldersLocally([folder])
- this.getUIContext().getPromptAction().showToast({ message: '删除成功' });
- } catch (error) {
- const err = error as Error;
- this.getUIContext().getPromptAction().showToast({ message: `删除失败: ${err.message}` });
- } finally {
- this.isDeletingSelection = false;
- }
- }
- private removeDisplayedRemoteSongsByKeys(keys: Set<string>): void {
- for (let index = this.dataSource.dataArray.length - 1; index >= 0; index--) {
- const item = this.dataSource.dataArray[index]
- if (item && keys.has(item.filePath)) {
- this.dataSource.deleteData(index)
- }
- }
- }
- private syncGlobalWebdavQueueAfterSongRemoval(keys: Set<string>): void {
- if (keys.size <= 0 || !ArrayUtil.isNotEmpty(globalWebdavVideoItems)) {
- return
- }
- const nextQueue = globalWebdavVideoItems.filter((item: VideoItem) => !keys.has(item.filePath))
- if (nextQueue.length === globalWebdavVideoItems.length) {
- return
- }
- const currentPath = this.currentSong?.filePath ?? '';
- let nextIndex = nextQueue.findIndex((item: VideoItem) => item.filePath === currentPath)
- if (nextIndex < 0) {
- nextIndex = nextQueue.length > 0 ? Math.min(globalWebdavCurrentPlayIndex, nextQueue.length - 1) : 0
- }
- globalWebdavVideoItems = nextQueue
- globalWebdavCurrentPlayIndex = nextIndex
- const eventQueueRefresh: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAY_QUEUE_REFRESH }
- emitter.emit(eventQueueRefresh, { data: { source: 'webdav-delete' } })
- }
- private removeSongsLocally(songs: VideoItem[]): void {
- if (ArrayUtil.isEmpty(songs)) {
- return
- }
- const keys: Set<string> = new Set<string>(songs.map((item: VideoItem) => item.filePath))
- this.songs = this.songs.filter((item: VideoItem) => !keys.has(item.filePath))
- this.webdavManager.webDavSongs = this.songs.slice()
- this.filteredList = this.filteredList.filter((item: VideoItem) => !keys.has(item.filePath))
- this.removeDisplayedRemoteSongsByKeys(keys)
- this.syncGlobalWebdavQueueAfterSongRemoval(keys)
- if (this.isSearchMode && this.searchText.length > 0) {
- this.syncSelectionAfterRefresh()
- } else {
- this.syncSelectionAfterRefresh()
- }
- this.invalidateRemoteThumbPrefetchRange()
- this.scheduleRemoteThumbPrefetch()
- }
- private removeFoldersLocally(folders: FileInfo[]): void {
- if (ArrayUtil.isEmpty(folders)) {
- return
- }
- const keys: Set<string> = new Set<string>(folders.map((item: FileInfo) => this.resolveFolderSelectionKey(item)))
- this.webDavFiles = this.webDavFiles.filter((item: FileInfo) => !keys.has(this.resolveFolderSelectionKey(item)))
- this.webdavManager.webDavFiles = this.webDavFiles.slice()
- this.visibleFoldersState = this.visibleFoldersState.filter((item: FileInfo) =>
- !keys.has(this.resolveFolderSelectionKey(item)))
- this.filteredFolderList = this.filteredFolderList.filter((item: FileInfo) =>
- !keys.has(this.resolveFolderSelectionKey(item)))
- if (this.isSearchMode && this.searchText.length > 0) {
- this.refreshDisplaySongs(this.filteredList)
- } else {
- this.updateVisibleFolders()
- }
- this.syncSelectionAfterRefresh()
- }
- @Builder
- private buildSwipeDeleteAction(onDelete: () => void) {
- Button() {
- Image($r('app.media.delete2'))
- .fillColor(Color.White)
- .width(20)
- .height(20)
- }
- .width(56)
- .height(56)
- .type(ButtonType.Circle)
- .backgroundColor('#E53935')
- .margin(8)
- .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.86 })
- .onClick(() => {
- onDelete()
- })
- }
- private canRenameRemoteSong(song: VideoItem): boolean {
- return song.type === CommonConstants.TYPE_WEBDAV ||
- song.type === CommonConstants.TYPE_SMB ||
- song.type === CommonConstants.TYPE_FTP ||
- song.type === CommonConstants.TYPE_BAIDU;
- }
- private canRenameRemoteFolder(): boolean {
- return !!this.selectedAccount &&
- (this.selectedAccount.webType === RemoteDriveType.WebDav ||
- this.selectedAccount.webType === RemoteDriveType.Smb ||
- this.selectedAccount.webType === RemoteDriveType.Ftp ||
- this.selectedAccount.webType === RemoteDriveType.Baidu);
- }
- private getRemoteSongRenameFileName(song: VideoItem): string {
- const rawName = StrUtil.isNotEmpty(song.fileName) ? song.fileName as string : song.name;
- return decodeUrlEncodedString(rawName ?? '');
- }
- private getRemoteSongRenameExtension(song: VideoItem): string {
- const fileName = this.getRemoteSongRenameFileName(song);
- const lastDotIndex = fileName.lastIndexOf('.');
- if (lastDotIndex <= 0) {
- return '';
- }
- return fileName.substring(lastDotIndex);
- }
- private async resolveSongAccountForAction(song: VideoItem): Promise<WebDavAccount | null> {
- if (song.webdav_account_id) {
- const account = await this.webdavManager.getWebDavAccountById(song.webdav_account_id);
- if (account) {
- return account;
- }
- }
- return this.selectedAccount ?? null;
- }
- private showRenameSongDialog(song: VideoItem): void {
- if (!this.canRenameRemoteSong(song)) {
- this.getUIContext().getPromptAction().showToast({ message: '当前文件暂不支持重命名' });
- return;
- }
- const extension = this.getRemoteSongRenameExtension(song);
- const currentThemeColor = this.themeColor;
- this.renameSongDialogController = new CustomDialogController({
- builder: CreateFolderDialog({
- onConfirm: (fileName: string) => {
- if (fileName.trim().length > 0) {
- void this.performRenameRemoteSong(song, fileName.trim());
- } else {
- this.getUIContext().getPromptAction().showToast({ message: '文件名称不能为空' });
- }
- },
- onCancel: () => {
- },
- initialName: this.getRemoteSongRenameFileName(song),
- title: '重命名',
- placeholder: extension.length > 0 ? `请输入文件名称,保留 ${extension} 格式` : '请输入文件名称',
- confirmText: '确定',
- cancelText: '取消',
- themeColor: currentThemeColor
- }),
- autoCancel: true,
- alignment: DialogAlignment.Center,
- customStyle: false
- });
- this.renameSongDialogController.open();
- }
- private showRenameFolderDialog(folder: FileInfo): void {
- if (!this.canRenameRemoteFolder()) {
- this.getUIContext().getPromptAction().showToast({ message: '当前文件夹暂不支持重命名' });
- return;
- }
- const currentThemeColor = this.themeColor;
- this.renameSongDialogController = new CustomDialogController({
- builder: CreateFolderDialog({
- onConfirm: (folderName: string) => {
- if (folderName.trim().length > 0) {
- void this.performRenameRemoteFolder(folder, folderName.trim());
- } else {
- this.getUIContext().getPromptAction().showToast({ message: '文件夹名称不能为空' });
- }
- },
- onCancel: () => {
- },
- initialName: this.getRemoteFolderDisplayName(folder),
- title: '重命名',
- placeholder: '请输入文件夹名称',
- confirmText: '确定',
- cancelText: '取消',
- themeColor: currentThemeColor
- }),
- autoCancel: true,
- alignment: DialogAlignment.Center,
- customStyle: false
- });
- this.renameSongDialogController.open();
- }
- private async performRenameRemoteSong(song: VideoItem, content: string): Promise<void> {
- const nextName = content ? content.trim() : '';
- if (!nextName) {
- this.getUIContext().getPromptAction().showToast({ message: '文件名称不能为空' });
- return;
- }
- const currentFileName = this.getRemoteSongRenameFileName(song);
- if (nextName === currentFileName) {
- return;
- }
- const account = await this.resolveSongAccountForAction(song);
- if (!account) {
- this.getUIContext().getPromptAction().showToast({ message: `请先选择${getRemoteDriveAccountLabel()}` });
- return;
- }
- this.isLoading = true;
- try {
- await this.webdavManager.renameRemoteSong(account, song, nextName);
- await this.webdavManager.loadFilesInfoFromAccount(account, this.currentDirectoryPath || account.filepath || '/');
- this.getUIContext().getPromptAction().showToast({ message: '重命名成功' });
- } catch (error) {
- const err = error as Error;
- this.getUIContext().getPromptAction().showToast({ message: `重命名失败: ${err.message}` });
- } finally {
- this.isLoading = false;
- }
- }
- private async performRenameRemoteFolder(folder: FileInfo, content: string): Promise<void> {
- const nextName = content ? content.trim().replace(/\/+$/, '') : '';
- if (!nextName) {
- this.getUIContext().getPromptAction().showToast({ message: '文件夹名称不能为空' });
- return;
- }
- const currentFolderName = this.getRemoteFolderDisplayName(folder);
- if (nextName === currentFolderName) {
- return;
- }
- if (!this.selectedAccount) {
- this.getUIContext().getPromptAction().showToast({ message: `请先选择${getRemoteDriveAccountLabel()}` });
- return;
- }
- this.isLoading = true;
- try {
- await this.webdavManager.renameRemoteFolder(this.selectedAccount, folder, nextName);
- await this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount,
- this.currentDirectoryPath || this.selectedAccount.filepath || '/');
- this.getUIContext().getPromptAction().showToast({ message: '重命名成功' });
- } catch (error) {
- const err = error as Error;
- this.getUIContext().getPromptAction().showToast({ message: `重命名失败: ${err.message}` });
- } finally {
- this.isLoading = false;
- }
- }
- private addSongToNextPlay(song: VideoItem): void {
- if (!song) {
- return;
- }
- const displayQueue: VideoItem[] = this.getCurrentPlaybackQueue();
- const queue: VideoItem[] = globalWebdavVideoItems && globalWebdavVideoItems.length > 0 ?
- globalWebdavVideoItems : displayQueue;
- if (!queue || queue.length === 0) {
- this.getUIContext().getPromptAction().showToast({ message: '当前列表为空' });
- return;
- }
- const currentPath = this.currentSong?.filePath;
- if (StrUtil.isEmpty(currentPath)) {
- const targetIndex = displayQueue.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.isCustomCover = song.isCustomCover;
- 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.syncDownloadCenterTasks();
- this.isShowDownloadCenter = true;
- }
- private syncDownloadCenterTasks(): void {
- this.downloadCenterActiveTasks = this.downloadCenterManager.getActiveTasks();
- this.downloadCenterCompletedTasks = this.downloadCenterManager.getCompletedTasks();
- }
- private scheduleDownloadCenterRefresh(): void {
- if (this.downloadCenterRefreshTimer >= 0) {
- return;
- }
- this.downloadCenterRefreshTimer = setTimeout((): void => {
- this.downloadCenterRefreshTimer = -1;
- this.syncDownloadCenterTasks();
- this.downloadCenterRefreshVersion += 1;
- }, 0);
- }
- private stopDownloadCenterRefresh(): void {
- if (this.downloadCenterRefreshTimer >= 0) {
- clearTimeout(this.downloadCenterRefreshTimer);
- this.downloadCenterRefreshTimer = -1;
- }
- }
- 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 deleteDownloadTask(taskId: string): void {
- if (StrUtil.isEmpty(taskId)) {
- return;
- }
- this.downloadCenterManager.deleteTask(taskId);
- this.syncDownloadCenterTasks();
- this.downloadCenterRefreshVersion += 1;
- }
- private buildDownloadedVideoItem(task: DownloadCenterTask): VideoItem {
- const fileName = StrUtil.isNotEmpty(task.fileName) ? task.fileName : FileUtil.getFileName(task.targetPath);
- const title = StrUtil.isNotEmpty(task.title) ? task.title : fileName;
- const item = new VideoItem(
- title,
- task.targetPath,
- task.targetPath,
- CommonConstants.TYPE_LOCAL,
- task.totalBytes,
- '',
- task.sizeText,
- task.coverPath,
- undefined,
- undefined,
- fileName
- );
- item.parentPath = StrUtil.isNotEmpty(task.downloadDir) ? task.downloadDir : FileUtil.getParentPath(task.targetPath);
- item.pixelMapPath = task.coverPath;
- item.size = task.sizeText;
- item.videoSize = task.totalBytes;
- item.type = CommonConstants.TYPE_LOCAL;
- return item;
- }
- private playCompletedDownloadTask(taskId: string): void {
- if (StrUtil.isEmpty(taskId)) {
- return;
- }
- const playableTasks = this.downloadCenterCompletedTasks.filter((task: DownloadCenterTask): boolean => {
- return StrUtil.isNotEmpty(task.targetPath)
- });
- const playlist: VideoItem[] = playableTasks.map((task: DownloadCenterTask): VideoItem => {
- return this.buildDownloadedVideoItem(task)
- });
- if (playlist.length <= 0) {
- this.getUIContext().getPromptAction().showToast({ message: '没有可播放的下载文件' });
- return;
- }
- const startIndex = playableTasks.findIndex((task: DownloadCenterTask): boolean => {
- return task.taskId === taskId;
- });
- if (startIndex < 0 || startIndex >= playlist.length) {
- this.getUIContext().getPromptAction().showToast({ message: '未找到对应下载任务' });
- return;
- }
- setWebdavPlaylist(playlist, startIndex);
- void this.playbackController.playQueue(playlist.slice(), startIndex, 'controller-open').catch((error: Error) => {
- Logger.error(TAG, `下载中心播放失败: ${error.message}`);
- this.getUIContext().getPromptAction().showToast({ message: '播放失败' });
- });
- }
- 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}`);
- }
- }
- if (song.type === CommonConstants.TYPE_WEBDAV) {
- try {
- let account: WebDavAccount | null = null;
- if (song.webdav_account_id) {
- account = await this.webdavManager.getWebDavAccountById(song.webdav_account_id);
- }
- if (!account && this.selectedAccount?.webType === RemoteDriveType.WebDav) {
- account = this.selectedAccount;
- }
- if (account) {
- const relativePath = song.remote_rel_path || song.filePath;
- const fullUrl = WebDavUrlUtil.buildFullUrl(account, relativePath);
- if (StrUtil.isNotEmpty(fullUrl)) {
- Logger.info(TAG, `下载中心使用WebDAV直链下载: ${song.name}, url=${fullUrl}`);
- return fullUrl;
- }
- }
- } catch (error) {
- Logger.warn(TAG, `WebDAV下载直链构建失败,回退播放地址: ${(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 buildSongQualityLabel(song: VideoItem): string {
- if (!song || StrUtil.isEmpty(song.md5Str)) {
- return '';
- }
- const quality = song.md5Str as string;
- return quality.includes('Lossless') ? '无损' : quality;
- }
- // 对话框控制器
- 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.isHomeBgFollowMusicCover = PreferencesUtil.getBooleanSync(SettingPage.IS_HOME_BG_FOLLOW_MUSIC_COVER, true)
- 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.isShowTitleBar = PreferencesUtil.getBooleanSync(SettingPage.IS_SHOW_TITLTBAR, true)
- this.autoHideTitle = PreferencesUtil.getBooleanSync('autoHideTitle', true)
- this.syncCustomizeMusicBgPath()
- }
- private onHomeBgCurrentSongChange(): void {
- this.syncCustomizeMusicBgPath()
- }
- private syncCustomizeMusicBgPath(): void {
- if (!this.isHomeBgFollowMusicCover) {
- return
- }
- const coverPath: string = this.currentSong?.pixelMapPath ?? ''
- AppStorage.setOrCreate('customizeMusicBgPath', coverPath)
- }
- private isMusicCoverHomeBackgroundActive(): boolean {
- return this.isHomeBgFollowMusicCover && this.customizeMusicBgPath.length > 0
- }
- 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.downloadCenterManager.subscribe(this.downloadCenterListener);
- 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.cancelPendingDirectoryRefresh();
- this.resetRemoteThumbPrefetchState();
- this.stopDirectoryNavigationLoading();
- this.webdavManager.unsubscribe(this.eventHandler);
- this.downloadCenterManager.unsubscribe(this.downloadCenterListener);
- this.stopDownloadCenterRefresh();
- emitter.off(EventConstants.EVENT_WEBDAV_METADATA_UPDATED);
- }
- // 处理WebDAV事件
- private handleWebdavEvent(event: string): void {
- switch (event) {
- case RemoteDriveManagerStates.LoadFilesInfoStart:
- this.resetRemoteThumbPrefetchState();
- this.startDirectoryNavigationLoading();
- if (this.dataSource.totalCount() > 0 || this.visibleFoldersState.length > 0) {
- this.isLoading = false;
- this.isRefreshingCache = true;
- } else {
- this.isLoading = true;
- this.isRefreshingCache = false;
- }
- break;
- case RemoteDriveManagerStates.LoadFilesInfoSucceed:
- const shouldScrollToTop: boolean = !this.isRefreshingCache;
- this.resetRemoteThumbPrefetchState();
- this.songs = this.webdavManager.webDavSongs;
- // 直接引用webdavManager的数组,避免@Observed序列化问题
- this.webDavFiles = this.webdavManager.webDavFiles;
- this.currentDirectoryPath = this.webdavManager.currentPath || '/';
- this.isLoading = false;
- this.isRefreshingCache = false;
- this.stopDirectoryNavigationLoading();
- // 更新可见文件夹列表
- this.updateVisibleFolders();
- this.breadcrumbs = this.webdavManager.getBreadcrumbs();
- this.syncSelectionAfterRefresh();
- if (this.isSearchMode && this.searchText.length > 0) {
- void this.applyGlobalSearch(this.searchText, ++this.searchTicket);
- } else {
- this.restoreCurrentDirectorySearchView(shouldScrollToTop);
- }
- // promptAction.showToast({
- // message: '加载成功: ' + this.webDavFiles.filter(f => f.isDirectory).length + '个文件夹, ' + this.songs.length + '首歌曲'
- // });
- break;
- case RemoteDriveManagerStates.LoadFilesInfoFailed:
- this.isLoading = false;
- this.isRefreshingCache = false;
- this.stopDirectoryNavigationLoading();
- // 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');
- const changedIndices: number[] = [];
- 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 changedIndex = this.updateSongMetadata(payload);
- if (changedIndex >= 0) {
- if (changedIndices.indexOf(changedIndex) < 0) {
- changedIndices.push(changedIndex);
- }
- Logger.info(TAG, `metadata updated for ${payload.filePath}`);
- }
- }
- if (changedIndices.length === 0) {
- Logger.info(TAG, 'handleWebDavMetadataUpdates no changes detected');
- return;
- }
- // 元数据回填只刷新变更行,避免后台补封面时整列表反复重建导致卡顿。
- for (let index = 0; index < changedIndices.length; index += 1) {
- const changedIndex = changedIndices[index];
- if (changedIndex >= 0 && changedIndex < this.dataSource.totalCount()) {
- this.dataSource.notifyDataChange(changedIndex);
- }
- }
- Logger.info(TAG, 'handleWebDavMetadataUpdates trigger refresh');
- }
- private updateSongMetadata(payload: WebDavMetadataUpdatePayload): number {
- if (!payload.filePath) {
- return -1;
- }
- const targetIndex = this.findSongIndex(payload.filePath);
- if (targetIndex < 0) {
- return -1;
- }
- const targetSong = this.songs[targetIndex];
- let mutated = false;
- if (mergeCoverWithPriority(targetSong, payload.pixelMapPath, payload.isCustomCover ?? 0)) {
- mutated = true;
- }
- if (payload.md5Str && payload.md5Str.length > 0 && targetSong.md5Str !== payload.md5Str) {
- targetSong.md5Str = payload.md5Str;
- 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 ? targetIndex : -1;
- }
- 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 loadAccounts(): void {
- this.accounts = this.webdavManager.getAllWebDavAccounts();
- }
- // 加载文件列表
- private loadFiles(): void {
- if (!this.selectedAccount) {
- this.getUIContext().getPromptAction().showToast({ message: '请先选择账户' });
- return;
- }
- console.info('heanup '+this.selectedAccount.name+'type '+this.selectedAccount.webType)
- this.scheduleDirectoryRefresh(this.selectedAccount.filepath,
- () => this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount), '加载文件失败')
- }
- // 进入文件夹
- private enterFolder(folder: FileInfo): void {
- const targetPath = this.webdavManager.prepareEnterFolder(folder)
- if (!targetPath || !this.selectedAccount) {
- return
- }
- Logger.info(TAG, `进入文件夹: current=${this.currentDirectoryPath}, target=${targetPath}`)
- this.scheduleDirectoryRefresh(targetPath,
- () => this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount, targetPath), '进入文件夹失败')
- }
- // 检查是否为当前目录的直接子项
- private isDirectChildOfCurrentPath(folder: FileInfo): boolean {
- const currentPath = this.currentDirectoryPath || '';
- // 如果在根目录(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()){
- const previousPath = this.webdavManager.prepareGoBack()
- if (!previousPath || !this.selectedAccount) {
- return
- }
- Logger.info(TAG, `返回上级目录: current=${this.currentDirectoryPath}, previous=${previousPath}`)
- this.scheduleDirectoryRefresh(previousPath,
- () => this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount, previousPath), '返回失败')
- }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 getCurrentPlaybackQueue(): VideoItem[] {
- if (this.isSearchMode && this.searchText.length > 0 && this.filteredList.length > 0) {
- return this.filteredList;
- }
- return this.songs;
- }
- private resolveSongPlayIndex(song: VideoItem, fallbackIndex: number, queue: VideoItem[]): number {
- if (queue.length <= 0) {
- return -1;
- }
- if (song && StrUtil.isNotEmpty(song.filePath)) {
- for (let i = 0; i < queue.length; i++) {
- if (queue[i].filePath === song.filePath) {
- return i;
- }
- }
- }
- if (fallbackIndex >= 0 && fallbackIndex < queue.length) {
- return fallbackIndex;
- }
- return 0;
- }
- // 播放WebDAV歌曲
- private playSong(song: VideoItem, index: number,isJump:boolean=false): void {
- try {
- const queue: VideoItem[] = this.getCurrentPlaybackQueue();
- Logger.info(TAG, `heanup === WebDAV playSong 方法被调用 ===`);
- Logger.info(TAG, 'heanup 播放指定歌曲: ' + song.name + ', 索引: ' + index);
- Logger.info(TAG, 'heanup 播放队列长度: ' + queue.length);
- const playIndex = this.resolveSongPlayIndex(song, index, queue);
- if (playIndex < 0 || playIndex >= queue.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 = queue.map(s => s.webdav_account_id).filter(id => id);
- Logger.info(TAG, `heanup 当前歌曲列表中有 ${accountIds.length}/${queue.length} 首歌曲设置了webdav_account_id`);
- // 如果发现歌曲缺少webdav_account_id,立即设置
- queue.forEach((item) => {
- 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}`);
- }
- });
- }
- const videoItems: VideoItem[] = queue.slice();
- setWebdavPlaylist(videoItems, playIndex);
- Logger.info(TAG, `heanup 保存WebDAV歌曲到全局内存,长度: ${videoItems.length}, 索引: ${playIndex}`);
- void this.playbackController.playQueue(
- videoItems,
- playIndex,
- isJump ? 'controller-open' : 'webdav-main'
- ).then(() => {
- Logger.info(TAG, `heanup WebDAV 直连播放成功,索引: ${playIndex}`);
- }).catch((error: Error) => {
- Logger.error(TAG, `播放歌曲失败: ${error.message}`);
- this.getUIContext().getPromptAction().showToast({ message: '播放失败' });
- });
- } 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.currentDirectoryPath || '/';
- 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;
- }
- if (!this.selectedAccount) {
- return
- }
- const targetPath = this.webdavManager.prepareEnterFolderFromPath(crumb.path)
- Logger.info(TAG, `面包屑导航: current=${this.currentDirectoryPath}, target=${targetPath}`)
- this.scheduleDirectoryRefresh(targetPath, () => this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount, targetPath),
- '导航到面包屑路径失败')
- }
- @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, shouldScrollToTop: boolean = true) {
- this.sortType = index;
- if (this.isSearchMode && this.searchText.length > 0) {
- if (this.filteredList.length > 0) {
- this.filteredList = this.sortSongsForType([...this.filteredList], index)
- }
- if (this.filteredFolderList.length > 0) {
- this.filteredFolderList = this.sortFoldersForType([...this.filteredFolderList], index)
- }
- this.refreshDisplaySongs(this.filteredList, shouldScrollToTop)
- return
- }
- this.songs = this.sortSongsForType([...this.songs], index)
- this.visibleFoldersState = this.sortFoldersForType([...this.visibleFoldersState], index)
- this.updateListData(this.songs,true, shouldScrollToTop)
- }
- 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())) {
- //左侧滑动按钮
- TitleBarPointLightButton({
- iconResource: $r('sys.symbol.sort'),
- iconSize: 25,
- pointColor: this.themeColor,
- clickScale: 0.6,
- clickHandler: () => {
- this.getUIContext().animateTo({ duration: 555 }, () => {
- this.isShowDrawer = !this.isShowDrawer
- this.offsetX = 0
- })
- }
- })
- }else{
- //左侧搜索返回按钮
- TitleBarPointLightButton({
- iconResource: $r('sys.symbol.chevron_left'),
- iconSize: 24,
- pointColor: this.themeColor,
- clickScale: 0.6,
- clickHandler: () => {
- if(this.isSearchMode){
- this.isSearchMode = false
- this.searchController.stopEditing()
- this.onSearchInput('')
- }else{
- if(this.webdavManager.canGoBack()){
- this.goBack()
- }
- }
- }
- })
- }
- 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.commitSearchHistory(value)
- 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) {
- TitleBarPointLightButton({
- iconResource: $r('sys.symbol.magnifyingglass'),
- iconSize: 25,
- pointColor: this.themeColor,
- clickScale: 0.6,
- clickHandler: () => {
- this.isSearchMode = true
- this.loadSearchHistory()
- this.restoreCurrentDirectorySearchView()
- void this.webdavManager.ensureGlobalSearchIndex(this.selectedAccount)
- }
- })
- //排序按钮
- TitleBarPointLightButton({
- iconResource: $r('sys.symbol.list_number'),
- iconSize: 25,
- pointColor: this.themeColor,
- clickScale: 0.6,
- menuBuilder: () => {
- this.SortMenuBuilder()
- }
- })
- //添加/上传综合按钮
- TitleBarPointLightButton({
- iconResource: $r('sys.symbol.plus'),
- iconSize: 25,
- pointColor: this.themeColor,
- clickScale: 0.6,
- menuBuilder: () => {
- this.MoreMenuBuilder()
- }
- })
- .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%')
- }
- @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,
- tasksVersion: this.downloadCenterRefreshVersion,
- activeTasksProp: this.downloadCenterActiveTasks,
- completedTasksProp: this.downloadCenterCompletedTasks,
- selectedIndexes: $downloadCenterTabIndex,
- onPauseTask: (taskId: string) => {
- this.pauseDownloadTask(taskId);
- },
- onResumeTask: (taskId: string) => {
- this.resumeDownloadTask(taskId);
- },
- onDeleteTask: (taskId: string) => {
- this.deleteDownloadTask(taskId);
- },
- onPlayCompletedTask: (taskId: string) => {
- this.playCompletedDownloadTask(taskId);
- },
- onClose: () => {
- this.isShowDownloadCenter = false;
- }
- })
- .width('100%')
- .height('100%')
- }
- //搜索功能的实现
- @State searchText: string = ''; // 用户输入内容
- @State filteredList: Array<VideoItem> = []; // 过滤后的歌曲结果
- @State filteredFolderList: Array<FileInfo> = []; // 过滤后的文件夹结果
- @State searchHistoryItems: string[] = [];
- private readonly searchHistoryScope: string = 'webdav_music';
- private loadSearchHistory(): void {
- this.searchHistoryItems = SearchHistoryUtil.load(this.searchHistoryScope);
- }
- private saveSearchHistory(keyword: string): void {
- this.searchHistoryItems = SearchHistoryUtil.save(this.searchHistoryScope, keyword);
- }
- private commitSearchHistory(keyword: string): void {
- const normalized = keyword.trim();
- if (normalized.length === 0) {
- return;
- }
- this.saveSearchHistory(normalized);
- }
- private applySearchHistory(keyword: string): void {
- this.searchText = keyword;
- this.searchController.stopEditing();
- this.onSearchInput(keyword);
- }
- private restoreCurrentDirectorySearchView(shouldScrollToTop: boolean = true): void {
- this.filteredList = []
- this.filteredFolderList = []
- this.songs = this.sortSongsForType([...this.songs], this.sortType)
- this.visibleFoldersState = this.sortFoldersForType([...this.visibleFoldersState], this.sortType)
- this.isSearchLoading = false
- this.refreshDisplaySongs(this.songs, shouldScrollToTop)
- this.syncSelectionAfterRefresh()
- }
- 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)
- this.syncSelectionAfterRefresh()
- } catch (error) {
- if (ticket !== this.searchTicket) {
- return
- }
- this.isSearchLoading = false
- this.filteredList = []
- this.filteredFolderList = []
- this.refreshDisplaySongs([])
- this.syncSelectionAfterRefresh()
- 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.loadSearchHistory();
- this.restoreCurrentDirectorySearchView();
- return;
- }
- if (!this.isSearchMode) {
- this.isSearchMode = true;
- }
- this.isSearchLoading = true;
- void this.applyGlobalSearch(keyword, ticket);
- }
- @Builder
- private SearchHistoryView(): void {
- Column({ space: 10 }) {
- Row() {
- Text('搜索历史')
- .fontSize(14)
- .fontWeight(FontWeight.Medium)
- .fontColor($r('app.color.text_color'))
- Blank()
- Button('清空')
- .fontSize(12)
- .fontColor(this.themeColor)
- .backgroundColor(Color.Transparent)
- .padding({ left: 8, right: 8, top: 4, bottom: 4 })
- .onClick(() => {
- SearchHistoryUtil.clear(this.searchHistoryScope);
- this.searchHistoryItems = [];
- })
- }
- .width('100%')
- Flex({ wrap: FlexWrap.Wrap }) {
- ForEach(this.searchHistoryItems, (keyword: string) => {
- Button(keyword)
- .fontSize(12)
- .fontColor($r('app.color.text_color'))
- .backgroundColor(this.isDarkMode ? '#222222' : '#F2F3F5')
- .borderRadius(16)
- .margin({ right: 8, bottom: 8 })
- .padding({ left: 12, right: 12, top: 8, bottom: 8 })
- .onClick(() => {
- this.applySearchHistory(keyword);
- })
- })
- }
- .width('100%')
- }
- .width('100%')
- .padding({ left: 20, right: 20, top: this.getSearchLoadingTopOffset(), bottom: 10 })
- }
- 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.isMusicCoverHomeBackgroundActive()
- ? this.customizeMusicBgPath
- : (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.isMusicCoverHomeBackgroundActive() || this.isCustomizeBg ? 0.1 : 0,
- lightUpDegree:this.bgBrightness})
- .backgroundBlurStyle(this.isMusicCoverHomeBackgroundActive()
- ? BlurStyle.BACKGROUND_ULTRA_THICK :
- BlurStyle.NONE)
- .bindSheet($$this.isShowSongPropertySheet, this.SongPropertySheetBuilder(), {
- height: '99%',
- dragBar: true,
- showClose: true,
- preferType: SheetType.CENTER,
- blurStyle: BlurStyle.Thin,
- title: { title: '歌曲属性' }
- })
- .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
- }
- private getSearchLoadingTopOffset(): number {
- return this.topSafeHeight + (this.currentDirectoryPath !== '' ? 100 : 56)
- }
- private shouldShowSearchIndexingTip(): boolean {
- return this.isSearchMode && this.searchText.length > 0 && this.isSearchLoading
- }
- private getListContentStartOffset(): number {
- if (this.shouldShowSearchIndexingTip()) {
- return 8
- }
- if(this.isLoading)
- return 8
- return this.topSafeHeight + 85
- }
- // 目录切换时统一在当前可见的账户图标外层显示转圈,避免页面中间闪大 loading。
- @Builder
- private buildDirectoryNavigationIndicator(iconSize: number, containerSize: number) {
- Stack({ alignContent: Alignment.Center }) {
- if (this.isDirectoryNavigationLoading) {
- // 使用环形进度外圈承载目录切换态,避免在标题栏或页面中央再出现第二套 loading。
- Progress({ value: 0, total: 100, type: ProgressType.Ring })
- .width(containerSize + 8)
- .height(containerSize + 8)
- .color(this.themeColor)
- .style({ strokeWidth: 2.5, status: ProgressStatus.LOADING, shadow: false })
- .scale({ x: -1, y: 1 })
- }
- Image(this.selectedAccount.coverPath ?
- this.selectedAccount.coverPath : getCloudDiskIcon(this.selectedAccount.webType))
- .width(iconSize)
- .height(iconSize)
- .alt($r('app.media.cloudDisk'))
- }
- .width(containerSize)
- .height(containerSize)
- .borderRadius(containerSize / 2)
- .backgroundColor('#EFEFEF')
- }
- @Builder
- breaker() {
- // 面包屑导航
- Column({ space: 8 }) {
- if (!this.isSearchMode || this.webdavManager.canGoBack()) {
- Row({ space: 8 }) {
- // 根目录只展示账号图标和加载态,不再承载返回动作;子目录才点击返回上级。
- if (this.webdavManager.canGoBack()) {
- Row() {
- this.buildDirectoryNavigationIndicator(18, 24)
- }
- .width(24)
- .height(24)
- .margin({left:5})
- .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.92 })
- .onClick(() => this.goBack())
- .bindContentCover($$this.isShowDownloadCenter, this.DownloadCenterBuilder(),
- {
- modalTransition:ModalTransition.DEFAULT,
- onWillDisappear: () => {
- this.isShowDownloadCenter = false
- },
- })
- } else {
- Row() {
- this.buildDirectoryNavigationIndicator(18, 24)
- }
- .width(24)
- .height(24)
- .margin({left:5})
- .bindContentCover($$this.isShowDownloadCenter, this.DownloadCenterBuilder(),
- {
- modalTransition:ModalTransition.DEFAULT,
- onWillDisappear: () => {
- this.isShowDownloadCenter = false
- },
- })
- }
- Row({ space: 4 }) {
- if (this.breadcrumbs.length === 0) {
- Text('根目录')
- .fontSize(15)
- .fontColor($r('app.color.text_color'))
- .maxLines(1)
- .textOverflow({ overflow: TextOverflow.Ellipsis })
- } else {
- 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.shouldShowSearchIndexingTip()) {
- 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)
- }
- .alignSelf(ItemAlign.Center)
- .padding({ left: 20, right: 20, top: this.getSearchLoadingTopOffset() })
- }
- // 文件列表(文件夹 + 歌曲)
- if ((this.isSearchMode ? this.filteredFolderList.length : this.visibleFoldersState.length) > 0 ||
- this.dataSource.totalCount() > 0) {
- List({ scroller: this.listScroller ,space: 0 }) {
- // 显示文件夹 - 只显示当前目录下的直接子文件夹
- ForEach(this.isSearchMode ? this.filteredFolderList : this.visibleFoldersState, (folder: FileInfo) => {
- if (this.isMultiSelect) {
- ListItem() {
- this.buildFolderItem(folder)
- }
- .clickEffect({ level: ClickEffectLevel.MIDDLE })
- } else {
- ListItem() {
- this.buildFolderItem(folder)
- }
- .clickEffect({ level: ClickEffectLevel.MIDDLE })
- .swipeAction({
- end: this.buildSwipeDeleteAction(() => {
- this.confirmDeleteSingleFolder(folder)
- }),
- edgeEffect: SwipeEdgeEffect.None
- })
- }
- }, (folder: FileInfo) => folder.name+folder.fileName)
- // 显示歌曲
- LazyForEach(this.dataSource, (song: VideoItem, index: number) => {
- if (this.isMultiSelect) {
- ListItem() {
- this.buildSongItem(song, index)
- }
- .clickEffect({ level: ClickEffectLevel.MIDDLE })
- } else {
- ListItem() {
- this.buildSongItem(song, index)
- }
- .clickEffect({ level: ClickEffectLevel.MIDDLE })
- .swipeAction({
- end: this.buildSwipeDeleteAction(() => {
- this.confirmDeleteSingleSong(song)
- }),
- edgeEffect: SwipeEdgeEffect.None
- })
- }
- }, (item: VideoItem, index: number) => {
- return buildWebDavSongItemRenderKey(item.filePath, index, item.pixelMapPath)
- })
- }
- .scrollBar(BarState.Off)
- .onScrollIndex((start: number, end: number) => {
- this.updateVisibleRemoteThumbRange(start, end)
- })
- .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.getListContentStartOffset())
- .contentEndOffset(this.bottomSafeHeight+70)
- .layoutWeight(1)
- .margin({ top: 4 })
- } else if (this.isSearchMode && this.searchText.length === 0 && this.searchHistoryItems.length > 0) {
- this.SearchHistoryView()
- } 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.getSelectedEntryCount() > 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.canOperateOnSelectedSongs())
- .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.canOperateOnSelectedSongs())
- .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 })
- }
- private getPointLightItemKey(prefix: string, value: string): string {
- return `${prefix}_${value}`
- }
- private handlePointLightTouch(itemKey: string, event: TouchEvent): void {
- if (deviceInfo.sdkApiVersion < 20) {
- return
- }
- if (event.type === TouchType.Down) {
- this.activePointLightItemKey = itemKey
- this.pointLightOptions = {
- color: this.themeColor,
- intensity: 1,
- height: 60
- }
- return
- }
- if ((event.type === TouchType.Up || event.type === TouchType.Cancel) && this.activePointLightItemKey === itemKey) {
- this.activePointLightItemKey = ''
- this.pointLightOptions = undefined
- }
- }
- private getPointLightOptions(itemKey: string): hdsEffect.PointLightOptions | undefined {
- if (this.activePointLightItemKey !== itemKey) {
- return undefined
- }
- return this.pointLightOptions
- }
- // 文件夹列表项
- @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)
- Checkbox({ name: `folder_checkbox_${this.resolveFolderSelectionKey(folder)}` })
- .select(this.isFolderSelected(folder))
- .selectedColor(this.themeColor)
- .opacity(this.isMultiSelect ? 1 : 0)
- .animation({
- duration: 300,
- curve: 'Smooth'
- })
- .visibility(this.isMultiSelect ? Visibility.Visible : Visibility.None)
- .onChange((checked: boolean) => this.handleFolderCheckboxSelection(folder, checked))
- .margin({ left: 10, top: 8, bottom: 8, right: 12 })
- .width(22)
- .height(22)
- }
- }
- .reuseId('dir_item')
- .width('100%')
- .padding(12)
- .backgroundColor(Color.Transparent)
- .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.8 })
- .onClick(() => {
- if (this.ignoreTapAfterExitMultiSelect) {
- return;
- }
- if (this.isMultiSelect) {
- this.toggleFolderSelection(folder);
- return;
- }
- if (this.isSearchMode && this.searchText.length > 0) {
- this.isSearchMode = false
- this.searchController.stopEditing()
- this.onSearchInput('')
- }
- this.enterFolder(folder);
- this.breadcrumbs = this.webdavManager.getBreadcrumbs();
- })
- .bindContextMenu(this.FolderLongPressMenuBuilder(folder), ResponseType.LongPress,
- {
- preview: MenuPreviewMode.IMAGE
- })
- .bindContextMenu(this.FolderLongPressMenuBuilder(folder), ResponseType.RightClick,
- {
- preview: MenuPreviewMode.IMAGE
- })
- .onTouch((event: TouchEvent) => {
- this.handlePointLightTouch(this.getPointLightItemKey('webdav_folder', folder.href), event)
- })
- .visualEffect(deviceInfo.sdkApiVersion >= 20
- ? new hdsEffect.HdsEffectBuilder()
- .pointLight({
- options: this.getPointLightOptions(this.getPointLightItemKey('webdav_folder', folder.href)),
- illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT
- })
- .buildEffect()
- : undefined)
- }
- // 歌曲列表项
- @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(){
- if (StrUtil.isNotEmpty(song.md5Str)) {
- Text(this.buildSongQualityLabel(song))
- .fontSize(10)
- .fontColor($r('app.color.index_tab_font_color'))
- .fontWeight(500)
- .padding({ top: 2, right: 5, left: 5, bottom: 2 })
- .borderRadius(6)
- .margin({right:3})
- .backgroundColor('#FFC107')
- .opacity(0.92)
- }
- 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)
- PlayingIndicator({
- isActive: this.currentSong?.filePath == song.filePath && !this.isMultiSelect,
- indicatorSize: 18,
- marginRight: 12,
- marginTop: 8,
- marginBottom: 8
- })
- }
- }
- }
- .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
- })
- .onTouch((event: TouchEvent) => {
- this.handlePointLightTouch(this.getPointLightItemKey('webdav_song', song.filePath), event)
- })
- .visualEffect(deviceInfo.sdkApiVersion >= 20
- ? new hdsEffect.HdsEffectBuilder()
- .pointLight({
- options: this.getPointLightOptions(this.getPointLightItemKey('webdav_song', song.filePath)),
- illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT
- })
- .buildEffect()
- : undefined)
- }
- @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.rename')),
- content: '重命名'
- })
- .visibility(this.canRenameRemoteSong(song) ? Visibility.Visible : Visibility.None)
- .onClick(() => {
- this.showRenameSongDialog(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)
- }
- @Builder
- FolderLongPressMenuBuilder(folder: FileInfo) {
- Scroll() {
- Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) {
- Menu() {
- MenuItem({
- symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.checkmark_square_on_square')),
- content: $r('app.string.select_all')
- })
- .onClick(() => {
- this.enterFolderMultiSelect(folder);
- })
- MenuItem({
- symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.rename')),
- content: '重命名'
- })
- .visibility(this.canRenameRemoteFolder() ? Visibility.Visible : Visibility.None)
- .onClick(() => {
- this.showRenameFolderDialog(folder);
- })
- MenuItem({
- symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.trash')),
- content: $r('app.string.delete')
- })
- .onClick(() => {
- this.confirmDeleteSingleFolder(folder);
- })
- }
- .font({ size: 14, weight: FontWeight.Normal })
- }
- .width(180)
- }
- .height('auto')
- .enableScrollInteraction(true)
- .scrollBar(BarState.Off)
- }
-
-
- // 对话框控制器
- private createFolderDialogController: CustomDialogController | null = null;
- private renameSongDialogController: 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.currentDirectoryPath || this.selectedAccount.filepath || '/');
- 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.currentDirectoryPath || this.selectedAccount.filepath || '/');
- 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;
- }
- }
- }
|