| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791 |
- import { SymbolGlyphModifier } from '@kit.ArkUI'
- import { PreferencesUtil, StrUtil, ToastUtil } from '@pura/harmony-utils'
- import { BusinessError, emitter } from '@kit.BasicServicesKit'
- import { common } from '@kit.AbilityKit'
- import { CommonConstants } from '../common/constants/CommonConstants'
- import { EventConstants } from '../common/constants/EventConstants'
- import MediaTable from '../common/util/MediaTable'
- import { MenuModifier, SymbolGlyphFancyModifier } from '../common/util/AttributeModifierUtil'
- import { BreakpointType, BreakpointTypeEnum } from '../common/util/BreakpointSystem'
- import Logger from '../common/util/Logger'
- import { RemoteDriveManager } from '../common/util/RemoteDriveManager'
- import { cloneVideoItem } from '../common/util/RemotePlayerUtil'
- import { SearchHistoryUtil } from '../common/util/SearchHistoryUtil'
- import { VideoItem } from '../viewmodel/VideoItem'
- import { WebDavAccount } from '../viewmodel/WebDavAccount'
- import { Playlist } from '../viewmodel/Playlist'
- import { ConfigTitle } from './ConfigTitle'
- import { PointLightActionButton } from './PointLight/PointLightActionButton'
- import { PointLightContentButton } from './PointLight/PointLightContentButton'
- import { TitleBarPointLightButton } from './PointLight/TitleBarPointLightButton'
- import { DeleteComptent } from './DeleteComptent'
- import { SettingPage } from '../pages/SettingPage'
- import { FindAlbumDetail } from './FindAlbumDetail'
- import { PlayingIndicator } from './PlayingIndicator'
- import {
- getFindPlaylistId,
- getFindPlaylistName,
- getFindVideoItems,
- setFindPlaylist
- } from '../common/util/FindPlaylistStore'
- import { PlaylistPlayRequest, savePendingPlaylistPlay } from '../common/util/PlaylistPlayRequestStore'
- import PlaylistTable from '../common/util/PlaylistTable'
- import { convertPlaylistSongsToVideoItems } from '../pages/PlaylistDetailPage'
- import { getWebdavVideoItems, setWebdavPlaylist } from '../pages/WebDavMainPage'
- import { getNavidromeVideoItems, setNavidromePlaylist } from '../common/util/NavidromePlaylistStore'
- import {
- buildPreferredRemotePlaybackPool,
- buildSortedDiscoverySongs,
- findPreferredRemotePlaybackAccountId,
- FindCollectionSortType,
- resolveQueueStartIndex,
- shouldWaitForIndexedRemotePlayback
- } from '../common/util/FindDiscoveryHelper'
- import { insertSongToNextPlayQueue, QueueInsertStatus } from '../common/util/PlayQueueHelper'
- import { resolveRemoteCoverForSong } from '../common/util/RemoteCoverResolver'
- @Builder
- export function FindViewBuilder() {
- FindView()
- }
- const TAG = 'FindView'
- const SWIPER_MAX_COUNT = 5
- const HOT_SECTION_COUNT = 10
- const REMOTE_POOL_COUNT = 32
- const REMOTE_VISIBLE_COVER_RESOLVE_LIMIT = 24
- const CLOUD_SECTION_COUNT = 18
- const RECENT_POOL_COUNT = 24
- const RECENT_SECTION_COUNT = 18
- const POPULAR_SECTION_COUNT = 18
- const FAVORITE_SECTION_COUNT = 18
- const TOP_PLAYED_COUNT = 30
- const FEATURED_ALBUM_COUNT = 18
- const CLOUD_ALBUM_COUNT = 18
- const PREFERRED_ALBUM_MIN_COUNT = 6
- function getFindSongKey(item: VideoItem, index: number): string {
- if (StrUtil.isNotEmpty(item.filePath)) {
- return item.filePath
- }
- if (StrUtil.isNotEmpty(item.name)) {
- return `${item.name}_${index}`
- }
- if (StrUtil.isNotEmpty(item.fileName)) {
- return `${item.fileName}_${index}`
- }
- return `find_song_${index}`
- }
- interface FindAlbumGroup {
- id: string
- title: string
- artist: string
- coverPath: string
- songCount: number
- songs: VideoItem[]
- isRemote: boolean
- sourceLabel: string
- }
- interface FindSongPage {
- id: string
- items: VideoItem[]
- }
- interface FindAlbumPage {
- id: string
- items: FindAlbumGroup[]
- }
- function getFindAlbumKey(item: FindAlbumGroup, index: number): string {
- if (StrUtil.isNotEmpty(item.id)) {
- return item.id
- }
- return `find_album_${index}`
- }
- function getFindSongPageKey(item: FindSongPage, index: number): string {
- if (StrUtil.isNotEmpty(item.id)) {
- return item.id
- }
- return 'find_song_page_' + index
- }
- function getFindPlaylistKey(item: Playlist, index: number): string {
- if (StrUtil.isNotEmpty(item.id)) {
- return item.id
- }
- return 'find_playlist_' + index
- }
- function getFindAlbumPageKey(item: FindAlbumPage, index: number): string {
- if (StrUtil.isNotEmpty(item.id)) {
- return item.id
- }
- return 'find_album_page_' + index
- }
- @Component
- export struct FindView {
- @StorageProp('isLandscape') isLandscape: boolean = false;
- @State isCustomizeBg: boolean = false //自定义背景界面
- @State isHomeBgFollowMusicCover: boolean = true //主页背景随当前播放封面
- @State blurValue: number = 0 //背景模糊
- @State bgBrightness: number = 0 //背景亮度
- @State customizeBgPath: string | undefined = '';
- @State isShowTitleBar: boolean = true //是否显示分类导航条
- private prevOffsetY: number = 0; // 记录上一次的Y轴偏移量
- @State autoHideTitle: boolean = true //滚动自动隐藏标题栏
- @Consume mType: number
- @Consume isShowDrawer: boolean
- @Consume offsetX: number
- @State private swiperSongs: VideoItem[] = []
- @State private hotSongs: VideoItem[] = []
- @State private remoteSongs: VideoItem[] = []
- @State private cloudSectionPages: FindSongPage[] = []
- @State private recentSongs: VideoItem[] = []
- @State private popularSongs: VideoItem[] = []
- @State private popularSectionPages: FindSongPage[] = []
- @State private favoriteSongs: VideoItem[] = []
- @State private cloudMoodSongs: VideoItem[] = []
- @State private heartPlaylists: Playlist[] = []
- @State private featuredAlbums: FindAlbumGroup[] = []
- @State private featuredAlbumPages: FindAlbumPage[] = []
- @State private cloudAlbums: FindAlbumGroup[] = []
- @State private cloudAlbumPages: FindAlbumPage[] = []
- @State @Watch('syncFindCanBackState') private isSearchMode: boolean = false
- @State @Watch('syncFindCanBackState') private isAlbumMode: boolean = false
- @State private currentAlbumId: string = ''
- @State private currentAlbumTitle: string = ''
- @State private currentAlbumArtist: string = ''
- @State private currentAlbumCoverPath: string = ''
- @State private currentAlbumSourceLabel: string = ''
- @State private currentAlbumSongs: VideoItem[] = []
- @State private currentAlbumDefaultSortType: number = FindCollectionSortType.TRACK_ASC
- @State private currentAlbumSupportRecentPlayedSort: boolean = false
- @State private searchText: string = ''
- @State private searchResults: VideoItem[] = []
- @State private searchHistoryItems: string[] = []
- @State private isSearchLoading: boolean = false
- @State private isRefreshing: boolean = false
- @State private isPageLoading: boolean = true
- @State private refreshText: string = '加载中...'
- @State private swiperIndex: number = 0
- @State private cloudSectionPageIndex: number = 0
- @State private featuredAlbumPageIndex: number = 0
- @State private cloudAlbumPageIndex: number = 0
- @State private recentSectionPageIndex: number = 0
- @State private popularSectionPageIndex: number = 0
- @State private favoriteSectionPageIndex: number = 0
- @State private refreshPullRatio: number = 1
- @State private maxRefreshingHeight: number = 100
- @State private pendingDeleteSongs: VideoItem[] = []
- searchController: SearchController = new SearchController()
- @StorageProp('currentBreakpoint') @Watch('onDiscoverBreakpointChange') currentBreakpoint: string = BreakpointTypeEnum.MD
- @StorageProp('windowWidth') windowWidth: number = 0
- @StorageProp('topSafeHeight') topSafeHeight: number = 0
- @StorageProp('bottomSafeHeight') bottomSafeHeight: number = 0
- @StorageProp('isDarkMode') isDarkMode: boolean = false
- @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR
- @StorageProp('customizeMusicBgPath') customizeMusicBgPath: string = ''
- @StorageLink('currentSong') @Watch('onHomeBgCurrentSongChange') currentSong: VideoItem | undefined = undefined
- @StorageLink('isPlaying') isPlaying: boolean = false
- private mediaTable?: MediaTable
- private playlistTable?: PlaylistTable
- private remoteDriveManager: RemoteDriveManager = RemoteDriveManager.getInstance()
- private localSongsPool: VideoItem[] = []
- private coveredSongsPool: VideoItem[] = []
- private remoteSongsPool: VideoItem[] = []
- private recentSongsPool: VideoItem[] = []
- private topPlayedSongsPool: VideoItem[] = []
- private favoriteSongsPool: VideoItem[] = []
- private featuredAlbumsPool: FindAlbumGroup[] = []
- private cloudAlbumsPool: FindAlbumGroup[] = []
- private searchRemoteSongsPool: VideoItem[] = []
- private remotePlaybackPool: VideoItem[] = []
- private readonly searchHistoryScope: string = 'find_music'
- private searchTicket: number = 0
- private cloudSectionPageVersion: number = 0
- private featuredAlbumPageVersion: number = 0
- private cloudAlbumPageVersion: number = 0
- private popularSectionPageVersion: number = 0
- private isRemoteBootstrapLoading: boolean = false
- private playlistSongsCache: Map<string, VideoItem[]> = new Map<string, VideoItem[]>()
- private heartPlaylistCoverTicket: number = 0
- private deleteComponentId: number = 0
- private remoteAccountMap: Map<string, WebDavAccount> = new Map<string, WebDavAccount>()
- private remoteCoverRepairTicket: number = 0
- private remoteCoverRepairTimer: number = -1
- aboutToAppear(): void {
- this.initSetting()
- this.syncFindCanBackState()
- let eventSetting: emitter.InnerEvent = { eventId: 333 }
- // 监听广播事件(通用设置配置更新)
- emitter.on(eventSetting, (eventData: emitter.EventData) => {
- this.initSetting()
- });
- emitter.on({ eventId: EventConstants.EVENT_FIND_VIEW_BACK }, () => {
- this.handleInnerBack()
- })
- this.ensureMediaTable()
- }
- aboutToDisappear(): void {
- AppStorage.setOrCreate('findCanBack', false)
- emitter.off(EventConstants.EVENT_FIND_VIEW_BACK)
- this.clearPendingRemoteCoverRepair()
- }
- 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.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
- }
- private shouldUseDiscoveryImageBackgroundOverlay(): boolean {
- return this.isCustomizeBg || this.isMusicCoverHomeBackgroundActive()
- }
- private getDiscoveryTopBarBackgroundColor(): ResourceColor | string {
- if (!this.isSearchMode && !this.isAlbumMode) {
- return Color.Transparent
- }
- if (this.shouldUseDiscoveryImageBackgroundOverlay()) {
- return this.isDarkMode ? '#66111111' : '#66FFFFFF'
- }
- return $r('app.color.start_window_background_blur')
- }
- private getDiscoverySearchBackgroundColor(): ResourceColor | string {
- if (this.isMusicCoverHomeBackgroundActive()) {
- return this.isDarkMode ? '#80111111' : '#80FFFFFF'
- }
- if (this.isCustomizeBg) {
- return this.isDarkMode ? '#66111111' : '#73FFFFFF'
- }
- return $r('app.color.input_background')
- }
- private onDiscoverBreakpointChange(): void {
- Logger.info(TAG, `FindView onDiscoverBreakpointChange breakpoint=${this.currentBreakpoint}, columns=${this.getDiscoverSectionColumns()}`)
- this.rebuildPagedSectionSources()
- }
- private ensureMediaTable(): void {
- if (!this.playlistTable) {
- try {
- this.playlistTable = new PlaylistTable(getContext(this) as common.Context)
- } catch (error) {
- Logger.warn(TAG, `初始化歌单数据库失败: ${this.toErrorMessage(error as Object)}`)
- }
- }
- if (this.mediaTable) {
- void this.loadDiscoveryContent(false)
- return
- }
- try {
- this.mediaTable = new MediaTable(getContext(this), () => {
- void this.loadDiscoveryContent(false)
- })
- } catch (error) {
- const message = this.toErrorMessage(error)
- Logger.error(TAG, `初始化媒体数据库失败: ${message}`)
- this.refreshText = '推荐加载失败,请下拉重试'
- this.isPageLoading = false
- }
- }
- private async loadDiscoveryContent(triggeredByRefresh: boolean): Promise<void> {
- if (!this.mediaTable) {
- return
- }
- if (triggeredByRefresh) {
- this.isRefreshing = true
- this.refreshText = '正在刷新'
- } else if (this.isPageLoading) {
- this.refreshText = '加载中...'
- }
- try {
- const allSongs = await this.mediaTable.queryAllVideos()
- const uniqueLocalSongs = this.filterUniqueSongs(allSongs)
- const coveredSongs = this.filterSongsWithCover(uniqueLocalSongs)
- const requestResults = await Promise.all([
- this.mediaTable.queryRemoteSongsAsync(REMOTE_POOL_COUNT),
- this.mediaTable.queryRecentPlayedRecordsAsync(RECENT_POOL_COUNT),
- this.queryTopPlayedSongs(TOP_PLAYED_COUNT),
- this.queryFavoriteSongs(),
- this.queryPlaylists()
- ])
- const remoteSongs = requestResults[0] as VideoItem[]
- const recentSongs = requestResults[1] as VideoItem[]
- const topPlayedSongs = requestResults[2] as VideoItem[]
- const favoriteSongs = requestResults[3] as VideoItem[]
- const playlists = requestResults[4] as Playlist[]
- const uniqueRemoteSongs = this.filterUniqueSongs(remoteSongs)
- const uniqueRecentSongs = this.filterUniqueSongs(recentSongs)
- const uniqueTopPlayedSongs = this.filterUniqueSongs(topPlayedSongs)
- const uniqueFavoriteSongs = this.filterUniqueSongs(favoriteSongs)
- const sortedRecentSongs = buildSortedDiscoverySongs(uniqueRecentSongs, FindCollectionSortType.RECENT_DESC)
- const sortedFavoriteSongs = buildSortedDiscoverySongs(uniqueFavoriteSongs, FindCollectionSortType.NAME_ASC)
- this.playlistSongsCache.clear()
- this.localSongsPool = uniqueLocalSongs
- this.coveredSongsPool = coveredSongs
- this.remotePlaybackPool = []
- this.applyRemoteDiscoverySongs(uniqueRemoteSongs)
- this.recentSongsPool = sortedRecentSongs
- this.topPlayedSongsPool = uniqueTopPlayedSongs
- this.favoriteSongsPool = sortedFavoriteSongs
- this.heartPlaylists = playlists
- this.featuredAlbumsPool = this.buildAlbumGroups(this.localSongsPool, false)
- this.searchRemoteSongsPool = []
- this.swiperSongs = this.pickRandomSongs(this.coveredSongsPool, SWIPER_MAX_COUNT)
- this.hotSongs = this.pickPreferredSongs(this.localSongsPool, HOT_SECTION_COUNT)
- this.recentSongs = this.recentSongsPool.slice(0, Math.min(RECENT_SECTION_COUNT, this.recentSongsPool.length))
- this.popularSongs = this.pickPreferredSongs(this.topPlayedSongsPool, POPULAR_SECTION_COUNT)
- this.favoriteSongs = this.favoriteSongsPool.slice(0, Math.min(FAVORITE_SECTION_COUNT, this.favoriteSongsPool.length))
- this.featuredAlbums = this.pickAlbumGroups(this.featuredAlbumsPool, FEATURED_ALBUM_COUNT)
- this.swiperIndex = 0
- this.resetPagedSectionIndices()
- this.rebuildPagedSectionSources()
- this.refreshText = ''
- this.heartPlaylistCoverTicket += 1
- void this.resolveHeartPlaylistCovers(this.heartPlaylistCoverTicket, playlists)
- if (uniqueRemoteSongs.length > 0) {
- void this.prepareRemoteAccounts()
- this.scheduleVisibleRemoteCoverRepair(false, 1200)
- } else {
- void this.bootstrapRemoteDiscoverySongsIfNeeded()
- }
- Logger.info(
- TAG,
- `[remote-debug] loadDiscoveryContent rawRemote=${remoteSongs.length}, uniqueRemote=${uniqueRemoteSongs.length}, ` +
- `sample=${this.buildSongDebugLog(uniqueRemoteSongs)}`
- )
- Logger.info(
- TAG,
- `发现页加载完成: local=${this.localSongsPool.length}, swiper=${this.swiperSongs.length}, remote=${this.remoteSongs.length}, recent=${this.recentSongs.length}, top=${this.topPlayedSongsPool.length}`
- )
- } catch (error) {
- const message = this.toErrorMessage(error)
- Logger.error(TAG, `发现页加载失败: ${message}`)
- this.swiperSongs = []
- this.hotSongs = []
- this.cloudMoodSongs = []
- this.remoteSongs = []
- this.cloudSectionPages = []
- this.recentSongs = []
- this.popularSongs = []
- this.popularSectionPages = []
- this.favoriteSongs = []
- this.heartPlaylists = []
- this.localSongsPool = []
- this.coveredSongsPool = []
- this.remoteSongsPool = []
- this.recentSongsPool = []
- this.topPlayedSongsPool = []
- this.favoriteSongsPool = []
- this.featuredAlbumsPool = []
- this.cloudAlbumsPool = []
- this.searchRemoteSongsPool = []
- this.remotePlaybackPool = []
- this.featuredAlbums = []
- this.featuredAlbumPages = []
- this.cloudAlbums = []
- this.cloudAlbumPages = []
- this.refreshText = '推荐加载失败,请下拉重试'
- } finally {
- this.isRefreshing = false
- this.isPageLoading = false
- }
- }
- private async queryPlaylists(): Promise<Playlist[]> {
- if (!this.playlistTable) {
- return []
- }
- try {
- const playlists = await this.playlistTable.queryAllPlaylists()
- return playlists.filter((item: Playlist) => StrUtil.isNotEmpty(item.id) && StrUtil.isNotEmpty(item.name))
- } catch (error) {
- Logger.warn(TAG, `发现页查询歌单失败: ${this.toErrorMessage(error as Object)}`)
- return []
- }
- }
- private applyRemoteDiscoverySongs(items: VideoItem[], refreshSections: boolean = false): void {
- this.remoteSongsPool = items
- this.cloudAlbumsPool = this.buildAlbumGroups(this.remoteSongsPool, true)
- this.cloudMoodSongs = this.pickPreferredSongs(this.remoteSongsPool, HOT_SECTION_COUNT)
- this.remoteSongs = this.pickPreferredSongs(this.remoteSongsPool, CLOUD_SECTION_COUNT)
- this.cloudAlbums = this.pickAlbumGroups(this.cloudAlbumsPool, CLOUD_ALBUM_COUNT)
- this.logRemoteDiscoveryCoverState('remoteSongsPool', this.remoteSongsPool)
- this.logRemoteDiscoveryCoverState('漫步云端', this.remoteSongs)
- this.logRemoteDiscoveryCoverState('云卷云舒', this.cloudMoodSongs)
- if (refreshSections) {
- this.cloudSectionPageIndex = 0
- this.cloudAlbumPageIndex = 0
- this.updateCloudSectionPages()
- this.updateCloudAlbumPages()
- }
- }
- private cacheRemoteAccounts(accounts: WebDavAccount[]): void {
- this.remoteAccountMap.clear()
- for (let i = 0; i < accounts.length; i++) {
- const account = accounts[i]
- if (account.id !== undefined && account.id !== null) {
- this.remoteAccountMap.set(account.id.toString(), account)
- }
- }
- }
- private async prepareRemoteAccounts(): Promise<WebDavAccount[]> {
- try {
- const context = getContext(this) as common.Context
- this.remoteDriveManager.setContext(context)
- await this.remoteDriveManager.createWebDavTableInDB()
- await this.remoteDriveManager.queryWebDavAccountsFromDB()
- const accounts: WebDavAccount[] = this.remoteDriveManager.getAllWebDavAccounts()
- .filter((account: WebDavAccount) => account.id !== undefined && account.id !== null && account.id > 0)
- this.cacheRemoteAccounts(accounts)
- return accounts
- } catch (error) {
- Logger.warn(TAG, `发现页加载远程账号失败: ${this.toErrorMessage(error)}`)
- this.remoteAccountMap.clear()
- return []
- }
- }
- private async ensureRemoteAccountMapReady(): Promise<Map<string, WebDavAccount>> {
- if (this.remoteAccountMap.size > 0) {
- return this.remoteAccountMap
- }
- await this.prepareRemoteAccounts()
- return this.remoteAccountMap
- }
- private collectVisibleRemoteCoverTargets(): VideoItem[] {
- const targets: VideoItem[] = []
- targets.push(...this.remoteSongs)
- targets.push(...this.cloudMoodSongs)
- for (let index = 0; index < this.cloudAlbums.length; index += 1) {
- const group = this.cloudAlbums[index]
- if (group.songs.length > 0) {
- targets.push(group.songs[0])
- }
- }
- // 只修当前发现页真正可见的一小批远程歌曲,避免初次进入时后台任务过重。
- return this.filterUniqueSongs(targets).slice(0, REMOTE_VISIBLE_COVER_RESOLVE_LIMIT)
- }
- private refreshVisibleRemoteAlbumCovers(): void {
- for (let index = 0; index < this.cloudAlbumsPool.length; index += 1) {
- const coverSong = this.pickAlbumCoverSong(this.cloudAlbumsPool[index].songs)
- this.cloudAlbumsPool[index].coverPath = coverSong?.pixelMapPath ?? ''
- }
- for (let index = 0; index < this.cloudAlbums.length; index += 1) {
- const coverSong = this.pickAlbumCoverSong(this.cloudAlbums[index].songs)
- this.cloudAlbums[index].coverPath = coverSong?.pixelMapPath ?? ''
- }
- }
- private clearPendingRemoteCoverRepair(): void {
- if (this.remoteCoverRepairTimer >= 0) {
- clearTimeout(this.remoteCoverRepairTimer)
- this.remoteCoverRepairTimer = -1
- }
- }
- private scheduleVisibleRemoteCoverRepair(persistResolvedCover: boolean = false, delayMs: number = 800): void {
- this.clearPendingRemoteCoverRepair()
- const ticket = ++this.remoteCoverRepairTicket
- this.remoteCoverRepairTimer = setTimeout(() => {
- this.remoteCoverRepairTimer = -1
- const coverTargets = this.collectVisibleRemoteCoverTargets()
- if (coverTargets.length === 0) {
- return
- }
- Logger.info(TAG,
- `[cover-debug] scheduleVisibleRemoteCoverRepair ticket=${ticket}, count=${coverTargets.length}, ` +
- `persist=${persistResolvedCover}, delayMs=${delayMs}`)
- void this.resolveRemoteSongCovers(coverTargets, persistResolvedCover)
- .then(() => {
- if (ticket !== this.remoteCoverRepairTicket) {
- return
- }
- this.refreshVisibleRemoteAlbumCovers()
- this.remoteSongs = this.remoteSongs.slice()
- this.cloudMoodSongs = this.cloudMoodSongs.slice()
- this.cloudAlbums = this.cloudAlbums.slice()
- this.cloudSectionPages = this.cloudSectionPages.slice()
- this.cloudAlbumPages = this.cloudAlbumPages.slice()
- this.logRemoteDiscoveryCoverState('remoteSongsPool', this.remoteSongsPool)
- this.logRemoteDiscoveryCoverState('漫步云端', this.remoteSongs)
- this.logRemoteDiscoveryCoverState('云卷云舒', this.cloudMoodSongs)
- })
- .catch((error: Object) => {
- Logger.warn(TAG, `发现页后台修正远程封面失败: ${this.toErrorMessage(error)}`)
- })
- }, delayMs)
- }
- private async resolveRemoteSongCovers(items: VideoItem[], persistResolvedCover: boolean): Promise<VideoItem[]> {
- if (!items || items.length === 0) {
- return items
- }
- const accountMap = await this.ensureRemoteAccountMapReady()
- const tasks: Promise<VideoItem>[] = items.map(async (item: VideoItem): Promise<VideoItem> => {
- const accountId = item.webdav_account_id ?? ''
- const account = accountMap.get(accountId)
- if (!account) {
- return item
- }
- const resolvedCover = await resolveRemoteCoverForSong(item, account)
- if (StrUtil.isEmpty(resolvedCover) || item.pixelMapPath === resolvedCover) {
- return item
- }
- Logger.info(
- TAG,
- `[cover-debug] repairRemoteSongCover title=${this.getSongTitle(item)}, type=${item.type}, acc=${accountId}, ` +
- `raw=${this.sanitizeCoverValue(item.pixelMapPath)}, resolved=${this.sanitizeCoverValue(resolvedCover)}`
- )
- item.pixelMapPath = resolvedCover
- if (persistResolvedCover && this.mediaTable) {
- await this.mediaTable.saveOrUpdateWebDavItem(cloneVideoItem(item))
- }
- return item
- })
- return await Promise.all(tasks)
- }
- private async ensureRemoteDiscoverySongsAvailable(): Promise<void> {
- if (!this.mediaTable) {
- return
- }
- if (this.remoteSongsPool.length > 0 || this.cloudAlbumsPool.length > 0) {
- return
- }
- const remoteSongs = this.filterUniqueSongs(await this.mediaTable.queryRemoteSongsAsync(REMOTE_POOL_COUNT))
- Logger.info(
- TAG,
- `[remote-debug] ensureRemoteDiscoverySongsAvailable dbRemote=${remoteSongs.length}, ` +
- `sample=${this.buildSongDebugLog(remoteSongs)}`
- )
- if (remoteSongs.length > 0) {
- this.applyRemoteDiscoverySongs(remoteSongs, true)
- this.scheduleVisibleRemoteCoverRepair()
- return
- }
- await this.bootstrapRemoteDiscoverySongsIfNeeded()
- }
- private async prepareActiveRemoteAccount(fallbackSongs?: VideoItem[]): Promise<WebDavAccount | undefined> {
- try {
- const context = getContext(this) as common.Context
- this.remoteDriveManager.setContext(context)
- await this.remoteDriveManager.createWebDavTableInDB()
- await this.remoteDriveManager.queryWebDavAccountsFromDB()
- const accounts: WebDavAccount[] = this.remoteDriveManager.getAllWebDavAccounts()
- if (accounts.length === 0) {
- return undefined
- }
- const activeAccount = this.remoteDriveManager.getActivatedWebDavAccount() || accounts[0]
- // 云端漫游优先挑“当前发现页已有歌曲所属账号”,避免固定落到第一个账号。
- const preferredAccountId = findPreferredRemotePlaybackAccountId(
- fallbackSongs ?? [],
- activeAccount?.id?.toString() ?? ''
- )
- if (StrUtil.isNotEmpty(preferredAccountId)) {
- for (let index = 0; index < accounts.length; index += 1) {
- if (accounts[index].id?.toString() === preferredAccountId) {
- Logger.info(
- TAG,
- `[remote-debug] prepareActiveRemoteAccount selected account=${accounts[index].name}, ` +
- `type=${accounts[index].webType}, preferredAccountId=${preferredAccountId}, ` +
- `fallbackSongs=${fallbackSongs?.length ?? 0}`
- )
- return accounts[index]
- }
- }
- }
- return activeAccount
- } catch (error) {
- Logger.warn(TAG, `发现页加载远程账号失败: ${this.toErrorMessage(error)}`)
- return undefined
- }
- }
- private async queryIndexedRemotePlaybackSongs(fallbackSongs: VideoItem[] = []): Promise<VideoItem[]> {
- const account = await this.prepareActiveRemoteAccount(fallbackSongs)
- if (!account) {
- Logger.warn(TAG, '[remote-debug] queryIndexedRemotePlaybackSongs skip: no active account')
- return []
- }
- const supportsGlobalIndex = this.remoteDriveManager.supportsGlobalSearchIndexForAccount(account)
- const isIndexReady = supportsGlobalIndex && this.remoteDriveManager.isGlobalSearchIndexReady(account)
- Logger.info(
- TAG,
- `[remote-debug] queryIndexedRemotePlaybackSongs account=${account.name}, type=${account.webType}, ` +
- `supportsGlobalIndex=${supportsGlobalIndex}, isIndexReady=${isIndexReady}`
- )
- if (supportsGlobalIndex && !isIndexReady) {
- ToastUtil.showToast('云端加载,请稍候')
- // 账号索引未就绪时,如果 DB 里已经有远程歌曲,就先用现有歌曲兜底播放。
- if (!shouldWaitForIndexedRemotePlayback(isIndexReady, fallbackSongs.length)) {
- Logger.info(
- TAG,
- `[remote-debug] queryIndexedRemotePlaybackSongs fallbackToDb account=${account.name}, ` +
- `fallbackSongCount=${fallbackSongs.length}`
- )
- void this.remoteDriveManager.ensureGlobalSearchIndex(account)
- return []
- }
- }
- const indexedSongs = await this.remoteDriveManager.getGlobalSearchIndexSongs(account)
- const clonedSongs = indexedSongs.map((item: VideoItem) => cloneVideoItem(item))
- Logger.info(TAG,
- `发现页全量云端索引池加载完成: account=${account.name}, type=${account.webType}, indexed=${clonedSongs.length}, ` +
- `sample=${this.buildSongDebugLog(clonedSongs)}`)
- return this.filterUniqueSongs(clonedSongs)
- }
- private async bootstrapRemoteDiscoverySongsIfNeeded(): Promise<void> {
- if (this.isRemoteBootstrapLoading || !this.mediaTable || this.remoteSongsPool.length > 0) {
- return
- }
- this.isRemoteBootstrapLoading = true
- try {
- const account = await this.prepareActiveRemoteAccount()
- if (!account) {
- return
- }
- Logger.info(TAG, `发现页开始预热远程歌曲: account=${account.name}, type=${account.webType}`)
- const seedSongs = await this.remoteDriveManager.getDiscoverySeedSongs(account, REMOTE_POOL_COUNT)
- Logger.info(
- TAG,
- `[remote-debug] bootstrapRemoteDiscoverySongs seedCount=${seedSongs.length}, sample=${this.buildSongDebugLog(seedSongs)}`
- )
- if (seedSongs.length === 0) {
- Logger.info(TAG, `发现页远程预热未拿到歌曲: account=${account.name}`)
- return
- }
- let savedCount = 0
- for (let index = 0; index < seedSongs.length; index += 1) {
- const seedSong = cloneVideoItem(seedSongs[index])
- if (!seedSong.webdav_account_id && account.id) {
- seedSong.webdav_account_id = account.id.toString()
- }
- if (StrUtil.isEmpty(seedSong.id)) {
- seedSong.id = seedSong.remote_rel_path || seedSong.filePath
- }
- const saved = await this.mediaTable.saveOrUpdateWebDavItem(seedSong)
- if (saved) {
- savedCount += 1
- } else {
- Logger.warn(
- TAG,
- `[remote-debug] bootstrapRemoteDiscoverySongs save failed name=${this.getSongTitle(seedSong)}, ` +
- `type=${seedSong.type}, account=${seedSong.webdav_account_id ?? ''}, path=${this.sanitizeSongPath(seedSong.filePath)}`
- )
- }
- }
- if (savedCount <= 0) {
- return
- }
- const refreshedRemoteSongs = this.filterUniqueSongs(await this.mediaTable.queryRemoteSongsAsync(REMOTE_POOL_COUNT))
- if (refreshedRemoteSongs.length === 0) {
- return
- }
- this.applyRemoteDiscoverySongs(refreshedRemoteSongs, true)
- this.scheduleVisibleRemoteCoverRepair()
- Logger.info(
- TAG,
- `发现页远程预热完成: saved=${savedCount}, remote=${refreshedRemoteSongs.length}, ` +
- `sample=${this.buildSongDebugLog(refreshedRemoteSongs)}`
- )
- } catch (error) {
- Logger.warn(TAG, `发现页远程预热失败: ${this.toErrorMessage(error)}`)
- } finally {
- this.isRemoteBootstrapLoading = false
- }
- }
- private queryTopPlayedSongs(limitCount: number): Promise<VideoItem[]> {
- return new Promise<VideoItem[]>((resolve) => {
- if (!this.mediaTable) {
- resolve([])
- return
- }
- this.mediaTable.queryByPlayCountDesc(limitCount, (result: VideoItem[]) => {
- resolve(result)
- })
- })
- }
- private queryFavoriteSongs(): Promise<VideoItem[]> {
- return new Promise<VideoItem[]>((resolve) => {
- if (!this.mediaTable) {
- resolve([])
- return
- }
- this.mediaTable.queryByisFav(1, (result: VideoItem[]) => {
- resolve(result)
- })
- })
- }
- private filterSongsWithCover(items: VideoItem[]): VideoItem[] {
- const result: VideoItem[] = []
- const seen: Set<string> = new Set<string>()
- for (let i = 0; i < items.length; i++) {
- const item = items[i]
- const filePath = item.filePath || ''
- if (StrUtil.isEmpty(filePath) || seen.has(filePath)) {
- continue
- }
- if (StrUtil.isEmpty(item.pixelMapPath)) {
- continue
- }
- seen.add(filePath)
- result.push(item)
- }
- return result
- }
- private filterUniqueSongs(items: VideoItem[]): VideoItem[] {
- const result: VideoItem[] = []
- const seen: Set<string> = new Set<string>()
- for (let i = 0; i < items.length; i++) {
- const item = items[i]
- const filePath = item.filePath || ''
- if (StrUtil.isEmpty(filePath) || seen.has(filePath)) {
- continue
- }
- seen.add(filePath)
- result.push(item)
- }
- return result
- }
- private pickRandomSongs(items: VideoItem[], count: number): VideoItem[] {
- const copy: VideoItem[] = items.slice()
- for (let i = copy.length - 1; i > 0; i--) {
- const randomIndex = Math.floor(Math.random() * (i + 1))
- const current = copy[i]
- copy[i] = copy[randomIndex]
- copy[randomIndex] = current
- }
- return copy.slice(0, Math.min(count, copy.length))
- }
- private pickPreferredSongs(items: VideoItem[], count: number): VideoItem[] {
- const songsWithCover = this.filterSongsWithCover(items)
- const result = this.pickRandomSongs(songsWithCover, count)
- if (result.length >= count) {
- return result
- }
- const seen: Set<string> = new Set<string>()
- for (let i = 0; i < result.length; i++) {
- seen.add(result[i].filePath)
- }
- const allSongs = this.pickRandomSongs(items, items.length)
- for (let i = 0; i < allSongs.length && result.length < count; i++) {
- const item = allSongs[i]
- if (seen.has(item.filePath)) {
- continue
- }
- seen.add(item.filePath)
- result.push(item)
- }
- return result
- }
- private isSameSongSelection(left: VideoItem[], right: VideoItem[]): boolean {
- if (left.length !== right.length) {
- return false
- }
- for (let i = 0; i < left.length; i++) {
- if (left[i].filePath !== right[i].filePath) {
- return false
- }
- }
- return true
- }
- private isSameLeadingSongs(left: VideoItem[], right: VideoItem[], count: number): boolean {
- const safeCount = Math.min(count, left.length, right.length)
- for (let i = 0; i < safeCount; i++) {
- if (left[i].filePath !== right[i].filePath) {
- return false
- }
- }
- return safeCount > 0
- }
- private isSameAlbumSelection(left: FindAlbumGroup[], right: FindAlbumGroup[]): boolean {
- if (left.length !== right.length) {
- return false
- }
- for (let i = 0; i < left.length; i++) {
- if (left[i].id !== right[i].id) {
- return false
- }
- }
- return true
- }
- private isSameLeadingAlbums(left: FindAlbumGroup[], right: FindAlbumGroup[], count: number): boolean {
- const safeCount = Math.min(count, left.length, right.length)
- for (let i = 0; i < safeCount; i++) {
- if (left[i].id !== right[i].id) {
- return false
- }
- }
- return safeCount > 0
- }
- private rotateSongs(items: VideoItem[], offset: number): VideoItem[] {
- if (items.length === 0) {
- return []
- }
- const safeOffset = ((offset % items.length) + items.length) % items.length
- if (safeOffset === 0) {
- return items.slice()
- }
- return items.slice(safeOffset).concat(items.slice(0, safeOffset))
- }
- private rotateAlbums(items: FindAlbumGroup[], offset: number): FindAlbumGroup[] {
- if (items.length === 0) {
- return []
- }
- const safeOffset = ((offset % items.length) + items.length) % items.length
- if (safeOffset === 0) {
- return items.slice()
- }
- return items.slice(safeOffset).concat(items.slice(0, safeOffset))
- }
- private ensureDifferentLeadingSongs(next: VideoItem[], current: VideoItem[]): VideoItem[] {
- const pageSize = this.getSectionPageSize(this.getDiscoverSectionColumns())
- if (!this.isSameLeadingSongs(next, current, pageSize)) {
- return next
- }
- if (next.length <= pageSize) {
- return next
- }
- const rotated = this.rotateSongs(next, pageSize)
- if (!this.isSameLeadingSongs(rotated, current, pageSize)) {
- return rotated
- }
- return next
- }
- private ensureDifferentLeadingAlbums(next: FindAlbumGroup[], current: FindAlbumGroup[]): FindAlbumGroup[] {
- const pageSize = this.getSectionPageSize(this.getDiscoverSectionColumns())
- if (!this.isSameLeadingAlbums(next, current, pageSize)) {
- return next
- }
- if (next.length <= pageSize) {
- return next
- }
- const rotated = this.rotateAlbums(next, pageSize)
- if (!this.isSameLeadingAlbums(rotated, current, pageSize)) {
- return rotated
- }
- return next
- }
- private pickDifferentPreferredSongs(items: VideoItem[], count: number, current: VideoItem[]): VideoItem[] {
- const next = this.pickPreferredSongs(items, count)
- if (!this.isSameSongSelection(next, current) || items.length <= current.length) {
- return next
- }
- const currentSet: Set<string> = new Set<string>()
- for (let i = 0; i < current.length; i++) {
- currentSet.add(current[i].filePath)
- }
- const differentItems: VideoItem[] = []
- for (let i = 0; i < items.length; i++) {
- const item = items[i]
- if (currentSet.has(item.filePath)) {
- continue
- }
- differentItems.push(item)
- }
- if (differentItems.length === 0) {
- return next
- }
- const refreshed = this.pickPreferredSongs(differentItems, count)
- if (refreshed.length >= Math.min(count, items.length)) {
- return refreshed
- }
- const result: VideoItem[] = refreshed.slice()
- const seen: Set<string> = new Set<string>()
- for (let i = 0; i < result.length; i++) {
- seen.add(result[i].filePath)
- }
- for (let i = 0; i < next.length && result.length < count; i++) {
- const item = next[i]
- if (seen.has(item.filePath)) {
- continue
- }
- seen.add(item.filePath)
- result.push(item)
- }
- return result
- }
- private pickDifferentAlbumGroups(items: FindAlbumGroup[], count: number, current: FindAlbumGroup[]): FindAlbumGroup[] {
- const next = this.pickAlbumGroups(items, count)
- if (!this.isSameAlbumSelection(next, current) || items.length <= current.length) {
- return next
- }
- const currentSet: Set<string> = new Set<string>()
- for (let i = 0; i < current.length; i++) {
- currentSet.add(current[i].id)
- }
- const differentItems: FindAlbumGroup[] = []
- for (let i = 0; i < items.length; i++) {
- const item = items[i]
- if (currentSet.has(item.id)) {
- continue
- }
- differentItems.push(item)
- }
- if (differentItems.length === 0) {
- return next
- }
- const refreshed = this.pickAlbumGroups(differentItems, count)
- if (refreshed.length >= Math.min(count, items.length)) {
- return refreshed
- }
- const result: FindAlbumGroup[] = refreshed.slice()
- const seen: Set<string> = new Set<string>()
- for (let i = 0; i < result.length; i++) {
- seen.add(result[i].id)
- }
- for (let i = 0; i < next.length && result.length < count; i++) {
- const item = next[i]
- if (seen.has(item.id)) {
- continue
- }
- seen.add(item.id)
- result.push(item)
- }
- return result
- }
- private shuffleAlbumGroups(items: FindAlbumGroup[]): FindAlbumGroup[] {
- const copy: FindAlbumGroup[] = items.slice()
- for (let i = copy.length - 1; i > 0; i--) {
- const randomIndex = Math.floor(Math.random() * (i + 1))
- const current = copy[i]
- copy[i] = copy[randomIndex]
- copy[randomIndex] = current
- }
- return copy
- }
- private pickAlbumGroups(items: FindAlbumGroup[], count: number): FindAlbumGroup[] {
- const preferred: FindAlbumGroup[] = []
- const fallback: FindAlbumGroup[] = []
- for (let i = 0; i < items.length; i++) {
- const item = items[i]
- if (item.songCount >= PREFERRED_ALBUM_MIN_COUNT) {
- preferred.push(item)
- } else {
- fallback.push(item)
- }
- }
- const result: FindAlbumGroup[] = []
- const shuffledPreferred = this.shuffleAlbumGroups(preferred)
- const shuffledFallback = this.shuffleAlbumGroups(fallback)
- for (let i = 0; i < shuffledPreferred.length && result.length < count; i++) {
- result.push(shuffledPreferred[i])
- }
- for (let i = 0; i < shuffledFallback.length && result.length < count; i++) {
- result.push(shuffledFallback[i])
- }
- return result
- }
- private buildAlbumGroups(items: VideoItem[], isRemote: boolean): FindAlbumGroup[] {
- const albumMap: Map<string, VideoItem[]> = new Map<string, VideoItem[]>()
- for (let i = 0; i < items.length; i++) {
- const item = items[i]
- const albumName = item.album?.trim() ?? ''
- if (albumName.length === 0) {
- continue
- }
- const artistName = item.artist?.trim() ?? ''
- const groupKey = `${isRemote ? item.type : CommonConstants.TYPE_LOCAL}_${albumName}_${artistName}`
- const groupSongs = albumMap.get(groupKey)
- if (groupSongs) {
- groupSongs.push(item)
- } else {
- albumMap.set(groupKey, [item])
- }
- }
- const result: FindAlbumGroup[] = []
- albumMap.forEach((songs: VideoItem[], key: string) => {
- const sortedSongs = this.sortAlbumSongs(songs)
- const firstSong = sortedSongs[0]
- const title = firstSong.album?.trim() ?? ''
- if (title.length === 0) {
- return
- }
- const coverSong = this.pickAlbumCoverSong(sortedSongs)
- result.push({
- id: key,
- title: title,
- artist: this.resolveAlbumArtist(sortedSongs),
- coverPath: coverSong?.pixelMapPath ?? '',
- songCount: sortedSongs.length,
- songs: sortedSongs,
- isRemote: isRemote,
- sourceLabel: isRemote ? this.getCloudTypeLabel(firstSong) : '本地'
- })
- })
- result.sort((left: FindAlbumGroup, right: FindAlbumGroup) => {
- if (right.songCount !== left.songCount) {
- return right.songCount - left.songCount
- }
- return left.title.localeCompare(right.title)
- })
- return result
- }
- private sortAlbumSongs(items: VideoItem[]): VideoItem[] {
- return buildSortedDiscoverySongs(items, FindCollectionSortType.TRACK_ASC)
- }
- private pickAlbumCoverSong(items: VideoItem[]): VideoItem | undefined {
- for (let i = 0; i < items.length; i++) {
- if (StrUtil.isNotEmpty(items[i].pixelMapPath)) {
- return items[i]
- }
- }
- return items[0]
- }
- private resolveAlbumArtist(items: VideoItem[]): string {
- for (let i = 0; i < items.length; i++) {
- const artistName = items[i].artist?.trim() ?? ''
- if (artistName.length > 0) {
- return artistName
- }
- }
- return '未知歌手'
- }
- private syncFindCanBackState(): void {
- AppStorage.setOrCreate('findCanBack', this.isSearchMode || this.isAlbumMode)
- }
- private handleInnerBack(): boolean {
- if (this.isSearchMode) {
- this.exitSearchMode()
- return true
- }
- if (this.isAlbumMode) {
- this.exitAlbumMode()
- return true
- }
- return false
- }
- private emitPlaylistPlay(playlistId: string, playlistName: string, songs: VideoItem[], startIndex: number,
- playType?: number): void {
- if (songs.length === 0) {
- ToastUtil.showToast('暂无可播放歌曲')
- return
- }
- const safeIndex = Math.min(Math.max(startIndex, 0), songs.length - 1)
- Logger.info(
- TAG,
- `[remote-debug] emitPlaylistPlay id=${playlistId}, name=${playlistName}, count=${songs.length}, ` +
- `startIndex=${safeIndex}, playType=${playType ?? -1}, sample=${this.buildSongDebugLog(songs)}`
- )
- if (playlistId === 'find-album-playlist' || playlistId.indexOf('find-') === 0) {
- setFindPlaylist(playlistId, playlistName, songs, safeIndex)
- }
- const eventPlaylistPlay: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYLIST_PLAY }
- const playlistData = new PlaylistPlayRequest(
- playlistId,
- playlistName,
- songs.length,
- safeIndex,
- songs.map((item: VideoItem): string => item.filePath),
- false,
- playType
- )
- savePendingPlaylistPlay(playlistData)
- emitter.emit(eventPlaylistPlay, { data: playlistData })
- }
- private getCurrentPlaybackQueue(): VideoItem[] {
- const queue = AppStorage.get('songList') as VideoItem[] | undefined
- return Array.isArray(queue) ? queue : []
- }
- private getCurrentPlaybackIndex(): number {
- const currentIndex = AppStorage.get('currIndex') as number | undefined
- return typeof currentIndex === 'number' ? currentIndex : -1
- }
- private canFavoriteSong(item: VideoItem): boolean {
- return StrUtil.isNotEmpty(item.filePath)
- }
- private canDeleteSong(item: VideoItem): boolean {
- return item.type === CommonConstants.TYPE_LOCAL && StrUtil.isNotEmpty(item.filePath)
- }
- private canQueueSongForNextPlay(item: VideoItem): boolean {
- return StrUtil.isNotEmpty(item.filePath)
- }
- private isSongFavorite(item: VideoItem): boolean {
- return item.isFav === 1
- }
- private syncPlaybackQueueStores(previousQueue: VideoItem[], nextQueue: VideoItem[], currentIndex: number): void {
- const findQueue = getFindVideoItems()
- if (findQueue.length > 0 && this.isSameSongSelection(previousQueue, findQueue)) {
- const playlistId = getFindPlaylistId().length > 0 ? getFindPlaylistId() : 'find-search'
- const playlistName = getFindPlaylistName().length > 0 ? getFindPlaylistName() : '发现搜索'
- setFindPlaylist(playlistId, playlistName, nextQueue, currentIndex)
- }
- const webdavQueue = getWebdavVideoItems()
- if (webdavQueue.length > 0 && this.isSameSongSelection(previousQueue, webdavQueue)) {
- setWebdavPlaylist(nextQueue, currentIndex)
- }
- const navidromeQueue = getNavidromeVideoItems()
- if (navidromeQueue.length > 0 && this.isSameSongSelection(previousQueue, navidromeQueue)) {
- setNavidromePlaylist(nextQueue, currentIndex)
- }
- }
- private handleAddSongToNextPlay(song: VideoItem): void {
- if (!this.canQueueSongForNextPlay(song)) {
- ToastUtil.showToast('当前歌曲暂不支持加入下一首播放')
- return
- }
- const currentQueue = this.getCurrentPlaybackQueue()
- const result = insertSongToNextPlayQueue(
- currentQueue,
- this.currentSong?.filePath ?? '',
- this.getCurrentPlaybackIndex(),
- song
- )
- if (result.status === QueueInsertStatus.INVALID_SONG) {
- ToastUtil.showToast('当前歌曲暂不支持加入下一首播放')
- return
- }
- if (result.status === QueueInsertStatus.ALREADY_PLAYING) {
- ToastUtil.showToast('当前正在播放这首歌')
- return
- }
- if (result.status === QueueInsertStatus.START_PLAY) {
- this.emitPlaylistPlay('find-search-next', '发现搜索', [song], 0)
- ToastUtil.showToast('已开始播放')
- return
- }
- AppStorage.setOrCreate('songList', result.queue)
- AppStorage.setOrCreate('currIndex', result.currentIndex)
- PreferencesUtil.putSync('LastMusicList', result.queue)
- this.syncPlaybackQueueStores(currentQueue, result.queue, result.currentIndex)
- ToastUtil.showToast('已添加到下一首播放')
- }
- @Builder
- private SongContextMenuBuilder(item: VideoItem) {
- Menu() {
- MenuItem({
- symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.forward_end_fill')),
- content: '下一首播放'
- })
- .visibility(this.canQueueSongForNextPlay(item) ? Visibility.Visible : Visibility.None)
- .onClick(() => {
- this.handleAddSongToNextPlay(item)
- })
- MenuItem({
- symbolStartIcon: new SymbolGlyphModifier(this.isSongFavorite(item) ?
- $r('sys.symbol.heart_fill') : $r('sys.symbol.heart')),
- content: this.isSongFavorite(item) ? '取消收藏' : '收藏'
- })
- .visibility(this.canFavoriteSong(item) ? Visibility.Visible : Visibility.None)
- .onClick(() => {
- void this.handleAlbumFavoriteSong(item)
- })
- MenuItem({
- symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.trash')),
- content: '删除'
- })
- .visibility(this.canDeleteSong(item) ? Visibility.Visible : Visibility.None)
- .onClick(() => {
- this.openDeleteSongDialog(item)
- })
- }
- .attributeModifier(new MenuModifier())
- }
- private async getFullLocalSongsPool(): Promise<VideoItem[]> {
- if (this.localSongsPool.length > 0) {
- return this.localSongsPool
- }
- if (!this.mediaTable) {
- return []
- }
- const localSongs = await this.mediaTable.queryAllVideos()
- this.localSongsPool = this.filterUniqueSongs(localSongs)
- return this.localSongsPool
- }
- private async getFullRemoteSongsPool(): Promise<VideoItem[]> {
- if (this.remotePlaybackPool.length > 0) {
- Logger.info(
- TAG,
- `[remote-debug] getFullRemoteSongsPool cacheHit=${this.remotePlaybackPool.length}, ` +
- `sample=${this.buildSongDebugLog(this.remotePlaybackPool)}`
- )
- return this.remotePlaybackPool
- }
- if (!this.mediaTable) {
- return []
- }
- let remoteSongs = this.filterUniqueSongs(await this.mediaTable.queryRemoteSongsAsync())
- const fallbackSongs = remoteSongs.length > 0 ? remoteSongs : this.remoteSongsPool
- const indexedSongs = await this.queryIndexedRemotePlaybackSongs(fallbackSongs)
- if (indexedSongs.length === 0 && remoteSongs.length === 0) {
- await this.ensureRemoteDiscoverySongsAvailable()
- remoteSongs = this.filterUniqueSongs(await this.mediaTable.queryRemoteSongsAsync())
- }
- Logger.info(
- TAG,
- `[remote-debug] getFullRemoteSongsPool beforeSelect indexed=${indexedSongs.length}, db=${remoteSongs.length}, ` +
- `indexedSample=${this.buildSongDebugLog(indexedSongs)}, dbSample=${this.buildSongDebugLog(remoteSongs)}`
- )
- this.remotePlaybackPool = buildPreferredRemotePlaybackPool(indexedSongs, remoteSongs)
- Logger.info(
- TAG,
- `发现页全量云端播放池已就绪: indexed=${indexedSongs.length}, db=${remoteSongs.length}, ` +
- `selected=${this.remotePlaybackPool.length}, source=${indexedSongs.length > 0 ? 'indexed' : 'db'}, ` +
- `sample=${this.buildSongDebugLog(this.remotePlaybackPool)}`
- )
- return this.remotePlaybackPool
- }
- private async playFromFullRemoteQueue(playlistId: string, playlistName: string, fallbackSongs: VideoItem[],
- targetSong?: VideoItem, randomStart: boolean = false): Promise<void> {
- const fullQueue = await this.getFullRemoteSongsPool()
- if (randomStart) {
- const queue = fullQueue.length > 0 ? fullQueue : fallbackSongs
- if (queue.length === 0) {
- ToastUtil.showToast('需要先配置网盘并加载歌曲后才能播放')
- return
- }
- const startIndex = Math.floor(Math.random() * queue.length)
- Logger.info(
- TAG,
- `[remote-debug] playFromFullRemoteQueue random playlist=${playlistId}, fullQueue=${fullQueue.length}, ` +
- `fallback=${fallbackSongs.length}, selectedQueue=${queue.length}, source=${fullQueue.length > 0 ? 'full' : 'fallback'}, ` +
- `startIndex=${startIndex}, startSong=${this.buildSongDebugItem(queue[startIndex])}`
- )
- this.emitPlaylistPlay(playlistId, playlistName, queue, startIndex, 3)
- return
- }
- if (!targetSong) {
- const queue = fullQueue.length > 0 ? fullQueue : fallbackSongs
- if (queue.length === 0) {
- ToastUtil.showToast('暂无可播放歌曲')
- return
- }
- this.emitPlaylistPlay(playlistId, playlistName, queue, 0)
- return
- }
- const targetFilePath = targetSong.filePath || ''
- Logger.info(
- TAG,
- `[remote-debug] playFromFullRemoteQueue precise playlist=${playlistId}, target=${this.buildSongDebugItem(targetSong)}, ` +
- `fullQueue=${fullQueue.length}, fallback=${fallbackSongs.length}`
- )
- const fullIndex = resolveQueueStartIndex(fullQueue, targetFilePath)
- if (fullIndex >= 0) {
- this.emitPlaylistPlay(playlistId, playlistName, fullQueue, fullIndex)
- return
- }
- const fallbackIndex = resolveQueueStartIndex(fallbackSongs, targetFilePath)
- if (fallbackIndex >= 0) {
- Logger.warn(TAG, `全量云端队列未命中当前歌曲,回退到当前卡片队列: ${targetFilePath}`)
- this.emitPlaylistPlay(playlistId, playlistName, fallbackSongs, fallbackIndex)
- return
- }
- const queue = fullQueue.length > 0 ? fullQueue : fallbackSongs
- if (queue.length === 0) {
- ToastUtil.showToast('暂无可播放歌曲')
- return
- }
- this.emitPlaylistPlay(playlistId, playlistName, queue, 0)
- }
- private async handleActionButtonTap(type: string): Promise<void> {
- if (type === 'top') {
- this.emitPlaylistPlay('find-top-played', '最近爱听', this.topPlayedSongsPool, 0)
- return
- }
- if (type === 'local-random') {
- const allLocalSongs = await this.getFullLocalSongsPool()
- if (allLocalSongs.length === 0) {
- ToastUtil.showToast('本地歌曲为空')
- return
- }
- const startIndex = Math.floor(Math.random() * allLocalSongs.length)
- this.emitPlaylistPlay('find-local-random-all', '随心所欲', allLocalSongs, startIndex, 3)
- return
- }
- const allRemoteSongs = await this.getFullRemoteSongsPool()
- Logger.info(
- TAG,
- `[remote-debug] handleActionButtonTap cloud-random remotePlaybackPool=${allRemoteSongs.length}, ` +
- `remoteSongsPool=${this.remoteSongsPool.length}, remoteSongs=${this.remoteSongs.length}, cloudMoodSongs=${this.cloudMoodSongs.length}`
- )
- if (allRemoteSongs.length === 0) {
- ToastUtil.showToast('需要先配置网盘并加载歌曲后才能播放')
- return
- }
- await this.playFromFullRemoteQueue('find-random-cloud', '云端漫游', this.remoteSongsPool, undefined, true)
- }
- private handleSwiperTap(index: number): void {
- this.emitPlaylistPlay('find-swiper', '发现推荐', this.swiperSongs, index)
- }
- private handleRemoteSongTap(index: number): void {
- const targetSong = this.remoteSongs[index]
- void this.playFromFullRemoteQueue('find-cloud', '漫步云端', this.remoteSongs, targetSong)
- }
- private handleHotSongTap(index: number): void {
- this.emitPlaylistPlay('find-local-random', '本地随机', this.hotSongs, index)
- }
- private handleCloudMoodSongTap(index: number): void {
- const targetSong = this.cloudMoodSongs[index]
- void this.playFromFullRemoteQueue('find-cloud-mood', '云卷云舒', this.cloudMoodSongs, targetSong)
- }
- private handleRecentSongTap(index: number): void {
- this.emitPlaylistPlay('find-recent', '最近播放', this.recentSongs, index)
- }
- private handlePopularSongTap(index: number): void {
- this.emitPlaylistPlay('find-popular', '热门歌曲', this.popularSongs, index)
- }
- private handleFavoriteSongTap(index: number): void {
- this.emitPlaylistPlay('find-favorite', '我的收藏', this.favoriteSongs, index)
- }
- private refreshLocalRandomSongs(): void {
- this.hotSongs = this.pickDifferentPreferredSongs(this.localSongsPool, HOT_SECTION_COUNT, this.hotSongs)
- }
- private refreshCloudMoodSongs(): void {
- this.cloudMoodSongs = this.pickDifferentPreferredSongs(this.remoteSongsPool, HOT_SECTION_COUNT, this.cloudMoodSongs)
- }
- private refreshCloudSongs(): void {
- const current = this.remoteSongs.slice()
- const next = this.pickDifferentPreferredSongs(this.remoteSongsPool, CLOUD_SECTION_COUNT, this.remoteSongs)
- const updated = this.ensureDifferentLeadingSongs(next, this.remoteSongs)
- Logger.info(
- TAG,
- `FindView refreshCloudSongs pool=${this.remoteSongsPool.length}, current=${this.buildSongSelectionLog(current)}, next=${this.buildSongSelectionLog(next)}, updated=${this.buildSongSelectionLog(updated)}`
- )
- this.remoteSongs = updated
- this.cloudSectionPageIndex = 0
- this.updateCloudSectionPages()
- }
- private refreshRecentSongs(): void {
- this.recentSongs = this.recentSongsPool.slice(0, Math.min(RECENT_SECTION_COUNT, this.recentSongsPool.length))
- this.recentSectionPageIndex = 0
- }
- private refreshPopularSongs(): void {
- const current = this.popularSongs.slice()
- const next = this.pickDifferentPreferredSongs(this.topPlayedSongsPool, POPULAR_SECTION_COUNT, this.popularSongs)
- const updated = this.ensureDifferentLeadingSongs(next, this.popularSongs)
- Logger.info(
- TAG,
- `FindView refreshPopularSongs pool=${this.topPlayedSongsPool.length}, current=${this.buildSongSelectionLog(current)}, next=${this.buildSongSelectionLog(next)}, updated=${this.buildSongSelectionLog(updated)}`
- )
- this.popularSongs = updated
- this.popularSectionPageIndex = 0
- this.updatePopularSectionPages()
- }
- private refreshFeaturedAlbums(): void {
- const current = this.featuredAlbums.slice()
- const next = this.pickDifferentAlbumGroups(this.featuredAlbumsPool, FEATURED_ALBUM_COUNT, this.featuredAlbums)
- const updated = this.ensureDifferentLeadingAlbums(next, this.featuredAlbums)
- Logger.info(
- TAG,
- `FindView refreshFeaturedAlbums pool=${this.featuredAlbumsPool.length}, current=${this.buildAlbumSelectionLog(current)}, next=${this.buildAlbumSelectionLog(next)}, updated=${this.buildAlbumSelectionLog(updated)}`
- )
- this.featuredAlbums = updated
- this.featuredAlbumPageIndex = 0
- this.updateFeaturedAlbumPages()
- }
- private refreshCloudAlbums(): void {
- const current = this.cloudAlbums.slice()
- const next = this.pickDifferentAlbumGroups(this.cloudAlbumsPool, CLOUD_ALBUM_COUNT, this.cloudAlbums)
- const updated = this.ensureDifferentLeadingAlbums(next, this.cloudAlbums)
- Logger.info(
- TAG,
- `FindView refreshCloudAlbums pool=${this.cloudAlbumsPool.length}, current=${this.buildAlbumSelectionLog(current)}, next=${this.buildAlbumSelectionLog(next)}, updated=${this.buildAlbumSelectionLog(updated)}`
- )
- this.cloudAlbums = updated
- this.cloudAlbumPageIndex = 0
- this.updateCloudAlbumPages()
- }
- private openLocalSpecialList(target: string): void {
- if (target === 'recent') {
- this.currentAlbumDefaultSortType = FindCollectionSortType.RECENT_DESC
- this.currentAlbumSupportRecentPlayedSort = true
- this.openSongCollectionDetail('find-recent-collection', '最近播放', '', this.recentSongsPool)
- return
- }
- if (target === 'favorite') {
- this.currentAlbumDefaultSortType = FindCollectionSortType.NAME_ASC
- this.currentAlbumSupportRecentPlayedSort = false
- this.openSongCollectionDetail('find-favorite-collection', '我的收藏', '', this.favoriteSongsPool)
- return
- }
- }
- private openSongCollectionDetail(id: string, title: string, sourceLabel: string, songs: VideoItem[]): void {
- if (songs.length === 0) {
- ToastUtil.showToast('暂无可展示歌曲')
- return
- }
- const coverSong = this.pickAlbumCoverSong(songs)
- this.currentAlbumId = id
- this.currentAlbumTitle = title
- this.currentAlbumArtist = ''
- this.currentAlbumCoverPath = coverSong?.pixelMapPath ?? ''
- this.currentAlbumSourceLabel = sourceLabel
- this.currentAlbumSongs = songs.slice()
- this.isSearchMode = false
- this.getUIContext()?.animateTo({ duration: 500 }, () => {
- this.isAlbumMode = true
- })
- }
- private openAlbumDetail(album: FindAlbumGroup): void {
- this.currentAlbumDefaultSortType = FindCollectionSortType.TRACK_ASC
- this.currentAlbumSupportRecentPlayedSort = false
- this.currentAlbumId = album.id
- this.currentAlbumTitle = album.title
- this.currentAlbumArtist = album.artist
- this.currentAlbumCoverPath = album.coverPath
- this.currentAlbumSourceLabel = album.isRemote ? '云端专辑' : '精选专辑'
- this.currentAlbumSongs = album.songs.slice()
- this.isAlbumMode = true
- this.isSearchMode = false
- }
- private getPlaylistCover(playlist: Playlist): string | Resource {
- if (StrUtil.isNotEmpty(playlist.coverPath)) {
- return playlist.coverPath as string
- }
- const cachedSongs = this.playlistSongsCache.get(playlist.id)
- const coverSong = cachedSongs ? this.pickAlbumCoverSong(cachedSongs) : undefined
- if (coverSong && StrUtil.isNotEmpty(coverSong.pixelMapPath)) {
- return coverSong.pixelMapPath as string
- }
- return $r('app.media.alt')
- }
- private getPlaylistSubtitle(playlist: Playlist): string {
- if (StrUtil.isNotEmpty(playlist.description)) {
- return playlist.description as string
- }
- return `${playlist.songCount} 首歌曲`
- }
- private presentPlaylistDetail(playlist: Playlist, songs: VideoItem[]): void {
- if (songs.length === 0) {
- ToastUtil.showToast('歌单里还没有可展示歌曲')
- return
- }
- this.currentAlbumDefaultSortType = FindCollectionSortType.TRACK_ASC
- this.currentAlbumSupportRecentPlayedSort = false
- const coverSong = this.pickAlbumCoverSong(songs)
- this.currentAlbumId = `find-playlist-${playlist.id}`
- this.currentAlbumTitle = playlist.name
- this.currentAlbumArtist = '心动歌单'
- this.currentAlbumCoverPath = StrUtil.isNotEmpty(playlist.coverPath) ? playlist.coverPath as string :
- (coverSong?.pixelMapPath ?? '')
- this.currentAlbumSourceLabel = '心动歌单'
- this.currentAlbumSongs = songs.slice()
- this.isAlbumMode = true
- this.isSearchMode = false
- }
- private async openPlaylistDetail(playlist: Playlist): Promise<void> {
- if (!this.playlistTable) {
- ToastUtil.showToast('歌单加载失败')
- return
- }
- if (playlist.songCount <= 0) {
- ToastUtil.showToast('歌单里还没有歌曲')
- return
- }
- const cachedSongs = this.playlistSongsCache.get(playlist.id)
- if (cachedSongs && cachedSongs.length > 0) {
- this.presentPlaylistDetail(playlist, cachedSongs)
- return
- }
- try {
- const playlistSongs = await this.playlistTable.queryPlaylistSongs(playlist.id)
- if (playlistSongs.length === 0) {
- ToastUtil.showToast('歌单里还没有歌曲')
- return
- }
- const songs = await convertPlaylistSongsToVideoItems(getContext(this) as common.Context, playlistSongs)
- if (songs.length === 0) {
- ToastUtil.showToast('歌单里还没有可播放歌曲')
- return
- }
- this.playlistSongsCache.set(playlist.id, songs)
- this.presentPlaylistDetail(playlist, songs)
- } catch (error) {
- Logger.error(TAG, `发现页打开歌单失败: ${this.toErrorMessage(error as Object)}`)
- ToastUtil.showToast('歌单加载失败')
- }
- }
- private async resolveHeartPlaylistCovers(ticket: number, playlists: Playlist[]): Promise<void> {
- if (!this.playlistTable || playlists.length === 0) {
- return
- }
- const context = getContext(this) as common.Context
- for (let index = 0; index < playlists.length; index += 1) {
- if (ticket !== this.heartPlaylistCoverTicket) {
- return
- }
- const playlist = playlists[index]
- if (StrUtil.isNotEmpty(playlist.coverPath) || playlist.songCount <= 0) {
- continue
- }
- try {
- let songs = this.playlistSongsCache.get(playlist.id)
- if (!songs || songs.length === 0) {
- const playlistSongs = await this.playlistTable.queryPlaylistSongs(playlist.id)
- if (playlistSongs.length === 0) {
- continue
- }
- songs = await convertPlaylistSongsToVideoItems(context, playlistSongs)
- if (songs.length > 0) {
- this.playlistSongsCache.set(playlist.id, songs)
- }
- }
- if (!songs || songs.length === 0) {
- continue
- }
- const coverSong = this.pickAlbumCoverSong(songs)
- if (!coverSong || StrUtil.isEmpty(coverSong.pixelMapPath)) {
- continue
- }
- playlist.coverPath = coverSong.pixelMapPath as string
- this.heartPlaylists = this.heartPlaylists.slice()
- } catch (error) {
- Logger.warn(TAG, `发现页补全歌单封面失败: ${playlist.name}, ${this.toErrorMessage(error as Object)}`)
- }
- }
- }
- private exitAlbumMode(): void {
- this.getUIContext()?.animateTo({ duration: 500 }, () => {
- this.isAlbumMode = false
- })
- this.currentAlbumId = ''
- this.currentAlbumTitle = ''
- this.currentAlbumArtist = ''
- this.currentAlbumCoverPath = ''
- this.currentAlbumSourceLabel = ''
- this.currentAlbumSongs = []
- this.currentAlbumDefaultSortType = FindCollectionSortType.TRACK_ASC
- this.currentAlbumSupportRecentPlayedSort = false
- }
- private canDeleteCurrentAlbumSongs(): boolean {
- if (this.currentAlbumSongs.length <= 0) {
- return false
- }
- return this.currentAlbumSongs.every((item: VideoItem) =>
- item.type === CommonConstants.TYPE_LOCAL && StrUtil.isNotEmpty(item.filePath))
- }
- private canFavoriteCurrentAlbumSong(song: VideoItem): boolean {
- return StrUtil.isNotEmpty(song.filePath)
- }
- private updateAlbumSongFavoriteState(filePath: string, isFav: number): void {
- this.currentAlbumSongs.forEach((item: VideoItem) => {
- if (item.filePath === filePath) {
- item.isFav = isFav
- }
- })
- }
- private async handleAlbumFavoriteSong(song: VideoItem): Promise<void> {
- if (!this.mediaTable) {
- ToastUtil.showToast('收藏功能暂不可用')
- return
- }
- if (!this.canFavoriteCurrentAlbumSong(song)) {
- ToastUtil.showToast('当前歌曲暂不支持收藏')
- return
- }
- const nextFav: number = song.isFav === 1 ? 0 : 1
- this.mediaTable.updateIsFavByFilePath(song.filePath, nextFav, async (result: boolean, error?: string) => {
- if (!result) {
- Logger.warn(TAG, `发现页专辑详情收藏更新失败: ${song.filePath}, error=${error ?? ''}`)
- ToastUtil.showToast(nextFav === 1 ? '收藏失败' : '取消收藏失败')
- return
- }
- song.isFav = nextFav
- this.updateAlbumSongFavoriteState(song.filePath, nextFav)
- await this.loadDiscoveryContent(false)
- this.syncCurrentAlbumDetailFromState()
- ToastUtil.showToast(nextFav === 1 ? '收藏成功' : '取消收藏成功')
- })
- }
- private closeDeleteDialog(afterClose?: () => void): void {
- if (this.deleteComponentId > 0) {
- try {
- this.getUIContext().getPromptAction().closeCustomDialog(this.deleteComponentId)
- } catch (_error) {
- }
- this.deleteComponentId = 0
- }
- if (afterClose) {
- setTimeout(() => {
- afterClose()
- }, 16)
- }
- }
- private filterPlaylistCachesByDeletedKeys(deletedKeys: Set<string>): void {
- this.playlistSongsCache.forEach((songs: VideoItem[], key: string) => {
- this.playlistSongsCache.set(key, songs.filter((item: VideoItem) => !deletedKeys.has(item.filePath)))
- })
- }
- private syncCurrentAlbumDetailFromState(): void {
- if (StrUtil.isEmpty(this.currentAlbumId)) {
- return
- }
- if (this.currentAlbumId === 'find-recent-collection') {
- this.currentAlbumSongs = this.recentSongsPool.slice()
- this.currentAlbumCoverPath = this.pickAlbumCoverSong(this.currentAlbumSongs)?.pixelMapPath ?? ''
- } else if (this.currentAlbumId === 'find-favorite-collection') {
- this.currentAlbumSongs = this.favoriteSongsPool.slice()
- this.currentAlbumCoverPath = this.pickAlbumCoverSong(this.currentAlbumSongs)?.pixelMapPath ?? ''
- } else if (this.currentAlbumId.startsWith('find-playlist-')) {
- const playlistId = this.currentAlbumId.replace('find-playlist-', '')
- const playlist = this.heartPlaylists.find((item: Playlist) => item.id === playlistId)
- const songs = this.playlistSongsCache.get(playlistId) ?? []
- this.currentAlbumSongs = songs.slice()
- if (playlist) {
- this.currentAlbumTitle = playlist.name
- this.currentAlbumArtist = '心动歌单'
- const coverSong = this.pickAlbumCoverSong(songs)
- this.currentAlbumCoverPath = StrUtil.isNotEmpty(playlist.coverPath) ? playlist.coverPath as string :
- (coverSong?.pixelMapPath ?? '')
- } else {
- this.currentAlbumCoverPath = ''
- }
- } else {
- const album = this.featuredAlbumsPool.find((item: FindAlbumGroup) => item.id === this.currentAlbumId) ??
- this.cloudAlbumsPool.find((item: FindAlbumGroup) => item.id === this.currentAlbumId)
- if (album) {
- this.currentAlbumTitle = album.title
- this.currentAlbumArtist = album.artist
- this.currentAlbumCoverPath = album.coverPath
- this.currentAlbumSourceLabel = album.isRemote ? '云端专辑' : '精选专辑'
- this.currentAlbumSongs = album.songs.slice()
- } else {
- this.currentAlbumCoverPath = ''
- this.currentAlbumSongs = []
- }
- }
- }
- private async handleAlbumDeleteSuccess(deletedItems: VideoItem[]): Promise<void> {
- if (deletedItems.length <= 0) {
- return
- }
- const deletedKeys: Set<string> = new Set<string>()
- deletedItems.forEach((item: VideoItem) => {
- if (StrUtil.isNotEmpty(item.filePath)) {
- deletedKeys.add(item.filePath)
- }
- })
- if (deletedKeys.size <= 0) {
- return
- }
- this.currentAlbumSongs = this.currentAlbumSongs.filter((item: VideoItem) => !deletedKeys.has(item.filePath))
- this.searchResults = this.searchResults.filter((item: VideoItem) => !deletedKeys.has(item.filePath))
- this.filterPlaylistCachesByDeletedKeys(deletedKeys)
- await this.loadDiscoveryContent(false)
- this.syncCurrentAlbumDetailFromState()
- if (this.isSearchMode && this.searchText.length > 0) {
- await this.onSearchInput(this.searchText)
- }
- ToastUtil.showToast('删除成功')
- }
- private openDeleteSongDialog(song: VideoItem): void {
- if (song.type !== CommonConstants.TYPE_LOCAL || StrUtil.isEmpty(song.filePath)) {
- ToastUtil.showToast('云端歌曲暂不支持删除')
- return
- }
- this.pendingDeleteSongs = [song]
- this.getUIContext().getPromptAction().openCustomDialog({
- builder: () => {
- this.buildDeleteSongDialog()
- },
- isModal: true,
- showInSubWindow: false,
- maskColor: Color.Transparent,
- dialogTransition: TransitionEffect.translate({ x: 0, y: 120, z: 0 })
- .combine(TransitionEffect.opacity(0.01))
- .animation({ duration: 260, curve: Curve.EaseOut }),
- maskTransition: TransitionEffect.opacity(0)
- .animation({ duration: 220, curve: Curve.EaseOut })
- }).then((dialogId: number) => {
- this.deleteComponentId = dialogId
- }).catch((error: BusinessError) => {
- Logger.error(TAG, `发现页打开删除弹窗失败: ${error.message}`)
- })
- }
- @Builder
- private buildDeleteSongDialog() {
- DeleteComptent({
- selectedFiles: this.pendingDeleteSongs,
- onCancel: () => {
- this.closeDeleteDialog(() => {
- this.pendingDeleteSongs = []
- })
- },
- onDeleteResult: (result: boolean) => {
- const deletedItems = this.pendingDeleteSongs.slice()
- this.closeDeleteDialog(() => {
- this.pendingDeleteSongs = []
- if (result) {
- void this.handleAlbumDeleteSuccess(deletedItems)
- } else {
- ToastUtil.showToast('删除失败')
- }
- })
- }
- })
- }
- private playCurrentAlbum(startIndex: number, songs: VideoItem[] = this.currentAlbumSongs): void {
- if (songs.length === 0) {
- ToastUtil.showToast('专辑里还没有歌曲')
- return
- }
- this.emitPlaylistPlay(
- 'find-album-playlist',
- this.currentAlbumTitle.length > 0 ? this.currentAlbumTitle : '专辑',
- songs,
- startIndex
- )
- }
- private handleAlbumPlayAll(songs?: VideoItem[]): void {
- const targetSongs = songs && songs.length > 0 ? songs : this.currentAlbumSongs
- this.playCurrentAlbum(0, targetSongs)
- }
- private handleAlbumRandomPlay(songs?: VideoItem[]): void {
- const targetSongs = songs && songs.length > 0 ? songs : this.currentAlbumSongs
- if (targetSongs.length === 0) {
- ToastUtil.showToast('专辑里还没有歌曲')
- return
- }
- const startIndex = Math.floor(Math.random() * targetSongs.length)
- this.playCurrentAlbum(startIndex, targetSongs)
- }
- private handleAlbumSongTap(index: number, songs?: VideoItem[]): void {
- const targetSongs = songs && songs.length > 0 ? songs : this.currentAlbumSongs
- this.playCurrentAlbum(index, targetSongs)
- }
- 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.searchResults = []
- if (!this.isSearchMode) {
- this.isSearchMode = true
- }
- this.isSearchLoading = true
- this.searchController.stopEditing()
- setTimeout(() => {
- void this.onSearchInput(keyword)
- }, 16)
- }
- private exitSearchMode(): void {
- this.getUIContext()?.animateTo({ duration: 555 }, () => {
- this.isSearchMode = false
- })
- this.searchText = ''
- this.searchResults = []
- this.isSearchLoading = false
- this.searchController.stopEditing()
- }
- private async ensureSearchSourceReady(): Promise<void> {
- if (!this.mediaTable) {
- return
- }
- if (this.localSongsPool.length === 0) {
- const localSongs = await this.mediaTable.queryAllVideos()
- this.localSongsPool = this.filterUniqueSongs(localSongs)
- }
- if (this.searchRemoteSongsPool.length === 0) {
- const remoteSongs = await this.mediaTable.queryRemoteSongsAsync()
- this.searchRemoteSongsPool = await this.resolveRemoteSongCovers(this.filterUniqueSongs(remoteSongs), true)
- }
- }
- private matchesSearchKeyword(item: VideoItem, keyword: string): boolean {
- const normalizedKeyword = keyword.toLowerCase()
- const fields: string[] = [
- this.getSongTitle(item),
- this.getSongSubtitle(item),
- StrUtil.isNotEmpty(item.artist) ? item.artist as string : '',
- StrUtil.isNotEmpty(item.album) ? item.album as string : '',
- StrUtil.isNotEmpty(item.fileName) ? item.fileName as string : ''
- ]
- for (let i = 0; i < fields.length; i++) {
- const field = fields[i]
- if (field.toLowerCase().indexOf(normalizedKeyword) >= 0) {
- return true
- }
- }
- return false
- }
- private async onSearchInput(value: string): Promise<void> {
- const currentTicket = ++this.searchTicket
- this.searchText = value.trim()
- if (this.searchText.length === 0) {
- this.isSearchLoading = false
- this.searchResults = []
- this.loadSearchHistory()
- return
- }
- if (!this.isSearchMode) {
- this.isSearchMode = true
- }
- this.isSearchLoading = true
- await new Promise<void>((resolve: () => void) => {
- setTimeout(() => {
- resolve()
- }, 16)
- })
- await this.ensureSearchSourceReady()
- if (currentTicket !== this.searchTicket) {
- return
- }
- const result: VideoItem[] = []
- for (let i = 0; i < this.localSongsPool.length; i++) {
- const item = this.localSongsPool[i]
- if (this.matchesSearchKeyword(item, this.searchText)) {
- result.push(item)
- }
- }
- for (let i = 0; i < this.searchRemoteSongsPool.length; i++) {
- const item = this.searchRemoteSongsPool[i]
- if (this.matchesSearchKeyword(item, this.searchText)) {
- result.push(item)
- }
- }
- this.searchResults = this.filterUniqueSongs(result)
- this.isSearchLoading = false
- }
- private getSwiperAspectRatio(): number {
- return this.currentBreakpoint === BreakpointTypeEnum.SM ? 1.06 : 1.66
- }
- private getSwiperTextSize(smallSize: number, largeSize: number): number {
- return this.currentBreakpoint === BreakpointTypeEnum.SM ? smallSize : largeSize
- }
- private getIndicatorItemWidth(): number {
- const widthVp = this.windowWidth > 0 ? px2vp(this.windowWidth) : 360
- const count = Math.max(1, this.swiperSongs.length)
- const gap = 8
- const horizontalPadding = this.currentBreakpoint === BreakpointTypeEnum.SM ? 64 : 120
- const usableWidth = Math.max(80, widthVp - horizontalPadding - gap * (count - 1))
- return usableWidth / count
- }
- private resetPagedSectionIndices(): void {
- this.cloudSectionPageIndex = 0
- this.featuredAlbumPageIndex = 0
- this.cloudAlbumPageIndex = 0
- this.recentSectionPageIndex = 0
- this.popularSectionPageIndex = 0
- this.favoriteSectionPageIndex = 0
- }
- private getDiscoverSectionColumns(): number {
- return this.currentBreakpoint === BreakpointTypeEnum.SM ? 3 : 4
- }
- private getSectionPageSize(columns: number): number {
- return columns * 2
- }
- private getSafeSectionPageIndex(pageCount: number, currentIndex: number): number {
- if (pageCount <= 0) {
- return 0
- }
- return Math.min(Math.max(currentIndex, 0), pageCount - 1)
- }
- private buildSongSelectionLog(items: VideoItem[], limit: number = 4): string {
- if (items.length === 0) {
- return '[]'
- }
- const parts: string[] = []
- const maxCount = Math.min(limit, items.length)
- for (let i = 0; i < maxCount; i++) {
- parts.push(this.getSongTitle(items[i]))
- }
- return `[${parts.join(', ')}](${items.length})`
- }
- private buildSongDebugLog(items: VideoItem[], limit: number = 5): string {
- if (items.length === 0) {
- return '[]'
- }
- const parts: string[] = []
- const maxCount = Math.min(limit, items.length)
- for (let i = 0; i < maxCount; i++) {
- parts.push(this.buildSongDebugItem(items[i]))
- }
- return `[${parts.join('; ')}](${items.length})`
- }
- private buildSongDebugItem(item?: VideoItem): string {
- if (!item) {
- return 'unknown'
- }
- return `${this.getSongTitle(item)}|type=${item.type}|acc=${item.webdav_account_id ?? ''}|path=${this.sanitizeSongPath(item.filePath)}|` +
- `cover=${this.sanitizeCoverValue(item.pixelMapPath)}`
- }
- private sanitizeSongPath(path?: string): string {
- if (StrUtil.isEmpty(path)) {
- return ''
- }
- const value = path as string
- if (value.length <= 48) {
- return value
- }
- return `...${value.substring(value.length - 48)}`
- }
- private sanitizeCoverValue(value?: string): string {
- if (StrUtil.isEmpty(value)) {
- return ''
- }
- const coverValue = value as string
- if (coverValue.length <= 72) {
- return coverValue
- }
- return `...${coverValue.substring(coverValue.length - 72)}`
- }
- private logRemoteDiscoveryCoverState(label: string, items: VideoItem[]): void {
- if (!items || items.length === 0) {
- Logger.info(TAG, `[cover-debug] ${label} items=0`)
- return
- }
- let coverCount = 0
- let absoluteCoverCount = 0
- let relativeCoverCount = 0
- for (let i = 0; i < items.length; i++) {
- const coverPath = items[i].pixelMapPath ?? ''
- if (coverPath.length <= 0) {
- continue
- }
- coverCount += 1
- if (coverPath.startsWith('http://') || coverPath.startsWith('https://') || coverPath.startsWith('file://')) {
- absoluteCoverCount += 1
- } else {
- relativeCoverCount += 1
- }
- }
- Logger.info(
- TAG,
- `[cover-debug] ${label} items=${items.length}, coverCount=${coverCount}, absoluteCover=${absoluteCoverCount}, ` +
- `relativeCover=${relativeCoverCount}, sample=${this.buildSongDebugLog(items)}`
- )
- }
- private buildAlbumSelectionLog(items: FindAlbumGroup[], limit: number = 4): string {
- if (items.length === 0) {
- return '[]'
- }
- const parts: string[] = []
- const maxCount = Math.min(limit, items.length)
- for (let i = 0; i < maxCount; i++) {
- parts.push(items[i].title)
- }
- return `[${parts.join(', ')}](${items.length})`
- }
- private buildSongPages(prefix: string, items: VideoItem[], columns: number): FindSongPage[] {
- const pages: FindSongPage[] = []
- const pageSize = this.getSectionPageSize(columns)
- for (let start = 0, pageIndex = 0; start < items.length; start += pageSize, pageIndex++) {
- pages.push({
- id: prefix + '_' + pageIndex,
- items: items.slice(start, start + pageSize)
- })
- }
- return pages
- }
- private buildAlbumPages(prefix: string, items: FindAlbumGroup[], columns: number): FindAlbumPage[] {
- const pages: FindAlbumPage[] = []
- const pageSize = this.getSectionPageSize(columns)
- for (let start = 0, pageIndex = 0; start < items.length; start += pageSize, pageIndex++) {
- pages.push({
- id: prefix + '_' + pageIndex,
- items: items.slice(start, start + pageSize)
- })
- }
- return pages
- }
- private rebuildPagedSectionSources(): void {
- this.updateCloudSectionPages()
- this.updateFeaturedAlbumPages()
- this.updateCloudAlbumPages()
- this.updatePopularSectionPages()
- Logger.info(
- TAG,
- `FindView rebuildPagedSectionSources columns=${this.getDiscoverSectionColumns()}, cloudPages=${this.cloudSectionPages.length}, featuredAlbumPages=${this.featuredAlbumPages.length}, cloudAlbumPages=${this.cloudAlbumPages.length}, popularPages=${this.popularSectionPages.length}`
- )
- }
- private updateCloudSectionPages(): void {
- this.cloudSectionPageVersion++
- this.cloudSectionPages =
- this.buildSongPages(`find_cloud_section_${this.cloudSectionPageVersion}`, this.remoteSongs, this.getDiscoverSectionColumns())
- this.cloudSectionPageIndex = this.getSafeSectionPageIndex(this.cloudSectionPages.length, this.cloudSectionPageIndex)
- Logger.info(
- TAG,
- `FindView updateCloudSectionPages version=${this.cloudSectionPageVersion}, pageIndex=${this.cloudSectionPageIndex}, pages=${this.cloudSectionPages.length}, songs=${this.buildSongSelectionLog(this.remoteSongs)}`
- )
- }
- private updateFeaturedAlbumPages(): void {
- this.featuredAlbumPageVersion++
- this.featuredAlbumPages =
- this.buildAlbumPages(`find_featured_album_section_${this.featuredAlbumPageVersion}`, this.featuredAlbums,
- this.getDiscoverSectionColumns())
- this.featuredAlbumPageIndex = this.getSafeSectionPageIndex(this.featuredAlbumPages.length, this.featuredAlbumPageIndex)
- Logger.info(
- TAG,
- `FindView updateFeaturedAlbumPages version=${this.featuredAlbumPageVersion}, pageIndex=${this.featuredAlbumPageIndex}, pages=${this.featuredAlbumPages.length}, albums=${this.buildAlbumSelectionLog(this.featuredAlbums)}`
- )
- }
- private updateCloudAlbumPages(): void {
- this.cloudAlbumPageVersion++
- this.cloudAlbumPages =
- this.buildAlbumPages(`find_cloud_album_section_${this.cloudAlbumPageVersion}`, this.cloudAlbums, this.getDiscoverSectionColumns())
- this.cloudAlbumPageIndex = this.getSafeSectionPageIndex(this.cloudAlbumPages.length, this.cloudAlbumPageIndex)
- Logger.info(
- TAG,
- `FindView updateCloudAlbumPages version=${this.cloudAlbumPageVersion}, pageIndex=${this.cloudAlbumPageIndex}, pages=${this.cloudAlbumPages.length}, albums=${this.buildAlbumSelectionLog(this.cloudAlbums)}`
- )
- }
- private updatePopularSectionPages(): void {
- this.popularSectionPageVersion++
- this.popularSectionPages =
- this.buildSongPages(`find_popular_section_${this.popularSectionPageVersion}`, this.popularSongs, this.getDiscoverSectionColumns())
- this.popularSectionPageIndex = this.getSafeSectionPageIndex(this.popularSectionPages.length, this.popularSectionPageIndex)
- Logger.info(
- TAG,
- `FindView updatePopularSectionPages version=${this.popularSectionPageVersion}, pageIndex=${this.popularSectionPageIndex}, pages=${this.popularSectionPages.length}, songs=${this.buildSongSelectionLog(this.popularSongs)}`
- )
- }
- private getRecentSectionPages(): FindSongPage[] {
- return this.buildSongPages('find_recent_section', this.recentSongs, this.getDiscoverSectionColumns())
- }
- private getFavoriteSectionPages(): FindSongPage[] {
- return this.buildSongPages('find_favorite_section', this.favoriteSongs, this.getDiscoverSectionColumns())
- }
- private getSongTitle(item: VideoItem): string {
- if (StrUtil.isNotEmpty(item.name)) {
- return item.name
- }
- if (StrUtil.isNotEmpty(item.fileName)) {
- return item.fileName as string
- }
- return '未知歌曲'
- }
- private getSongSubtitle(item: VideoItem): string {
- if (StrUtil.isNotEmpty(item.artist) && StrUtil.isNotEmpty(item.album)) {
- return `${item.artist} · ${item.album}`
- }
- if (StrUtil.isNotEmpty(item.artist)) {
- return item.artist as string
- }
- if (StrUtil.isNotEmpty(item.album)) {
- return item.album as string
- }
- return '本地音乐'
- }
- private getSongCover(item: VideoItem): string | Resource {
- return StrUtil.isNotEmpty(item.pixelMapPath) ? item.pixelMapPath as string : $r('app.media.alt')
- }
- private getSongQualityText(item: VideoItem): string {
- if (StrUtil.isNotEmpty(item.md5Str)) {
- const quality = item.md5Str as string
- return quality.includes('Lossless') ? '无损' : quality
- }
- const sampleRate = Number(item.sampleRate ?? '0')
- const bitDepth = Number(item.bits_per_raw_sample ?? '0')
- if (sampleRate >= 88200 || bitDepth >= 24) {
- return 'Hi-Res'
- }
- const mimeType = (item.mimeType ?? '').toLowerCase()
- const fileName = (item.fileName ?? item.name ?? '').toLowerCase()
- const isLosslessFormat = mimeType.includes('flac') || mimeType.includes('wav') || mimeType.includes('ape') ||
- mimeType.includes('alac') || mimeType.includes('dsf') || mimeType.includes('dff') ||
- fileName.endsWith('.flac') || fileName.endsWith('.wav') || fileName.endsWith('.ape') ||
- fileName.endsWith('.alac') || fileName.endsWith('.dsf') || fileName.endsWith('.dff')
- return isLosslessFormat ? '无损' : ''
- }
- private getAlbumCover(coverPath: string): string | Resource {
- return StrUtil.isNotEmpty(coverPath) ? coverPath : $r('app.media.alt')
- }
- private getCloudTypeLabel(item: VideoItem): string {
- switch (item.type) {
- case CommonConstants.TYPE_WEBDAV:
- return 'WebDAV'
- case CommonConstants.TYPE_SMB:
- return 'SMB'
- case CommonConstants.TYPE_NAVIDROME:
- return 'Navidrome'
- case CommonConstants.TYPE_FTP:
- return 'FTP'
- case CommonConstants.TYPE_BAIDU:
- return '百度网盘'
- case CommonConstants.TYPE_JELLYFIN:
- return 'Jellyfin'
- case CommonConstants.TYPE_EMBY:
- return 'Emby'
- case CommonConstants.TYPE_AUDIOSTATION:
- return 'AudioStation'
- case CommonConstants.TYPE_PLEX:
- return 'Plex'
- case CommonConstants.TYPE_DAOLIYU:
- return '道理鱼'
- default:
- return '云端'
- }
- }
- private getRecentSongSubtitle(item: VideoItem): string {
- if (StrUtil.isNotEmpty(item.lastPlayedStr)) {
- return item.lastPlayedStr as string
- }
- return this.getSongSubtitle(item)
- }
- private getSearchSourceLabel(item: VideoItem): string {
- if (item.type === CommonConstants.TYPE_LOCAL) {
- return '本地'
- }
- return this.getCloudTypeLabel(item)
- }
- private toErrorMessage(error: Object): string {
- const businessError = error as BusinessError
- if (businessError && businessError.message) {
- return businessError.message
- }
- const rawError = error as Error
- if (rawError && StrUtil.isNotEmpty(rawError.message)) {
- return rawError.message
- }
- return `${error}`
- }
- private getPrimaryTextColor(): ResourceColor {
- return $r('app.color.text_color')
- }
- private getSecondaryTextColor(): ResourceColor {
- return $r('app.color.find_secondary_text')
- }
- private getCardBackgroundColor(): ResourceColor {
- return $r('app.color.find_card_background')
- }
- private getSongQualityBadgeTextColor(): ResourceColor {
- return $r('app.color.album_detail_song_quality_text')
- }
- private getSongQualityBadgeBackgroundColor(): ResourceColor {
- return $r('app.color.album_detail_song_quality_background')
- }
- @Builder
- private buildLoadingState() {
- Column({ space: 14 }) {
- LoadingProgress()
- .width(46)
- .height(46)
- .color(this.themeColor)
- Text(this.refreshText)
- .fontSize(15)
- .fontWeight(FontWeight.Medium)
- .fontColor(this.getSecondaryTextColor())
- }
- .width('100%')
- .layoutWeight(1)
- .justifyContent(FlexAlign.Center)
- .alignItems(HorizontalAlign.Center)
- }
- @Builder
- private buildTopErrorBanner() {
- if (StrUtil.isNotEmpty(this.refreshText)) {
- if (this.isRefreshing) {
- Row({ space: 8 }) {
- LoadingProgress()
- .width(14)
- .height(14)
- .color(this.themeColor)
- Text(this.refreshText)
- .fontSize(12)
- .fontColor(this.getSecondaryTextColor())
- .textAlign(TextAlign.Center)
- .maxLines(1)
- }
- .width('100%')
- .padding({ left: 12, right: 12, top: 10, bottom: 10 })
- .justifyContent(FlexAlign.Center)
- .alignItems(VerticalAlign.Center)
- } else {
- Row({ space: 6 }) {
- SymbolGlyph($r('sys.symbol.exclamationmark_circle'))
- .fontSize(15)
- .fontColor([$r('app.color.orange')])
- Text(this.refreshText)
- .layoutWeight(1)
- .fontSize(12)
- .fontColor($r('app.color.orange'))
- .maxLines(1)
- .textOverflow({ overflow: TextOverflow.Ellipsis })
- }
- .width('100%')
- .padding({ left: 12, right: 12, top: 10, bottom: 10 })
- .backgroundColor($r('app.color.timeColor_bg'))
- .borderRadius(16)
- }
- }
- }
- @Builder
- private topTitleBar() {
- Column() {
- Row({ space: 12 }) {
- if (!this.isSearchMode && !this.isAlbumMode) {
- TitleBarPointLightButton({
- iconResource: $r('sys.symbol.sort'),
- iconSize: 25,
- pointColor: this.themeColor,
- clickScale: 0.8,
- clickHandler: () => {
- this.getUIContext().animateTo({ duration: 500 }, () => {
- this.isShowDrawer = !this.isShowDrawer
- this.offsetX = 0
- })
- }
- })
- Text('发现页')
- .margin({ left: 3, right: 10 })
- .fontColor($r('app.color.text_color'))
- .fontSize(19)
- .fontWeight(FontWeight.Bold)
- .maxLines(1)
- .textOverflow({ overflow: TextOverflow.MARQUEE })
- .layoutWeight(1)
- } else {
- TitleBarPointLightButton({
- iconResource: $r('sys.symbol.chevron_left'),
- iconSize: 24,
- pointColor: this.themeColor,
- clickScale: 0.8,
- clickHandler: () => {
- if (this.isSearchMode) {
- this.exitSearchMode()
- return
- }
- this.exitAlbumMode()
- }
- })
- if (this.isAlbumMode) {
- Column({ space: 2 }) {
- Text(this.currentAlbumTitle)
- .fontColor($r('app.color.text_color'))
- .fontSize(18)
- .fontWeight(FontWeight.Bold)
- .maxLines(1)
- .textOverflow({ overflow: TextOverflow.Ellipsis })
- .width('100%')
- Text(this.currentAlbumArtist + ' · ' + this.currentAlbumSongs.length + ' 首')
- .fontColor(this.getSecondaryTextColor())
- .fontSize(11)
- .maxLines(1)
- .textOverflow({ overflow: TextOverflow.Ellipsis })
- .width('100%')
- }
- .layoutWeight(1)
- .alignItems(HorizontalAlign.Start)
- .margin({ right: 10 })
- }
- }
- if (this.isSearchMode) {
- 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(40)
- .placeholderColor(this.getSecondaryTextColor())
- .placeholderFont({ size: 14, weight: 400 })
- .textFont({ size: 14, weight: 400 })
- .onSubmit((value: string) => {
- this.searchController.stopEditing()
- this.commitSearchHistory(value)
- void this.onSearchInput(value)
- })
- .onChange((value: string) => {
- void this.onSearchInput(value)
- })
- .animation({ duration: 300, curve: Curve.Ease })
- }
- if (!this.isSearchMode && !this.isAlbumMode) {
- TitleBarPointLightButton({
- iconResource: $r('sys.symbol.magnifyingglass'),
- iconSize: 25,
- pointColor: this.themeColor,
- clickScale: 0.6,
- clickHandler: () => {
- this.isAlbumMode = false
- this.getUIContext()?.animateTo({ duration: 500 }, () => {
- this.isSearchMode = true
- })
- this.loadSearchHistory()
- void this.ensureSearchSourceReady()
- }
- })
- }
- }
- }
- .padding({ top: this.topSafeHeight + 10, left: 10, right: 10, bottom: 6 })
- .width('100%')
- }
- @Builder
- private buildSearchLoadingState() {
- Column({ space: 12 }) {
- LoadingProgress()
- .width(38)
- .height(38)
- .color(this.themeColor)
- Text('正在搜索歌曲...')
- .fontSize(14)
- .fontColor(this.getSecondaryTextColor())
- }
- .width('100%')
- .padding({ top: 48, bottom: 48 })
- .alignItems(HorizontalAlign.Center)
- }
- @Builder
- private SearchHistoryView() {
- 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) => {
- PointLightContentButton({
- pointColor: this.themeColor,
- buttonColor: $r('app.color.find_history_background'),
- pointLightHeight: 40,
- builder: () => {
- this.buildSearchHistoryChipContent(keyword)
- }
- })
- .margin({ right: 8, bottom: 8 })
- .padding({ left: 12, right: 12, top: 8, bottom: 8 })
- .onClick(() => {
- this.applySearchHistory(keyword)
- })
- })
- }
- .width('100%')
- }
- .layoutWeight(1)
- .width('100%')
- }
- @Builder
- private buildSearchHistoryChipContent(keyword: string) {
- Text(keyword)
- .fontSize(12)
- .fontColor($r('app.color.text_color'))
- .padding({ left: 8, right: 8, top: 5, bottom: 5 })
- }
- @Builder
- private buildSearchResultCardContent(item: VideoItem) {
- Row({ space: 12 }) {
- Stack({ alignContent: Alignment.BottomStart }) {
- Image(this.getSongCover(item))
- .width(58)
- .height(58)
- .borderRadius(8)
- .objectFit(ImageFit.Cover)
- Row() {
- Text(this.getSearchSourceLabel(item))
- .fontColor($r('app.color.white'))
- .fontSize(8)
- .padding({ left: 5, right: 5, top: 2, bottom: 2 })
- .backgroundColor('#7A000000')
- .borderRadius(999)
- }
- .width('100%')
- .padding({ left: 6, bottom: 6 })
- }
- .width(58)
- .height(58)
- Column({ space: 4 }) {
- Text(this.getSongTitle(item))
- .fontSize(14)
- .fontWeight(FontWeight.Bold)
- .fontColor(this.getPrimaryTextColor())
- .maxLines(1)
- .textOverflow({ overflow: TextOverflow.Ellipsis })
- .width('100%')
- Row (){
- if (StrUtil.isNotEmpty(this.getSongQualityText(item))) {
- Text(this.getSongQualityText(item))
- .fontSize(10)
- .fontWeight(FontWeight.Medium)
- .fontColor(this.getSongQualityBadgeTextColor())
- .padding({ left: 6, right: 6, top: 2, bottom: 2 })
- .backgroundColor(this.getSongQualityBadgeBackgroundColor())
- .borderRadius(4)
- }
- Text(this.getSongSubtitle(item))
- .fontSize(11)
- .lineHeight(14)
- .fontColor(this.getSecondaryTextColor())
- .maxLines(1)
- .textOverflow({ overflow: TextOverflow.Ellipsis })
- .width('100%')
- }
- }
- .layoutWeight(1)
- .alignItems(HorizontalAlign.Start)
- Column({ space: 8 }) {
- PlayingIndicator({
- isActive: this.isPlaying && this.currentSong?.filePath === item.filePath,
- indicatorSize: 18,
- marginRight: 20,
- marginTop: 8,
- marginBottom: 8
- })
- }
- .alignItems(HorizontalAlign.End)
- }
- .width('100%')
- .padding(10)
- }
- @Builder
- private buildSwipeDeleteAction(item: VideoItem) {
- Row() {
- Button('删除')
- .width(72)
- .height(52)
- .fontSize(14)
- .fontColor(Color.White)
- .backgroundColor($r('app.color.btn_red'))
- .borderRadius(14)
- .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.8 })
- .onClick(() => {
- this.openDeleteSongDialog(item)
- })
- }
- .padding({ left: 8, right: 4 })
- }
- @Builder
- private buildSearchContent() {
- List({ space: 12 }) {
- if (this.isSearchLoading) {
- ListItem() {
- this.buildSearchLoadingState()
- }
- } else if (this.searchText.length === 0 && this.searchHistoryItems.length > 0) {
- ListItem() {
- this.SearchHistoryView()
- }
- } else if (this.searchText.length === 0) {
- ListItem() {
- this.buildSectionEmptyState('搜索本地和网盘歌曲', '输入歌曲名、歌手或专辑名后即可开始搜索', false)
- }
- } else if (this.searchResults.length === 0) {
- ListItem() {
- this.buildSectionEmptyState('没有找到匹配歌曲', '试试其他关键词,结果会同时包含本地和网盘歌曲', false)
- }
- } else {
- ListItem() {
- Row() {
- Text('搜索结果')
- .fontSize(14)
- .fontWeight(FontWeight.Medium)
- .fontColor(this.getSecondaryTextColor())
- Blank()
- Text(`${this.searchResults.length} 首`)
- .fontSize(12)
- .fontColor(this.getSecondaryTextColor())
- }
- .width('100%')
- }
- ForEach(this.searchResults, (item: VideoItem, index: number) => {
- ListItem() {
- PointLightContentButton({
- pointColor: this.themeColor,
- buttonColor: this.getCardBackgroundColor(),
- buttonRadius: 18,
- pointLightHeight: 132,
- useShadow: true,
- builder: () => {
- this.buildSearchResultCardContent(item)
- }
- })
- .width('100%')
- .onClick(() => {
- this.emitPlaylistPlay('find-search', '发现搜索', this.searchResults, index)
- })
- }
- .swipeAction(this.canDeleteSong(item)
- ? {
- end: this.buildSwipeDeleteAction(item),
- edgeEffect: SwipeEdgeEffect.None
- }
- : {})
- .bindContextMenu(this.SongContextMenuBuilder(item), ResponseType.LongPress,
- {
- preview: MenuPreviewMode.IMAGE
- })
- .bindContextMenu(this.SongContextMenuBuilder(item), ResponseType.RightClick,
- {
- preview: MenuPreviewMode.IMAGE
- })
- }, getFindSongKey)
- }
- }
- .width('100%')
- .height('100%')
- .padding({ left: 12, right: 12, top: this.topSafeHeight + 65 })
- .scrollBar(BarState.Off)
- .edgeEffect(EdgeEffect.Spring)
- .layoutWeight(1)
- }
- @Builder
- private buildSwiperCardContent(item: VideoItem) {
- Stack({ alignContent: Alignment.BottomStart }) {
- Image(this.getSongCover(item))
- .width('100%')
- .aspectRatio(this.getSwiperAspectRatio())
- .objectFit(ImageFit.Cover)
- Column({ space: 10 }) {
- Column({ space: 4 }) {
- Text(this.getSongTitle(item))
- .fontColor($r('app.color.white'))
- .fontSize(this.getSwiperTextSize(18, 24))
- .fontWeight(FontWeight.Bold)
- .maxLines(1)
- .textOverflow({ overflow: TextOverflow.Ellipsis })
- Text(this.getSongSubtitle(item))
- .fontColor('#E6FFFFFF')
- .fontSize(this.getSwiperTextSize(12, 15))
- .maxLines(1)
- .textOverflow({ overflow: TextOverflow.Ellipsis })
- }
- .alignItems(HorizontalAlign.Start)
- }
- .width('100%')
- .padding({
- left: this.currentBreakpoint === BreakpointTypeEnum.SM ? 16 : 22,
- right: this.currentBreakpoint === BreakpointTypeEnum.SM ? 16 : 22,
- bottom: this.currentBreakpoint === BreakpointTypeEnum.SM ? 16 : 22
- })
- .linearGradient({
- direction: GradientDirection.Top,
- colors: [['#D9000000', 0], ['#7A000000', 0.48], ['#00000000', 1]]
- })
- }
- .width('100%')
- .clip(true)
- }
- @Builder
- private buildSwiperSection() {
- if (this.swiperSongs.length > 0) {
- Swiper() {
- ForEach(this.swiperSongs, (item: VideoItem, index: number) => {
- PointLightContentButton({
- pointColor: this.themeColor,
- buttonRadius: 0,
- pointLightHeight: 180,
- builder: () => {
- this.buildSwiperCardContent(item)
- }
- })
- .width('100%')
- .onClick(() => {
- this.handleSwiperTap(index)
- })
- }, getFindSongKey)
- }
- .width('100%')
- .clip(true)
- .displayCount(this.getTopSwiperDisplayCount())
- .autoPlay(true)
- .interval(3200)
- .loop(true)
- .onChange((index: number) => {
- this.swiperIndex = index
- })
- .indicator(new DotIndicator()
- .itemWidth(this.getIndicatorItemWidth())
- .itemHeight(2)
- .selectedItemWidth(this.getIndicatorItemWidth())
- .selectedItemHeight(4))
- } else {
- this.buildSectionEmptyState('还没有可展示的封面音乐', '给本地歌曲补上封面后,这里会自动随机推荐', true)
- }
- }
- private getSwiperDisplayCount(): number {
- return new BreakpointType<number>({
- sm: 1,
- md: 1,
- lg: 1,
- xl: 2,
- xxl: 2
- }).getValue(this.currentBreakpoint) ?? 1
- }
- private getTopSwiperDisplayCount(): number {
- return new BreakpointType<number>({
- sm: 1,
- md: 2,
- lg: 2,
- xl: 3,
- xxl: 3
- }).getValue(this.currentBreakpoint) ?? 1
- }
- @Builder
- private buildActionButtons() {
- Row({ space: 10 }) {
- PointLightActionButton({
- text: '最近爱听',
- iconResource: $r('sys.symbol.heart'),
- pointColor: this.themeColor,
- textColor: $r('app.color.text_color'),
- buttonColor: this.getCardBackgroundColor()
- })
- .opacity(0.8)
- .layoutWeight(1)
- .onClick(() => {
- void this.handleActionButtonTap('top')
- })
- PointLightActionButton({
- text: '随心所欲',
- iconResource: $r('sys.symbol.shuffle'),
- pointColor: this.themeColor,
- textColor: $r('app.color.text_color'),
- buttonColor: this.getCardBackgroundColor()
- })
- .opacity(0.8)
- .layoutWeight(1)
- .onClick(() => {
- void this.handleActionButtonTap('local-random')
- })
- PointLightActionButton({
- text: '云端漫游',
- iconResource: $r('sys.symbol.cloud'),
- pointColor: this.themeColor,
- textColor: $r('app.color.text_color'),
- buttonColor: this.getCardBackgroundColor()
- })
- .opacity(0.8)
- .layoutWeight(1)
- .onClick(() => {
- void this.handleActionButtonTap('cloud-random')
- })
- }
- .width('100%')
- }
- @Builder
- private buildCloudSongCardContent(item: VideoItem) {
- Column({ space: 6 }) {
- Stack({ alignContent: Alignment.Bottom }) {
- Image(this.getSongCover(item))
- .width('100%')
- .aspectRatio(3 / 4)
- .borderRadius(16)
- .alt($r('app.media.alt'))
- .objectFit(ImageFit.Cover)
- Row() {
- Text(this.getCloudTypeLabel(item))
- .fontColor($r('app.color.white'))
- .fontWeight(FontWeight.Medium)
- .padding({ left: 6, right: 6, top: 3, bottom: 3 })
- .fontSize(9)
- .backgroundColor('#7A000000')
- .borderRadius(999)
- }
- .width('100%')
- .padding({ left: 8, right: 8, bottom: 8 })
- .justifyContent(FlexAlign.Start)
- }
- Text(this.getSongTitle(item))
- .fontSize(14)
- .fontWeight(FontWeight.Bold)
- .lineHeight(18)
- .fontColor(this.getPrimaryTextColor())
- .maxLines(1)
- .textOverflow({ overflow: TextOverflow.Ellipsis })
- .width('100%')
- Text(this.getSongSubtitle(item))
- .fontSize(11)
- .lineHeight(14)
- .fontColor(this.getSecondaryTextColor())
- .maxLines(1)
- .textOverflow({ overflow: TextOverflow.Ellipsis })
- .width('100%')
- }
- .width('100%')
- }
- @Builder
- private buildCloudSection() {
- ConfigTitle({
- text: '漫步云端',
- indicatorCount: this.cloudSectionPages.length,
- indicatorIndex: this.getSafeSectionPageIndex(this.cloudSectionPages.length, this.cloudSectionPageIndex),
- onActionClick: () => {
- Logger.info(TAG, `FindView clickChange cloudSection current=${this.buildSongSelectionLog(this.remoteSongs)}`)
- this.refreshCloudSongs()
- }
- })
- if (this.remoteSongs.length === 0) {
- this.buildSectionEmptyState('还没有网盘歌曲', '先去网盘页或远程音乐页加载歌曲,这里会自动展示', false)
- } else {
- Swiper() {
- ForEach(this.cloudSectionPages, (page: FindSongPage, pageIndex: number) => {
- GridRow({ columns: this.getDiscoverSectionColumns(), gutter: 8 }) {
- ForEach(page.items, (item: VideoItem, itemIndex: number) => {
- GridCol() {
- PointLightContentButton({
- pointColor: this.themeColor,
- buttonRadius: 16,
- builder: () => {
- this.buildCloudSongCardContent(item)
- }
- })
- .width('100%')
- .onClick(() => {
- this.handleRemoteSongTap(pageIndex * this.getSectionPageSize(this.getDiscoverSectionColumns()) + itemIndex)
- })
- }
- }, getFindSongKey)
- }
- }, getFindSongPageKey)
- }
- .width('100%')
- .indicator(false)
- .autoPlay(false)
- .displayCount(this.getSwiperDisplayCount())
- .loop(false)
- .onChange((index: number) => {
- this.cloudSectionPageIndex = index
- })
- }
- }
- @Builder
- private buildAlbumCardContent(album: FindAlbumGroup) {
- Column({ space: 6 }) {
- Stack({ alignContent: Alignment.Bottom }) {
- Image(this.getAlbumCover(album.coverPath))
- .width('100%')
- .aspectRatio(3 / 4)
- .borderRadius(18)
- .alt($r('app.media.alt'))
- .objectFit(ImageFit.Cover)
- Row() {
- Text(album.sourceLabel)
- .fontColor($r('app.color.white'))
- .fontWeight(FontWeight.Medium)
- .padding({ left: 6, right: 6, top: 3, bottom: 3 })
- .fontSize(9)
- .backgroundColor('#7A000000')
- .borderRadius(999)
- .visibility(album.sourceLabel === '本地' ? Visibility.None : Visibility.Visible)
- Text(album.songCount + ' 首')
- .fontColor($r('app.color.white'))
- .fontSize(9)
- .padding({ left: 6, right: 6, top: 3, bottom: 3 })
- .backgroundColor('#5C000000')
- .borderRadius(999)
- }
- .width('100%')
- .padding({ left: 8, right: 8, bottom: 8 })
- .justifyContent(FlexAlign.SpaceBetween)
- }
- Text(album.title)
- .fontSize(14)
- .fontWeight(FontWeight.Bold)
- .lineHeight(18)
- .fontColor(this.getPrimaryTextColor())
- .maxLines(1)
- .textOverflow({ overflow: TextOverflow.Ellipsis })
- .width('100%')
- Text(album.artist)
- .fontSize(11)
- .lineHeight(14)
- .fontColor(this.getSecondaryTextColor())
- .maxLines(1)
- .textOverflow({ overflow: TextOverflow.Ellipsis })
- .width('100%')
- }
- .width('100%')
- }
- @Builder
- private buildFeaturedAlbumSection() {
- ConfigTitle({
- text: '精选专辑',
- indicatorCount: this.featuredAlbumPages.length,
- indicatorIndex: this.getSafeSectionPageIndex(this.featuredAlbumPages.length, this.featuredAlbumPageIndex),
- onActionClick: () => {
- Logger.info(TAG, `FindView clickChange featuredAlbums current=${this.buildAlbumSelectionLog(this.featuredAlbums)}`)
- this.refreshFeaturedAlbums()
- }
- })
- if (this.featuredAlbums.length === 0) {
- this.buildSectionEmptyState('还没有可展示的专辑', '带专辑信息的本地歌曲会自动整理到这里', false)
- } else {
- Swiper() {
- ForEach(this.featuredAlbumPages, (page: FindAlbumPage) => {
- GridRow({ columns: this.getDiscoverSectionColumns(), gutter: 8 }) {
- ForEach(page.items, (album: FindAlbumGroup) => {
- GridCol() {
- PointLightContentButton({
- pointColor: this.themeColor,
- buttonRadius: 18,
- builder: () => {
- this.buildAlbumCardContent(album)
- }
- })
- .width('100%')
- .onClick(() => {
- this.openAlbumDetail(album)
- })
- }
- }, getFindAlbumKey)
- }
- }, getFindAlbumPageKey)
- }
- .width('100%')
- .indicator(false)
- .autoPlay(false)
- .loop(false)
- .displayCount(this.getSwiperDisplayCount())
- .onChange((index: number) => {
- this.featuredAlbumPageIndex = index
- })
- }
- }
- @Builder
- private buildCloudAlbumSection() {
- ConfigTitle({
- text: '云端专辑',
- indicatorCount: this.cloudAlbumPages.length,
- indicatorIndex: this.getSafeSectionPageIndex(this.cloudAlbumPages.length, this.cloudAlbumPageIndex),
- onActionClick: () => {
- Logger.info(TAG, `FindView clickChange cloudAlbums current=${this.buildAlbumSelectionLog(this.cloudAlbums)}`)
- this.refreshCloudAlbums()
- }
- })
- if (this.cloudAlbums.length === 0) {
- this.buildSectionEmptyState('云端歌曲还没有专辑信息', '网盘歌曲带有专辑标签后,这里会自动汇总', false)
- } else {
- Swiper() {
- ForEach(this.cloudAlbumPages, (page: FindAlbumPage) => {
- GridRow({ columns: this.getDiscoverSectionColumns(), gutter: 8 }) {
- ForEach(page.items, (album: FindAlbumGroup) => {
- GridCol() {
- PointLightContentButton({
- pointColor: this.themeColor,
- buttonRadius: 18,
- builder: () => {
- this.buildAlbumCardContent(album)
- }
- })
- .width('100%')
- .onClick(() => {
- this.openAlbumDetail(album)
- })
- }
- }, getFindAlbumKey)
- }
- }, getFindAlbumPageKey)
- }
- .width('100%')
- .indicator(false)
- .autoPlay(false)
- .loop(false)
- .displayCount(this.getSwiperDisplayCount())
- .onChange((index: number) => {
- this.cloudAlbumPageIndex = index
- })
- }
- }
- @Builder
- private buildHotSongCardContent(item: VideoItem) {
- Column({ space: 5 }) {
- Stack({ alignContent: Alignment.Bottom }) {
- Image(this.getSongCover(item))
- .aspectRatio(3 / 4)
- .borderRadius(16)
- .width(160)
- .alt($r('app.media.alt'))
- .objectFit(ImageFit.Cover)
- Row() {
- Text(this.getSongSubtitle(item))
- .fontColor($r('app.color.white'))
- .fontSize(9)
- .lineHeight(11)
- .maxLines(1)
- .textOverflow({ overflow: TextOverflow.Ellipsis })
- }
- .justifyContent(FlexAlign.End)
- .padding({ left: 8, right: 8, bottom: 9 })
- .width(160)
- }
- Text(this.getSongTitle(item))
- .lineHeight(16)
- .maxLines(1)
- .width(160)
- .fontColor(this.getPrimaryTextColor())
- .fontSize(14)
- .fontWeight(FontWeight.Bold)
- .textOverflow({ overflow: TextOverflow.Ellipsis })
- Text(this.getRecentSongSubtitle(item))
- .fontColor(this.getSecondaryTextColor())
- .fontSize(10)
- .maxLines(1)
- .textOverflow({ overflow: TextOverflow.Ellipsis })
- .lineHeight(11)
- .width(160)
- .textAlign(TextAlign.Start)
- }
- }
- @Builder
- private buildLocalRandomSection() {
- ConfigTitle({
- text: '今日乐曲',
- onActionClick: () => {
- this.refreshLocalRandomSongs()
- }
- })
- if (this.hotSongs.length === 0) {
- this.buildSectionEmptyState('本地随机还没有内容', '本地曲库加载完成后,这里会随机展示歌曲', false)
- } else {
- List({ space: 8 }) {
- ForEach(this.hotSongs, (item: VideoItem, index: number) => {
- ListItem() {
- PointLightContentButton({
- pointColor: this.themeColor,
- buttonRadius: 16,
- builder: () => {
- this.buildHotSongCardContent(item)
- }
- })
- .onClick(() => {
- this.handleHotSongTap(index)
- })
- }
- .margin({
- left: index === 0 ? 2 : 0,
- right: index === this.hotSongs.length - 1 ? 2 : 0
- })
- }, getFindSongKey)
- }
- .listDirection(Axis.Horizontal)
- .scrollBar(BarState.Off)
- .edgeEffect(EdgeEffect.Spring, { alwaysEnabled: true })
- }
- }
- @Builder
- private buildHeartPlaylistCardContent(playlist: Playlist) {
- Column({ space: 6 }) {
- Stack({ alignContent: Alignment.Bottom }) {
- Image(this.getPlaylistCover(playlist))
- .aspectRatio(3 / 4)
- .borderRadius(16)
- .width(160)
- .alt($r('app.media.alt'))
- .objectFit(ImageFit.Cover)
- Row() {
- Text('心动歌单')
- .fontColor($r('app.color.white'))
- .fontSize(9)
- .padding({ left: 6, right: 6, top: 3, bottom: 3 })
- .backgroundColor('#7A000000')
- .borderRadius(999)
- Text(`${playlist.songCount} 首`)
- .fontColor($r('app.color.white'))
- .fontSize(9)
- .padding({ left: 6, right: 6, top: 3, bottom: 3 })
- .backgroundColor('#5C000000')
- .borderRadius(999)
- }
- .justifyContent(FlexAlign.SpaceBetween)
- .padding({ left: 8, right: 8, bottom: 8 })
- .width(160)
- }
- Text(playlist.name)
- .lineHeight(16)
- .maxLines(1)
- .width(160)
- .fontColor(this.getPrimaryTextColor())
- .fontSize(14)
- .fontWeight(FontWeight.Bold)
- .textOverflow({ overflow: TextOverflow.Ellipsis })
- Text(this.getPlaylistSubtitle(playlist))
- .fontColor(this.getSecondaryTextColor())
- .fontSize(10)
- .maxLines(1)
- .textOverflow({ overflow: TextOverflow.Ellipsis })
- .lineHeight(11)
- .width(160)
- .textAlign(TextAlign.Start)
- }
- }
- @Builder
- private buildHeartPlaylistSection() {
- ConfigTitle({
- text: '心动歌单',
- showAction: false
- })
- List({ space: 8 }) {
- ForEach(this.heartPlaylists, (playlist: Playlist) => {
- ListItem() {
- PointLightContentButton({
- pointColor: this.themeColor,
- buttonRadius: 16,
- builder: () => {
- this.buildHeartPlaylistCardContent(playlist)
- }
- })
- .onClick(() => {
- void this.openPlaylistDetail(playlist)
- })
- }
- }, getFindPlaylistKey)
- }
- .listDirection(Axis.Horizontal)
- .scrollBar(BarState.Off)
- .edgeEffect(EdgeEffect.Spring, { alwaysEnabled: true })
- }
- @Builder
- private buildCloudMoodSection() {
- ConfigTitle({
- text: '云卷云舒',
- onActionClick: () => {
- this.refreshCloudMoodSongs()
- }
- })
- if (this.cloudMoodSongs.length === 0) {
- this.buildSectionEmptyState('网盘随机还没有内容', '网盘歌曲加载完成后,这里会随机展示歌曲', false)
- } else {
- List({ space: 8 }) {
- ForEach(this.cloudMoodSongs, (item: VideoItem, index: number) => {
- ListItem() {
- PointLightContentButton({
- pointColor: this.themeColor,
- buttonRadius: 16,
- builder: () => {
- this.buildHotSongCardContent(item)
- }
- })
- .onClick(() => {
- this.handleCloudMoodSongTap(index)
- })
- }
- .margin({
- left: index === 0 ? 2 : 0,
- right: index === this.cloudMoodSongs.length - 1 ? 2 : 0
- })
- }, getFindSongKey)
- }
- .listDirection(Axis.Horizontal)
- .scrollBar(BarState.Off)
- .edgeEffect(EdgeEffect.Spring, { alwaysEnabled: true })
- }
- }
- @Builder
- private buildRecentSongCardContent(item: VideoItem) {
- Column({ space: 6 }) {
- Stack({ alignContent: Alignment.Bottom }) {
- Image(this.getSongCover(item))
- .width('100%')
- .aspectRatio(3 / 4)
- .borderRadius(16)
- .objectFit(ImageFit.Cover)
- Row() {
- Text(item.duration ? item.duration : '')
- .fontColor($r('app.color.white'))
- .fontSize(9)
- .visibility(item.duration ? Visibility.Visible : Visibility.None)
- }
- .width('100%')
- .padding({ left: 8, right: 8, bottom: 8 })
- .justifyContent(FlexAlign.SpaceBetween)
- }
- Text(this.getSongTitle(item))
- .fontSize(13)
- .fontWeight(FontWeight.Bold)
- .lineHeight(17)
- .fontColor(this.getPrimaryTextColor())
- .maxLines(1)
- .textOverflow({ overflow: TextOverflow.Ellipsis })
- .width('100%')
- Text(this.getRecentSongSubtitle(item))
- .fontSize(10)
- .lineHeight(13)
- .fontColor(this.getSecondaryTextColor())
- .maxLines(1)
- .textOverflow({ overflow: TextOverflow.Ellipsis })
- .width('100%')
- }
- .width('100%')
- }
- @Builder
- private buildRecentSection() {
- ConfigTitle({
- text: '最近播放',
- actionText: '更多',
- actionSymbol: $r('sys.symbol.chevron_right'),
- indicatorCount: this.getRecentSectionPages().length,
- indicatorIndex: this.getSafeSectionPageIndex(this.getRecentSectionPages().length, this.recentSectionPageIndex),
- onActionClick: () => {
- this.openLocalSpecialList('recent')
- }
- })
- if (this.recentSongs.length === 0) {
- this.buildSectionEmptyState('最近播放还是空的', '播放过的歌曲会自动出现在这里', false)
- } else {
- Swiper() {
- ForEach(this.getRecentSectionPages(), (page: FindSongPage, pageIndex: number) => {
- GridRow({ columns: this.getDiscoverSectionColumns(), gutter: 8 }) {
- ForEach(page.items, (item: VideoItem, itemIndex: number) => {
- GridCol() {
- PointLightContentButton({
- pointColor: this.themeColor,
- buttonRadius: 16,
- builder: () => {
- this.buildRecentSongCardContent(item)
- }
- })
- .width('100%')
- .onClick(() => {
- this.handleRecentSongTap(pageIndex * this.getSectionPageSize(this.getDiscoverSectionColumns()) + itemIndex)
- })
- }
- }, getFindSongKey)
- }
- }, getFindSongPageKey)
- }
- .width('100%')
- .indicator(false)
- .autoPlay(false)
- .displayCount(this.getSwiperDisplayCount())
- .loop(false)
- .onChange((index: number) => {
- this.recentSectionPageIndex = index
- })
- }
- }
- @Builder
- private buildPopularSongCardContent(item: VideoItem) {
- Column({ space: 6 }) {
- Stack({ alignContent: Alignment.Bottom }) {
- Image(this.getSongCover(item))
- .aspectRatio(0.8)
- .objectFit(ImageFit.Cover)
- .width('100%')
- .borderRadius(16)
- Row() {
- Text(item.playCount ? item.playCount + ' 次' : '')
- .fontColor($r('app.color.white'))
- .fontSize(9)
- .lineHeight(11)
- .visibility(item.playCount ? Visibility.Visible : Visibility.None)
- }
- .width('100%')
- .justifyContent(FlexAlign.SpaceBetween)
- .padding({ left: 8, right: 8, bottom: 8 })
- }
- Text(this.getSongTitle(item))
- .fontColor(this.getPrimaryTextColor())
- .maxLines(1)
- .fontSize(14)
- .lineHeight(16)
- .fontWeight(FontWeight.Bold)
- .textOverflow({ overflow: TextOverflow.Ellipsis })
- .width('100%')
- Text(this.getSongSubtitle(item))
- .fontColor(this.getSecondaryTextColor())
- .textAlign(TextAlign.Start)
- .maxLines(1)
- .textOverflow({ overflow: TextOverflow.Ellipsis })
- .fontSize(10)
- .lineHeight(11)
- .width('100%')
- }
- .width('100%')
- }
- @Builder
- private buildPopularSection() {
- ConfigTitle({
- text: '热门歌曲',
- indicatorCount: this.popularSectionPages.length,
- indicatorIndex: this.getSafeSectionPageIndex(this.popularSectionPages.length, this.popularSectionPageIndex),
- onActionClick: () => {
- Logger.info(TAG, `FindView clickChange popularSongs current=${this.buildSongSelectionLog(this.popularSongs)}`)
- this.refreshPopularSongs()
- }
- })
- if (this.popularSongs.length === 0) {
- this.buildSectionEmptyState('热门歌曲还没生成', '播放次数高的歌曲会优先展示在这里', false)
- } else {
- Swiper() {
- ForEach(this.popularSectionPages, (page: FindSongPage, pageIndex: number) => {
- GridRow({ columns: this.getDiscoverSectionColumns(), gutter: 8 }) {
- ForEach(page.items, (item: VideoItem, itemIndex: number) => {
- GridCol() {
- PointLightContentButton({
- pointColor: this.themeColor,
- buttonRadius: 16,
- builder: () => {
- this.buildPopularSongCardContent(item)
- }
- })
- .width('100%')
- .onClick(() => {
- this.handlePopularSongTap(pageIndex * this.getSectionPageSize(this.getDiscoverSectionColumns()) + itemIndex)
- })
- }
- }, getFindSongKey)
- }
- }, getFindSongPageKey)
- }
- .width('100%')
- .indicator(false)
- .autoPlay(false)
- .loop(false)
- .displayCount(this.getSwiperDisplayCount())
- .onChange((index: number) => {
- this.popularSectionPageIndex = index
- })
- }
- }
- @Builder
- private buildFavoriteSongCardContent(item: VideoItem) {
- Column({ space: 6 }) {
- Stack({ alignContent: Alignment.Bottom }) {
- Image(this.getSongCover(item))
- .width('100%')
- .aspectRatio(3 / 4)
- .borderRadius(16)
- .objectFit(ImageFit.Cover)
- Row() {
- Text(item.duration ? item.duration : '')
- .fontColor($r('app.color.white'))
- .fontSize(9)
- .visibility(item.duration ? Visibility.Visible : Visibility.None)
- }
- .width('100%')
- .padding({ left: 8, right: 8, bottom: 8 })
- .justifyContent(FlexAlign.SpaceBetween)
- }
- Text(this.getSongTitle(item))
- .fontSize(13)
- .fontWeight(FontWeight.Bold)
- .lineHeight(17)
- .fontColor(this.getPrimaryTextColor())
- .maxLines(1)
- .textOverflow({ overflow: TextOverflow.Ellipsis })
- .width('100%')
- Text(this.getSongSubtitle(item))
- .fontSize(10)
- .lineHeight(13)
- .fontColor(this.getSecondaryTextColor())
- .maxLines(1)
- .textOverflow({ overflow: TextOverflow.Ellipsis })
- .width('100%')
- }
- .width('100%')
- }
- @Builder
- private buildFavoriteSection() {
- ConfigTitle({
- text: '我的收藏',
- actionText: '更多',
- actionSymbol: $r('sys.symbol.chevron_right'),
- indicatorCount: this.getFavoriteSectionPages().length,
- indicatorIndex: this.getSafeSectionPageIndex(this.getFavoriteSectionPages().length, this.favoriteSectionPageIndex),
- onActionClick: () => {
- this.openLocalSpecialList('favorite')
- }
- })
- if (this.favoriteSongs.length === 0) {
- this.buildSectionEmptyState('我的收藏还是空的', '', false)
- } else {
- Swiper() {
- ForEach(this.getFavoriteSectionPages(), (page: FindSongPage, pageIndex: number) => {
- GridRow({ columns: this.getDiscoverSectionColumns(), gutter: 8 }) {
- ForEach(page.items, (item: VideoItem, itemIndex: number) => {
- GridCol() {
- PointLightContentButton({
- pointColor: this.themeColor,
- buttonRadius: 16,
- builder: () => {
- this.buildFavoriteSongCardContent(item)
- }
- })
- .width('100%')
- .onClick(() => {
- this.handleFavoriteSongTap(pageIndex * this.getSectionPageSize(this.getDiscoverSectionColumns()) + itemIndex)
- })
- }
- }, getFindSongKey)
- }
- }, getFindSongPageKey)
- }
- .width('100%')
- .indicator(false)
- .autoPlay(false)
- .displayCount(this.getSwiperDisplayCount())
- .loop(false)
- .onChange((index: number) => {
- this.favoriteSectionPageIndex = index
- })
- }
- }
- @Builder
- private buildSectionEmptyState(title: string, subtitle: string, useLargeCard: boolean) {
- Column({ space: 8 }) {
- SymbolGlyph($r('sys.symbol.music'))
- .attributeModifier(new SymbolGlyphFancyModifier(useLargeCard ?70 :42, '', ''))
- .opacity(0.72)
- Text(title)
- .fontSize(15)
- .fontWeight(FontWeight.Medium)
- .fontColor(this.getPrimaryTextColor())
- Text(subtitle)
- .fontSize(12)
- .fontColor(this.getSecondaryTextColor())
- .textAlign(TextAlign.Center)
- }
- .width('100%')
- .padding({ top: useLargeCard ? 42 : 26, bottom: useLargeCard ? 42 : 26, left: 12, right: 12 })
- .borderRadius(20)
- .justifyContent(FlexAlign.Center)
- .alignItems(HorizontalAlign.Center)
- }
- @Builder
- private buildDiscoveryContent() {
- Scroll() {
- Column() {
- this.buildTopErrorBanner()
- this.buildSwiperSection()
- Column({ space: 6 }) {
- this.buildActionButtons()
- this.buildLocalRandomSection()
- if (this.favoriteSongsPool.length > 0 || this.favoriteSongs.length > 0) {
- this.buildFavoriteSection()
- }
- if (this.heartPlaylists.length > 0) {
- this.buildHeartPlaylistSection()
- }
- if (this.remoteSongsPool.length > 0 || this.remoteSongs.length > 0 || this.cloudMoodSongs.length > 0) {
- this.buildCloudSection()
- }
- this.buildFeaturedAlbumSection()
- if (this.remoteSongsPool.length > 0 || this.remoteSongs.length > 0 || this.cloudMoodSongs.length > 0) {
- this.buildCloudMoodSection()
- }
- if (this.remoteSongsPool.length > 0 || this.cloudAlbumsPool.length > 0 || this.cloudAlbums.length > 0) {
- this.buildCloudAlbumSection()
- }
- this.buildRecentSection()
- this.buildPopularSection()
- }
- .width('100%')
- .padding({
- left: this.currentBreakpoint === BreakpointTypeEnum.SM ? 12 : 20,
- right: this.currentBreakpoint === BreakpointTypeEnum.SM ? 12 : 20,
- top: 16,
- bottom: this.bottomSafeHeight + 96
- })
- .alignItems(HorizontalAlign.Start)
- }
- }
- .scrollBar(BarState.Off)
- .edgeEffect(EdgeEffect.Spring)
- .width('100%')
- .layoutWeight(1)
- }
- build() {
- Stack() {
- this.buildContentView();
- // 顶部区域:标题栏在上,面包屑在下
- Column() {
- this.topTitleBar()
- }
- .backgroundColor(Color.Transparent)
- .width('100%')
- .visibility(this.isAlbumMode ? Visibility.None : (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'))
- }
- .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)
- .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
- }
- @Builder
- buildContentView() {
- Column() {
- if (this.isSearchMode) {
- this.buildSearchContent()
- } else if (this.isAlbumMode) {
- FindAlbumDetail({
- albumTitle: this.currentAlbumTitle,
- albumArtist: this.currentAlbumArtist,
- albumCoverPath: this.currentAlbumCoverPath,
- albumSourceLabel: this.currentAlbumSourceLabel,
- songs: this.currentAlbumSongs,
- useTransparentBackground: this.isCustomizeBg || this.isMusicCoverHomeBackgroundActive(),
- defaultSortType: this.currentAlbumDefaultSortType,
- supportRecentPlayedSort: this.currentAlbumSupportRecentPlayedSort,
- allowDelete: this.canDeleteCurrentAlbumSongs(),
- onBack: () => {
- this.exitAlbumMode()
- },
- topSafeHeight: this.topSafeHeight,
- bottomSafeHeight: this.bottomSafeHeight,
- themeColor: this.themeColor,
- onPlayAll: (songs?: VideoItem[]) => {
- this.handleAlbumPlayAll(songs)
- },
- onRandomPlay: (songs?: VideoItem[]) => {
- this.handleAlbumRandomPlay(songs)
- },
- onSongTap: (index: number, songs?: VideoItem[]) => {
- this.handleAlbumSongTap(index, songs)
- },
- onPlayNextSong: (song: VideoItem) => {
- this.handleAddSongToNextPlay(song)
- },
- onFavoriteSong: (song: VideoItem) => {
- void this.handleAlbumFavoriteSong(song)
- },
- onDeleteSong: (song: VideoItem) => {
- this.openDeleteSongDialog(song)
- }
- })
- } else {
- Refresh({ refreshing: $$this.isRefreshing }) {
- Column() {
- if (this.isPageLoading) {
- this.buildLoadingState()
- } else {
- this.buildDiscoveryContent()
- }
- }
- .width('100%')
- .height('100%')
- }
- .width('100%')
- .layoutWeight(1)
- .pullDownRatio(this.refreshPullRatio)
- .pullToRefresh(true)
- .refreshOffset(0)
- .onOffsetChange((offset: number) => {
- this.refreshPullRatio = 1 - Math.pow((offset / this.maxRefreshingHeight), 3)
- })
- .onRefreshing(() => {
- void this.loadDiscoveryContent(true)
- })
- }
- }
- .width('100%')
- .height('100%')
- .backgroundColor(this.isCustomizeBg || this.isMusicCoverHomeBackgroundActive()
- ? Color.Transparent : $r('app.color.start_window_background'))
- }
- }
|