| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790 |
- 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)
- .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'))
- }
- }
|