NewIndex.ets 111 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308
  1. import { AppUtil, ArrayUtil, DeviceUtil, DisplayUtil, LogUtil, PreferencesUtil,
  2. StrUtil,
  3. ToastUtil } from '@pura/harmony-utils';
  4. import TitleBar from '../view/TitleBar';
  5. import { curves,
  6. ImmersiveMode,
  7. LengthMetrics,
  8. LevelMode,
  9. MenuModifier,
  10. router, SegmentButton, SegmentButtonOptions, SymbolGlyphModifier } from '@kit.ArkUI';
  11. import mainViewModel, { MainViewModel } from '../viewmodel/MainViewModel';
  12. import ItemData from '../viewmodel/ItemData';
  13. import type { sysResource } from '../viewmodel/ItemData';
  14. import { CommonConstants } from '../common/constants/CommonConstants';
  15. import { EventConstants } from '../common/constants/EventConstants';
  16. import { VideoItem } from '../viewmodel/VideoItem';
  17. import ScreenUtil from '../common/util/ScreenUtil';
  18. import { Utility } from '../common/util/Utility';
  19. import { BusinessError, emitter } from '@kit.BasicServicesKit';
  20. import { common } from '@kit.AbilityKit';
  21. import { BreakpointSystem, BreakpointTypeEnum } from '../common/util/BreakpointSystem';
  22. import dataStorage from '@ohos.data.storage';
  23. import ConfigurationConstant from '@ohos.app.ability.ConfigurationConstant';
  24. import { SettingPage } from './SettingPage';
  25. import { deviceInfo } from '@kit.BasicServicesKit';
  26. import { resourceManager } from '@kit.LocalizationKit';
  27. import { systemShare } from '@kit.ShareKit';
  28. import { uniformTypeDescriptor } from '@kit.ArkData';
  29. import { hilog } from '@kit.PerformanceAnalysisKit';
  30. import { DialogHelper, DialogAction } from '@pura/harmony-dialog';
  31. import UserUtil from '../common/util/UserUtil';
  32. import json from '@ohos.util.json';
  33. import { UserCenter } from './UserCenter';
  34. import { ScanFilePage } from './ScanFilePage';
  35. import { AboutPage, gotoShare } from './AboutPage';
  36. import { smartMobilityCommon } from '@kit.CarKit';
  37. import { UpdateLogManager } from '../common/util/UpdateLogManager';
  38. import OnlineUpdateLogDialog from '../dialog/OnlineUpdateLog';
  39. import OnlineUpdateLog from '../dialog/OnlineUpdateLog';
  40. import { LocalMusic } from '../view/LocalMusic';
  41. import { ChartsCount } from './ChartsCount';
  42. import { image } from '@kit.ImageKit';
  43. import { Playlist, PlaylistSong } from '../viewmodel/Playlist';
  44. import PlaylistTable from '../common/util/PlaylistTable';
  45. import { convertPlaylistSongsToVideoItems } from './PlaylistDetailPage';
  46. import MediaTable from '../common/util/MediaTable';
  47. import { showCreatePlaylistDialog } from '../dialog/PlaylistDialog';
  48. import { ImagePickerUtil } from '../common/util/ImagePickerUtil';
  49. import { WebDavMainPage } from './WebDavMainPage';
  50. import { WebDavAccount } from '../viewmodel/WebDavAccount';
  51. import { RemoteDriveManager } from '../common/util/RemoteDriveManager';
  52. import { getCloudDiskIcon, RemoteDriveAccountDialog } from '../dialog/RemoteDriveAccountDialog';
  53. import ReqPermissionUtil from '../common/util/ReqPermissionUtil';
  54. import { getRemoteDriveAccountLabel, getRemoteDriveDisplayLabel, getRemoteDriveProtocolLabel } from '../common/util/RemoteDriveLabel';
  55. import { RemoteDriveType } from '../common/enums/RemoteDriveType';
  56. import { RemoteMusicPage } from '../view/RemoteMusicPage';
  57. import { ServerLogUtil } from '../common/util/ServerLogUtil';
  58. import { PlayStatus } from '../common/PlayStatus';
  59. import { PointLightDefaultButton } from '../view/PointLight/PointLightDeFaultButton';
  60. import { PointLightContentButton } from '../view/PointLight/PointLightContentButton';
  61. import { PlayingIndicator } from '../view/PlayingIndicator';
  62. import { FindView } from '../view/FindView';
  63. import { hdsEffect } from '@kit.UIDesignKit';
  64. import { MiniPlayerBar } from '../view/MiniPlayerBar';
  65. import {
  66. resolveMiniPlayerMorphTarget,
  67. } from '../common/util/PlayerDismissHelper';
  68. import { shouldShowMiniPlayerBar } from '../common/util/MiniPlayerHostViewHelper';
  69. import { resolveMiniPlayerMirrorState } from '../common/util/MiniPlayerMirrorStateHelper';
  70. import {
  71. MiniPlayerOrbTapAction,
  72. resolveMiniPlayerOrbTapAction,
  73. shouldTriggerMiniPlayerOrbSingleTap
  74. } from '../common/util/MiniPlayerOrbTapHelper';
  75. import { MusicPlaybackController, MusicPlaybackHostActions } from '../controller/MusicPlaybackController';
  76. import { PlaybackCoordinator } from '../controller/PlaybackCoordinator';
  77. import {
  78. ensurePlaybackHostStorageDefaults,
  79. requestPlaybackPlayerOpen,
  80. requestPlaybackPlaylistOpen,
  81. setPlaybackRuntimeReady,
  82. showPlaybackPlayer
  83. } from '../playback/PlaybackHostState';
  84. import { getRegisteredPlaybackRuntime } from '../playback/PlaybackRuntimeRegistry';
  85. import { BackgroundAudioPlaybackHost } from '../playback/BackgroundAudioPlaybackHost';
  86. // import { FFmpeg, FFProgressMessageParser } from '@sj/ffmpeg';
  87. const TAG = 'NewIndex'; // 日志标签
  88. const FIND_DEFAULT_HOME_PROMPT_COUNT_KEY = 'find_default_home_prompt_count'
  89. const FIND_DEFAULT_HOME_PROMPT_HANDLED_KEY = 'find_default_home_prompt_handled'
  90. const FIND_DEFAULT_HOME_PROMPT_TARGET_COUNT = 5
  91. /**
  92. * 首页主组件,包含顶部标题栏、主内容区(本地音乐/网络内容)、侧边抽屉菜单等。
  93. * 支持多端适配、广告集成、导航、生命周期管理等功能。
  94. */
  95. @Preview
  96. @Entry
  97. @Component
  98. struct NewIndex {
  99. //背景流光控制器
  100. @State bgController: hdsEffect.ShaderEffectController|undefined = deviceInfo.sdkApiVersion>=20
  101. &&canIUse("SystemCapability.UIDesign.HDSComponent.Core")?
  102. new hdsEffect.ShaderEffectController():undefined;
  103. @State defalut_home_type:number = 0//首页默认类型 可支持首页 媒体库 歌单 网盘
  104. @State isDetailView: boolean = false; // 是否在艺术家/专辑详情视图
  105. @StorageLink('isShowPlay') @Watch('syncMiniPlayerVisibility') isShowPlay: boolean = false;
  106. @Provide @Watch('onMiniPlayerControlStatusChanged') CONTROL_PlayStatus: number = PlayStatus.INIT;
  107. @Provide @Watch('onMiniPlayerProgressChanged') progressValue: number = 0;
  108. @State isShowPrecious: boolean = false //播控条显示上一首按钮
  109. @State bottomBarHeight: number = 64;
  110. // 迷你播放条的整体形变状态,既用于出现/消失,也用于 full bar <-> orb 切换。
  111. @State miniPlayerScaleX: number = 1;
  112. @State miniPlayerScaleY: number = 1;
  113. @State miniPlayerOpacity: number = 1;
  114. @State miniPlayerBorderRadius: number = 200;
  115. @State miniPlayerProxySize: number = 0;
  116. @State miniPlayerProxyOpacity: number = 0;
  117. // 内容层和圆球层分开控制,保证收拢到右侧圆球时有独立位移/透明度/缩放。
  118. @State miniPlayerContentScaleY: number = 1;
  119. @State miniPlayerContentTranslateY: number = 0;
  120. @State miniPlayerFullContentTranslateX: number = 0;
  121. @State miniPlayerTranslateY: number = 0;
  122. @State miniPlayerSurfaceWidth: number = 0;
  123. @State miniPlayerSurfaceHeight: number = 0;
  124. @State miniPlayerFullContentOpacity: number = 1;
  125. @State miniPlayerOrbOpacity: number = 0;
  126. @State miniPlayerOrbScale: number = 1;
  127. @State miniPlayerOrbTranslateX: number = 0;
  128. @State isMiniPlayerOrbMode: boolean = false;
  129. @State isMiniPlayerMounted: boolean = true;
  130. @StorageProp('bottomSafeHeight') bottomSafeHeight: number = 0;
  131. @Provide @Watch('onMiniPlayerCoverChanged') cover: string | undefined = '';
  132. // @Provide currentSong: VideoItem | undefined = undefined;
  133. @StorageLink('currentSong') currentSong: VideoItem | undefined = undefined;
  134. @StorageLink('CONTROL_PlayStatus') @Watch('onHostMiniPlayerControlStatusChanged')
  135. hostControlPlayStatus: number = PlayStatus.INIT;
  136. @StorageLink('progressValue') @Watch('onHostMiniPlayerProgressChanged')
  137. hostProgressValue: number = 0;
  138. @StorageLink('cover') @Watch('onHostMiniPlayerCoverChanged')
  139. hostCover: string | undefined = '';
  140. @StorageLink('playbackRuntimeReady') @Watch('syncPlaybackRuntimeBinding')
  141. playbackRuntimeReady: boolean = false;
  142. @Provide @Watch('syncMiniPlayerVisibility') isMultiSelect: boolean = false;
  143. /** 页面上下文 */
  144. context = this.getUIContext().getHostContext() as common.UIAbilityContext
  145. private playbackController: MusicPlaybackController = MusicPlaybackController.getInstance()
  146. private playbackCoordinator: PlaybackCoordinator = PlaybackCoordinator.getInstance()
  147. @Provide currentSongListName:string = '' //当前歌单名称
  148. @Provide currentSongListID:string='' //当前歌单ID
  149. @State isDarkMode: boolean = false
  150. /** 列表滚动器,用于抽屉菜单列表滚动 */
  151. private scroller: Scroller = new Scroller();
  152. /** 应用包名 */
  153. @State bundleName: string = ''
  154. @State appName: string = ''
  155. /** 是否显示侧边抽屉菜单 */
  156. @Provide isShowDrawer: boolean = false;
  157. /** 抽屉菜单 X 轴偏移量,用于手势滑动动画 */
  158. @Provide offsetX: number = 0;
  159. @StorageProp('findCanBack') findCanBack: boolean = false
  160. /** 本地视频列表,页面参数传入 */
  161. @Provide videoLocalList: Array<VideoItem> = []
  162. /** Y 轴偏移量(预留) */
  163. @State offsetY: number = 0;
  164. /** 主内容类型(0:本地音乐,1:网络内容) */
  165. @Provide @Watch('syncMiniPlayerVisibility') mType: number = 0
  166. /** 模式类型(如文件夹 媒体库 艺术家、专辑等) */
  167. @Provide modeType: number = 0
  168. /** 是否为零状态(预留) */
  169. @Provide('isZero') isZero: boolean = false;
  170. /** 是否显示赞助入口 */
  171. @State isShowSponsorship: boolean = false
  172. /** 音乐是否为零状态(预留) */
  173. @Provide('isMusicZero') isMusicZero: boolean = false;
  174. @State isShowTitleBar: boolean = false //是否显示分类导航条
  175. /** 本地音乐列表,持久化存储 */
  176. @StorageLink('musicLocalList') musicLocalList: Array<VideoItem> = []
  177. @Provide isFavMusic: boolean = false
  178. @StorageProp('windowWidth') @Watch('onMiniPlayerLayoutContextChange') windowWidth: number = 0;
  179. @StorageProp('windowHeight') @Watch('onMiniPlayerLayoutContextChange') windowHeight: number = 0;
  180. @StorageProp('isHiCarStatus') isHiCarStatus: boolean = false;
  181. @StorageProp('curDisplayIsHiCar') @Watch('onMiniPlayerLayoutContextChange') curDisplayIsHiCar: boolean = false;
  182. /**
  183. * 是否显示更新日志开关
  184. */
  185. @State isShowUpdateDialog: boolean = false
  186. /** 应用版本号 */
  187. @State versionName: string = ''
  188. /** 音频根目录路径 */
  189. @Provide rootPath: string = ''
  190. /** 当前路径 */
  191. @Provide currentPath: string = ''
  192. /** 是否为历史记录页面 */
  193. @Provide isHistory: boolean = false
  194. /** 是否可以返回上一级 */
  195. @Provide isCanBack: boolean = false
  196. @StorageProp('currentColorMode') currentMode: number = ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT;
  197. @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
  198. /**
  199. * 一多界面适配断点系统
  200. */
  201. private breakpointSystem: BreakpointSystem = new BreakpointSystem();
  202. /** 当前断点类型(如大屏/小屏) */
  203. @StorageProp('currentBreakpoint') @Watch('onMiniPlayerLayoutContextChange') currentBreakpoint: string =
  204. BreakpointTypeEnum.MD;
  205. /** 记录上一次点击返回键的时间戳,用于双击退出 */
  206. private backTime: number = 0;
  207. private isFindDefaultPromptShowing: boolean = false;
  208. @State tabOptions: SegmentButtonOptions = SegmentButtonOptions.tab({
  209. buttons: [{ text: '分类' }, { text: '歌单' }, { text: '网盘' }],
  210. direction: Direction.Ltr,
  211. backgroundColor: $r('app.color.index_background'),
  212. selectedBackgroundColor:$r('app.color.start_window_background'),
  213. selectedFontColor: $r('app.color.text_color'),
  214. localizedTextPadding: {
  215. end: LengthMetrics.vp(10),
  216. start: LengthMetrics.vp(10)
  217. }
  218. })
  219. @State @Watch('tabSelectedIndexesChanged') tabSelectedIndexes: number[] = [0]
  220. // 播放条动画拆成多个阶段,用定时器串起抖动、回弹和最终落位。
  221. private miniPlayerAnimationTimer: number = -1;
  222. private miniPlayerCleanupTimer: number = -1;
  223. private miniPlayerShakeTimer: number = -1;
  224. private miniPlayerShakeSettleTimer: number = -1;
  225. private miniPlayerModeAnimationTimer: number = -1;
  226. private miniPlayerModeSettleTimer: number = -1;
  227. private miniPlayerModeCompleteTimer: number = -1;
  228. private miniPlayerOrbTapTimer: number = -1;
  229. private lastMiniPlayerOrbTapAt: number = 0;
  230. private readonly miniPlayerMorphDuration: number = 240;
  231. private readonly miniPlayerDotDuration: number = 190;
  232. private readonly miniPlayerShakeFirstDuration: number = 110;
  233. private readonly miniPlayerShakeSecondDuration: number = 140;
  234. private readonly miniPlayerShakeSettleDuration: number = 170;
  235. private readonly miniPlayerModeDuration: number = 360;
  236. private readonly miniPlayerModeBounceDuration: number = 220;
  237. private readonly miniPlayerOrbDoubleTapWindowMs: number = 240;
  238. private isMiniPlayerModeTransitioning: boolean = false;
  239. private pendingPlayerPageOpenTimer: number = -1;
  240. private readonly playbackHostActions: MusicPlaybackHostActions = {
  241. showPlayerView: (): void => {
  242. LogUtil.info(TAG, '[MiniState] hostActions.showPlayerView -> requestPlaybackPlayerOpen')
  243. requestPlaybackPlayerOpen()
  244. },
  245. dismissPlayerView: (): void => {
  246. showPlaybackPlayer(false)
  247. },
  248. openPlayList: (): void => {
  249. requestPlaybackPlaylistOpen()
  250. }
  251. }
  252. private logMiniPlayerState(stage: string): void {
  253. LogUtil.info(TAG,
  254. `[MiniState] ${stage} localStatus=${this.CONTROL_PlayStatus}, hostStatus=${this.hostControlPlayStatus}, ` +
  255. `localProgress=${this.progressValue}, hostProgress=${this.hostProgressValue}, ` +
  256. `localCover=${this.cover ?? ''}, hostCover=${this.hostCover ?? ''}, ` +
  257. `song=${this.currentSong?.name ?? ''}`)
  258. }
  259. private syncMiniPlayerStateFromHost(reason: string): void {
  260. const mirror = resolveMiniPlayerMirrorState(this.hostControlPlayStatus, this.hostProgressValue, this.hostCover)
  261. this.CONTROL_PlayStatus = mirror.controlPlayStatus
  262. this.progressValue = mirror.progressValue
  263. this.cover = mirror.cover
  264. this.logMiniPlayerState(`mirrored from host: ${reason}`)
  265. }
  266. private onMiniPlayerControlStatusChanged(): void {
  267. this.logMiniPlayerState('local status changed')
  268. }
  269. private onMiniPlayerProgressChanged(): void {
  270. this.logMiniPlayerState('local progress changed')
  271. }
  272. private onMiniPlayerCoverChanged(): void {
  273. this.logMiniPlayerState('local cover changed')
  274. }
  275. private onHostMiniPlayerControlStatusChanged(): void {
  276. this.logMiniPlayerState('host status changed')
  277. this.syncMiniPlayerStateFromHost('status')
  278. }
  279. private onHostMiniPlayerProgressChanged(): void {
  280. this.logMiniPlayerState('host progress changed')
  281. this.syncMiniPlayerStateFromHost('progress')
  282. }
  283. private onHostMiniPlayerCoverChanged(): void {
  284. this.logMiniPlayerState('host cover changed')
  285. this.syncMiniPlayerStateFromHost('cover')
  286. }
  287. private syncPlaybackRuntimeBinding(): void {
  288. this.playbackCoordinator.bindDefaultRuntime(BackgroundAudioPlaybackHost.getInstance().getRuntime())
  289. const runtime = getRegisteredPlaybackRuntime()
  290. if (this.playbackRuntimeReady && runtime) {
  291. this.playbackCoordinator.setRuntime(runtime)
  292. return
  293. }
  294. this.playbackCoordinator.clearRuntime()
  295. }
  296. //当胶囊按钮的选择发生变化时调用此函数
  297. tabSelectedIndexesChanged() {
  298. if(this.tabSelectedIndexes[0]==1){
  299. this.mType = 0
  300. } else if(this.tabSelectedIndexes[0]==2){
  301. // 网盘选项卡
  302. if(this.webDavAccounts.length>0&&(this.webDavAccounts[0].webType===RemoteDriveType.Navidrome||
  303. this.webDavAccounts[0].webType===RemoteDriveType.Jellyfin||
  304. this.webDavAccounts[0].webType===RemoteDriveType.Emby||
  305. this.webDavAccounts[0].webType===RemoteDriveType.DaoLiYu)){
  306. this.mType = 7
  307. }else{
  308. this.mType = 6
  309. }
  310. }
  311. }
  312. // 歌单相关状态变量
  313. @State playlistList: Playlist[] = []
  314. @State isShowCreatePlaylistDialog: boolean = false
  315. @State selectedPlaylist: Playlist | null = null
  316. private playlistTable: PlaylistTable | null = null
  317. // 网盘账户相关状态变量
  318. @State webDavAccounts: WebDavAccount[] = []
  319. private webdavManager: RemoteDriveManager = RemoteDriveManager.getInstance()
  320. private _webDavLoading: boolean = false // 防止重复加载网盘账户的标志
  321. @State selectedAccount: WebDavAccount = new WebDavAccount()
  322. /**
  323. * 返回键处理逻辑:
  324. * - 如果不是根目录或有历史记录,发送广播通知更新列表
  325. * - 如果是根目录,双击返回键退出应用,否则提示
  326. */
  327. onBackPress(): boolean | void {
  328. console.info('onecold onBackPress isDetailView = '+ this.isDetailView);
  329. console.info('onecold onBackPress mType = '+ this.mType);
  330. if (this.isShowPlay) {
  331. void this.playbackController.dismissPlayerView();
  332. return true
  333. }
  334. if (this.mType === 4) {
  335. this.returnToConfiguredHome()
  336. } else if (this.currentPath !== this.rootPath || this.isHistory || this.isFavMusic ||
  337. (this.modeType !== 0 && this.isCanBack)) {
  338. console.info('onecold 返回键处理逻辑:发送音频广播通知更新列表');
  339. const eventData: emitter.EventData = {};
  340. emitter.emit({ eventId: EventConstants.EVENT_SWIPE_BACK_UPDATE }, eventData); // 发送音频广播通知更新doSwipBack
  341. } else if(this.mType === 7) {
  342. if(this.isDetailView){
  343. console.info('onecold RemoteMusicPage 返回键处理:发送手势返回事件');
  344. const eventData: emitter.EventData = {};
  345. emitter.emit({ eventId: EventConstants.EVENT_SWIPE_BACK_NAVID}, eventData);
  346. }else{
  347. if (this.isShowDrawer) {
  348. // 如果抽屉菜单打开,先关闭抽屉
  349. this.getUIContext()?.animateTo({ duration: 555 }, () => {
  350. this.isShowDrawer = false
  351. })
  352. } else {
  353. return false
  354. }
  355. }
  356. } else if(this.mType === 6) {
  357. console.info('onecold 网盘 返回键处理:发送手势返回事件');
  358. const eventData: emitter.EventData = {};
  359. emitter.emit({ eventId: EventConstants.EVENT_SWIPE_BACK_DISK }, eventData);
  360. } else if(this.mType === 8) {
  361. if (this.findCanBack) {
  362. const eventData: emitter.EventData = {}
  363. emitter.emit({ eventId: EventConstants.EVENT_FIND_VIEW_BACK }, eventData)
  364. } else if (this.isShowDrawer) {
  365. this.getUIContext()?.animateTo({ duration: 555 }, () => {
  366. this.isShowDrawer = false
  367. })
  368. } else {
  369. return false
  370. }
  371. } else if(this.mType > 0){
  372. this.getUIContext()?.animateTo({ duration: 555 }, () => {
  373. // 动画闭包内控制Image组件的出现和消失
  374. // this.isShowDrawer = !this.isShowDrawer
  375. // this.offsetX = 0
  376. this.enterFindPage()
  377. })
  378. } else {
  379. console.info('onecold 返回键处理 关闭');
  380. if (this.isShowDrawer) {
  381. // 如果抽屉菜单打开,先关闭抽屉
  382. this.getUIContext()?.animateTo({ duration: 555 }, () => {
  383. this.isShowDrawer = false
  384. })
  385. } else {
  386. return false
  387. }
  388. }
  389. return true;
  390. }
  391. /**
  392. * 刷新@State用户信息变量,确保UI同步
  393. */
  394. refreshUserInfoState() {
  395. this.isLogin = PreferencesUtil.getBooleanSync('isLogin', false);
  396. this.userId = PreferencesUtil.getNumberSync('userId', 0);
  397. this.userName = PreferencesUtil.getStringSync('userName', '未登录用户');
  398. this.userAvatarUrl = PreferencesUtil.getStringSync('userAvatarUrl', '');
  399. this.subscriptionName = PreferencesUtil.getStringSync('subscriptionName', '');
  400. this.subscriptionEndDate = PreferencesUtil.getStringSync('subscriptionEndDate', '');
  401. this.hasActiveSubscription = PreferencesUtil.getBooleanSync('hasActiveSubscription', false);
  402. }
  403. onPageShow() {
  404. LogUtil.info('heanup NewIndex', 'onPageShow 被调用,刷新歌单列表和网盘账户列表')
  405. // 加载歌单列表
  406. this.loadPlaylistList()
  407. // 加载网盘账户列表
  408. this.loadWebDavAccounts()
  409. // 通知 LocalMusic 页面已显示,恢复旋转
  410. AppStorage.setOrCreate('isPageVisible', true)
  411. }
  412. onPageHide() {
  413. // 通知 LocalMusic 页面已隐藏,停止旋转
  414. AppStorage.setOrCreate('isPageVisible', false)
  415. }
  416. /**
  417. * 页面显示生命周期钩子
  418. * - 注册断点系统
  419. * - 获取页面参数
  420. * - 设置状态栏样式
  421. * - 初始化图片缓存
  422. * - 判断是否显示赞助入口
  423. */
  424. async aboutToAppear() {
  425. ensurePlaybackHostStorageDefaults()
  426. this.syncMiniPlayerStateFromHost('aboutToAppear')
  427. this.logMiniPlayerState('aboutToAppear after defaults')
  428. this.playbackController.setHostActions(this.playbackHostActions)
  429. this.syncPlaybackRuntimeBinding()
  430. this.defalut_home_type = PreferencesUtil.getNumberSync('defalut_home_type', 0)
  431. this.isShowPrecious = PreferencesUtil.getBooleanSync('isShowPrecious', false)
  432. ReqPermissionUtil.reqPermissionsFromUser(ReqPermissionUtil.permissions, this.context);
  433. this.isShowTitleBar = PreferencesUtil.getBooleanSync(SettingPage.IS_SHOW_TITLTBAR, false)
  434. Utility.enableFullScreen()
  435. this.topRectHeight = px2vp(AppUtil.getStatusBarHeight());
  436. Utility.getAppName(getContext(this)).then((appName: string) => {
  437. this.appName = appName
  438. })
  439. this.isDarkMode = this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK
  440. this.breakpointSystem.register();
  441. let params = router.getParams() as Record<string, Object>;
  442. if (params && params.videoList) {
  443. this.videoLocalList = params.videoList as VideoItem[];
  444. LogUtil.info('heanup NewIndex', '从路由参数获取到播放列表,长度:' + this.videoLocalList.length)
  445. } else {
  446. this.videoLocalList = []
  447. LogUtil.info('heanup NewIndex', '路由参数中没有播放列表,使用空数组')
  448. }
  449. // 检查是否从WebDavPage返回
  450. if (params && params.fromWebDavPage) {
  451. LogUtil.info('heanup NewIndex', '从WebDavPage返回,切换到网盘标签页')
  452. this.mType = 6
  453. this.tabSelectedIndexes = [2] // 切换到网盘标签
  454. }
  455. // Utility.setStatusBarLight()
  456. ScreenUtil.setScreenSize();
  457. this.bundleName = AppUtil.getBundleName()
  458. this.versionName = AppUtil.getVersionName();
  459. this.syncMiniPlayerVisibility(false)
  460. // await ImageKnife.getInstance().initFileCache(this.context, 256, 256 * 1024 * 1024)
  461. this.isShowSponsorship = Utility.isOpenTime()
  462. let themeColor = PreferencesUtil.getStringSync('THEME_COLOR', CommonConstants.DEFAULT_THEME_COLOR);
  463. hilog.info(0x0000, 'Heanup', '当前 NewIndex themeColor:' + themeColor);
  464. AppStorage.setOrCreate('themeColor', themeColor);
  465. this.themeColor = themeColor
  466. // ====== 本地会员同步提醒逻辑 ======
  467. // this.refreshUserInfoState();
  468. // if (this.isLogin) {
  469. // void this.fetchUserInfo();
  470. // }
  471. await UserUtil.fetchUserInfo();
  472. this.refreshUserInfoState();
  473. let changeUserState: emitter.InnerEvent = { eventId: EventConstants.EVENT_USER_STATE_CHANGE }
  474. emitter.on(changeUserState, () => {
  475. this.refreshUserInfoState();
  476. });
  477. let eventSetting: emitter.InnerEvent = { eventId: EventConstants.EVENT_SETTING_UPDATE }
  478. // 监听广播事件(通用设置配置更新)
  479. emitter.on(eventSetting, (eventData: emitter.EventData) => {
  480. this.isShowTitleBar = PreferencesUtil.getBooleanSync(SettingPage.IS_SHOW_TITLTBAR, true)
  481. });
  482. emitter.on({ eventId: EventConstants.EVENT_SETTING_BACK_TO_HOME }, () => {
  483. this.returnToConfiguredHome()
  484. });
  485. this.logHiCar('info', `getHiCarStatus isHiCarStatus=${this.isHiCarStatus}`)
  486. if(this.curDisplayIsHiCar||this.isBigScreen()){
  487. this.getUIContext()?.animateTo({ duration: 555 }, () => {
  488. this.isShowDrawer = true
  489. this.offsetX = 0
  490. })
  491. this.isShowTitleBar = false
  492. }
  493. // 检查并显示更新日志
  494. this.checkAndShowUpdateLog();
  495. // 初始化歌单数据库
  496. await this.initPlaylistTable();
  497. // 初始化WebDAV管理器
  498. await this.initWebDavManager();
  499. // 监听歌单刷新事件
  500. emitter.on({ eventId: EventConstants.EVENT_PLAYLIST_REFRESH }, (eventData: emitter.EventData) => {
  501. LogUtil.info('heanup NewIndex', '收到歌单刷新事件,开始刷新歌单列表')
  502. this.loadPlaylistList()
  503. });
  504. if(Utility.isOpenTime()&&!PreferencesUtil.getBooleanSync('isShowHaoPingDialog18', false)){
  505. setTimeout(()=>{
  506. Utility.showHaoPingDialog(this.getUIContext(),this.context,this.appName,this.bundleName)
  507. PreferencesUtil.putSync('isShowHaoPingDialog18', true)
  508. },50000)
  509. }
  510. this.initDefalutType()
  511. }
  512. private initDefalutType(){
  513. this.applyDefaultHomeState()
  514. console.info('onecold 网盘 默认首页2 this.mType='+this.mType )
  515. }
  516. private enterFindPage(): void {
  517. const wasFindPage: boolean = this.mType === 8
  518. this.mType = 8
  519. this.modeType = 5
  520. if (!wasFindPage) {
  521. this.handleFindPageEntry()
  522. }
  523. }
  524. private handleFindPageEntry(): void {
  525. if (this.defalut_home_type === 5 || PreferencesUtil.getBooleanSync(FIND_DEFAULT_HOME_PROMPT_HANDLED_KEY, false)) {
  526. return
  527. }
  528. const nextCount: number = PreferencesUtil.getNumberSync(FIND_DEFAULT_HOME_PROMPT_COUNT_KEY, 0) + 1
  529. PreferencesUtil.putSync(FIND_DEFAULT_HOME_PROMPT_COUNT_KEY, nextCount)
  530. if (nextCount >= FIND_DEFAULT_HOME_PROMPT_TARGET_COUNT) {
  531. setTimeout(() => {
  532. this.showFindDefaultHomePrompt()
  533. }, 360)
  534. }
  535. }
  536. private showFindDefaultHomePrompt(): void {
  537. if (this.mType !== 8 || this.defalut_home_type === 5 || this.isFindDefaultPromptShowing) {
  538. return
  539. }
  540. if (PreferencesUtil.getBooleanSync(FIND_DEFAULT_HOME_PROMPT_HANDLED_KEY, false)) {
  541. return
  542. }
  543. if (PreferencesUtil.getNumberSync(FIND_DEFAULT_HOME_PROMPT_COUNT_KEY, 0) < FIND_DEFAULT_HOME_PROMPT_TARGET_COUNT) {
  544. return
  545. }
  546. this.isFindDefaultPromptShowing = true
  547. AlertDialog.show({
  548. title: '设为默认发现页',
  549. message: '你已经多次进入发现页,是否将发现页设为启动默认页面?',
  550. primaryButton: {
  551. value: '取消',
  552. action: () => {
  553. PreferencesUtil.putSync(FIND_DEFAULT_HOME_PROMPT_HANDLED_KEY, true)
  554. this.isFindDefaultPromptShowing = false
  555. }
  556. },
  557. secondaryButton: {
  558. value: '确定',
  559. fontColor: this.themeColor,
  560. action: () => {
  561. this.defalut_home_type = 5
  562. PreferencesUtil.putSync('defalut_home_type', 5)
  563. PreferencesUtil.putSync(FIND_DEFAULT_HOME_PROMPT_HANDLED_KEY, true)
  564. emitter.emit({ eventId: EventConstants.EVENT_SETTING_UPDATE }, {})
  565. ToastUtil.showToast('已设为默认发现页')
  566. this.isFindDefaultPromptShowing = false
  567. }
  568. }
  569. })
  570. }
  571. private isRemoteMusicAccount(account: WebDavAccount | null | undefined): boolean {
  572. if (!account) {
  573. return false
  574. }
  575. return account.webType === RemoteDriveType.Navidrome
  576. || account.webType === RemoteDriveType.Jellyfin
  577. || account.webType === RemoteDriveType.Emby
  578. || account.webType === RemoteDriveType.AudioStation
  579. || account.webType === RemoteDriveType.Plex
  580. || account.webType === RemoteDriveType.DaoLiYu
  581. }
  582. private getConfiguredHomeAccount(): WebDavAccount | null {
  583. const activatedAccount = this.webdavManager.getActivatedWebDavAccount()
  584. if (activatedAccount) {
  585. return activatedAccount
  586. }
  587. if (this.selectedAccount && (this.selectedAccount.id > 0 || this.selectedAccount.host.length > 0)) {
  588. return this.selectedAccount
  589. }
  590. if (this.webDavAccounts.length > 0) {
  591. return this.webDavAccounts[0]
  592. }
  593. return null
  594. }
  595. private applyDefaultHomeState(): void {
  596. this.mType = 0
  597. this.defalut_home_type = PreferencesUtil.getNumberSync('defalut_home_type', 0)
  598. const configuredAccount = this.getConfiguredHomeAccount()
  599. this.currentSongListID = ''
  600. this.currentSongListName = ''
  601. this.modeType = 0
  602. this.tabSelectedIndexes = [0]
  603. if(this.defalut_home_type==1){
  604. this.mType = 0
  605. this.modeType = 1
  606. }else if(this.defalut_home_type==2){//如果是网盘的话
  607. this.tabSelectedIndexes = [2]
  608. if (configuredAccount) {
  609. this.selectedAccount = configuredAccount
  610. }
  611. this.mType = this.isRemoteMusicAccount(configuredAccount) ? 7 : 6
  612. }else if(this.defalut_home_type==3){
  613. this.mType = 0
  614. this.modeType = 2
  615. }else if(this.defalut_home_type==4){
  616. this.mType = 0
  617. this.modeType = 3
  618. }else if(this.defalut_home_type==5){
  619. this.enterFindPage()
  620. }
  621. }
  622. private returnToConfiguredHome(): void {
  623. this.getUIContext()?.animateTo({ duration: 555 }, () => {
  624. this.applyDefaultHomeState()
  625. })
  626. }
  627. /**
  628. * 检查并显示更新日志
  629. */
  630. private async checkAndShowUpdateLog() {
  631. try {
  632. const shouldShow = await UpdateLogManager.checkAndShowUpdateLog();
  633. if (shouldShow) {
  634. this.isShowUpdateDialog = !this.isShowUpdateDialog
  635. UpdateLogManager.markCurrentVersionShown()
  636. }
  637. // this.isShowUpdateDialog = true;//调试期间显示更新日志,测试完成后请删除
  638. } catch (error) {
  639. console.error('NewIndex checkAndShowUpdateLog error:', error);
  640. }
  641. }
  642. /**
  643. * 页面消失生命周期钩子
  644. * 注销断点系统
  645. */
  646. aboutToDisappear() {
  647. console.info('NewIndex aboutToDisappear');
  648. this.playbackController.clearHostActions(this.playbackHostActions)
  649. this.playbackCoordinator.clearRuntime()
  650. this.playbackCoordinator.clearDefaultRuntime(BackgroundAudioPlaybackHost.getInstance().getRuntime())
  651. setPlaybackRuntimeReady(false)
  652. this.clearMiniPlayerAnimationTimer();
  653. this.breakpointSystem.unregister();
  654. emitter.off(EventConstants.EVENT_SWIPE_BACK_UPDATE);
  655. emitter.off(EventConstants.EVENT_USER_STATE_CHANGE);
  656. emitter.off(EventConstants.EVENT_SETTING_BACK_TO_HOME);
  657. // 监听歌单刷新事件
  658. emitter.off(EventConstants.EVENT_PLAYLIST_REFRESH);
  659. // 清理WebDAV管理器订阅
  660. if (this.webdavManager) {
  661. this.webdavManager.observers = [];
  662. }
  663. }
  664. private clearMiniPlayerAnimationTimer(): void {
  665. if (this.pendingPlayerPageOpenTimer >= 0) {
  666. clearTimeout(this.pendingPlayerPageOpenTimer)
  667. this.pendingPlayerPageOpenTimer = -1
  668. }
  669. if (this.miniPlayerAnimationTimer >= 0) {
  670. clearTimeout(this.miniPlayerAnimationTimer)
  671. this.miniPlayerAnimationTimer = -1
  672. }
  673. if (this.miniPlayerCleanupTimer >= 0) {
  674. clearTimeout(this.miniPlayerCleanupTimer)
  675. this.miniPlayerCleanupTimer = -1
  676. }
  677. if (this.miniPlayerShakeTimer >= 0) {
  678. clearTimeout(this.miniPlayerShakeTimer)
  679. this.miniPlayerShakeTimer = -1
  680. }
  681. if (this.miniPlayerShakeSettleTimer >= 0) {
  682. clearTimeout(this.miniPlayerShakeSettleTimer)
  683. this.miniPlayerShakeSettleTimer = -1
  684. }
  685. if (this.miniPlayerModeAnimationTimer >= 0) {
  686. clearTimeout(this.miniPlayerModeAnimationTimer)
  687. this.miniPlayerModeAnimationTimer = -1
  688. }
  689. if (this.miniPlayerModeSettleTimer >= 0) {
  690. clearTimeout(this.miniPlayerModeSettleTimer)
  691. this.miniPlayerModeSettleTimer = -1
  692. }
  693. if (this.miniPlayerModeCompleteTimer >= 0) {
  694. clearTimeout(this.miniPlayerModeCompleteTimer)
  695. this.miniPlayerModeCompleteTimer = -1
  696. }
  697. this.clearMiniPlayerOrbTapTimer()
  698. this.isMiniPlayerModeTransitioning = false
  699. }
  700. private clearMiniPlayerOrbTapTimer(): void {
  701. if (this.miniPlayerOrbTapTimer >= 0) {
  702. clearTimeout(this.miniPlayerOrbTapTimer)
  703. this.miniPlayerOrbTapTimer = -1
  704. }
  705. this.lastMiniPlayerOrbTapAt = 0
  706. }
  707. private scheduleMiniPlayerOrbSingleTap(triggerAt: number): void {
  708. this.clearMiniPlayerOrbTapTimer()
  709. this.lastMiniPlayerOrbTapAt = triggerAt
  710. this.miniPlayerOrbTapTimer = setTimeout(() => {
  711. this.miniPlayerOrbTapTimer = -1
  712. if (!shouldTriggerMiniPlayerOrbSingleTap(this.lastMiniPlayerOrbTapAt, Date.now(),
  713. this.miniPlayerOrbDoubleTapWindowMs)) {
  714. return
  715. }
  716. this.lastMiniPlayerOrbTapAt = 0
  717. this.expandMiniPlayerFromOrb()
  718. }, this.miniPlayerOrbDoubleTapWindowMs)
  719. }
  720. private handleMiniPlayerOrbTap(): void {
  721. const now = Date.now()
  722. const action = resolveMiniPlayerOrbTapAction(
  723. this.miniPlayerOrbTapTimer >= 0,
  724. this.lastMiniPlayerOrbTapAt,
  725. now,
  726. this.miniPlayerOrbDoubleTapWindowMs
  727. )
  728. if (action === MiniPlayerOrbTapAction.TRIGGER_DOUBLE_TAP) {
  729. this.clearMiniPlayerOrbTapTimer()
  730. this.setShowPlayTrue()
  731. return
  732. }
  733. this.scheduleMiniPlayerOrbSingleTap(now)
  734. }
  735. // 窗口尺寸、断点或显示设备切换时,重新同步播放条表面尺寸。
  736. // 这样从手机态切到 HiCar 横屏后,surface 会立即按当前显示环境重新拉伸。
  737. private onMiniPlayerLayoutContextChange(): void {
  738. if (this.isMiniPlayerModeTransitioning) {
  739. return
  740. }
  741. this.syncMiniPlayerVisibility(false)
  742. }
  743. // 解析当前屏宽下播放条完整宽度,为 full/orb 切换提供统一目标值。
  744. private resolveMiniPlayerMorphMetrics() {
  745. const viewportWidthVp = this.windowWidth > 0 ? px2vp(this.windowWidth) : 360
  746. const miniPlayerHeightVp = this.bottomBarHeight > 0 ? this.bottomBarHeight : 70
  747. return resolveMiniPlayerMorphTarget(viewportWidthVp, 0.9, miniPlayerHeightVp)
  748. }
  749. // orb 直径跟随播放条高度,但限制在较稳定的范围内,避免不同设备上忽大忽小。
  750. private resolveMiniPlayerOrbSize(): number {
  751. const barHeightVp = this.bottomBarHeight > 0 ? this.bottomBarHeight : 70
  752. return Math.max(54, Math.min(60, barHeightVp - 8))
  753. }
  754. private resolveMiniPlayerSurfaceTargetWidth(): number {
  755. return this.isMiniPlayerOrbMode ? this.resolveMiniPlayerOrbSize() : this.resolveMiniPlayerMorphMetrics().barWidth
  756. }
  757. private resolveMiniPlayerSurfaceTargetHeight(): number {
  758. return this.isMiniPlayerOrbMode ? this.resolveMiniPlayerOrbSize() : this.bottomBarHeight
  759. }
  760. private resolveMiniPlayerSurfaceTargetRadius(): number {
  761. return this.isMiniPlayerOrbMode ? this.resolveMiniPlayerOrbSize() / 2 : 200
  762. }
  763. private applyMiniPlayerPresentationState(): void {
  764. this.miniPlayerSurfaceWidth = this.resolveMiniPlayerSurfaceTargetWidth()
  765. this.miniPlayerSurfaceHeight = this.resolveMiniPlayerSurfaceTargetHeight()
  766. this.miniPlayerBorderRadius = this.resolveMiniPlayerSurfaceTargetRadius()
  767. this.miniPlayerFullContentOpacity = this.isMiniPlayerOrbMode ? 0 : 1
  768. this.miniPlayerOrbOpacity = this.isMiniPlayerOrbMode ? 1 : 0
  769. }
  770. // 重置到稳定停留态,避免中途中断动画后状态残留。
  771. private resetMiniPlayerAnimationState(): void {
  772. this.miniPlayerScaleX = 1
  773. this.miniPlayerScaleY = 1
  774. this.miniPlayerOpacity = 1
  775. this.miniPlayerProxySize = 0
  776. this.miniPlayerProxyOpacity = 0
  777. this.miniPlayerContentScaleY = 1
  778. this.miniPlayerContentTranslateY = 0
  779. this.miniPlayerFullContentTranslateX = 0
  780. this.miniPlayerTranslateY = 0
  781. this.miniPlayerOrbScale = 1
  782. this.miniPlayerOrbTranslateX = 0
  783. this.applyMiniPlayerPresentationState()
  784. }
  785. // 播放条重新出现时做一次上下抖动,带动内部封面/文字/按钮一起轻微回弹。
  786. private startMiniPlayerRevealShake(): void {
  787. this.miniPlayerContentScaleY = 1
  788. this.miniPlayerContentTranslateY = 0
  789. this.getUIContext()?.animateTo({
  790. duration: this.miniPlayerShakeFirstDuration,
  791. curve: Curve.Sharp
  792. }, () => {
  793. this.miniPlayerScaleX = 1.012
  794. this.miniPlayerScaleY = 0.9
  795. this.miniPlayerContentScaleY = 0.965
  796. this.miniPlayerContentTranslateY = 1.2
  797. })
  798. this.miniPlayerShakeTimer = setTimeout(() => {
  799. this.miniPlayerShakeTimer = -1
  800. this.getUIContext()?.animateTo({
  801. duration: this.miniPlayerShakeSecondDuration,
  802. curve: Curve.Friction
  803. }, () => {
  804. this.miniPlayerScaleX = 0.996
  805. this.miniPlayerScaleY = 1.036
  806. this.miniPlayerContentScaleY = 1.018
  807. this.miniPlayerContentTranslateY = -0.8
  808. })
  809. }, this.miniPlayerShakeFirstDuration + 16)
  810. this.miniPlayerShakeSettleTimer = setTimeout(() => {
  811. this.miniPlayerShakeSettleTimer = -1
  812. this.getUIContext()?.animateTo({
  813. duration: this.miniPlayerShakeSettleDuration,
  814. curve: Curve.Friction
  815. }, () => {
  816. this.miniPlayerScaleX = 1
  817. this.miniPlayerScaleY = 1
  818. this.miniPlayerContentScaleY = 1
  819. this.miniPlayerContentTranslateY = 0
  820. })
  821. }, this.miniPlayerShakeFirstDuration + this.miniPlayerShakeSecondDuration + 32)
  822. }
  823. private isMiniPlayerFullyVisible(): boolean {
  824. const targetWidth = this.resolveMiniPlayerSurfaceTargetWidth()
  825. const targetRadius = this.resolveMiniPlayerSurfaceTargetRadius()
  826. const targetFullContentOpacity = this.isMiniPlayerOrbMode ? 0 : 1
  827. const targetOrbOpacity = this.isMiniPlayerOrbMode ? 1 : 0
  828. return this.isMiniPlayerMounted
  829. && this.miniPlayerOpacity >= 0.99
  830. && this.miniPlayerScaleX >= 0.999
  831. && this.miniPlayerScaleY >= 0.999
  832. && this.miniPlayerProxySize <= 0
  833. && Math.abs(this.miniPlayerSurfaceWidth - targetWidth) < 1
  834. && Math.abs(this.miniPlayerSurfaceHeight - this.resolveMiniPlayerSurfaceTargetHeight()) < 1
  835. && Math.abs(this.miniPlayerBorderRadius - targetRadius) < 1
  836. && Math.abs(this.miniPlayerFullContentOpacity - targetFullContentOpacity) < 0.05
  837. && Math.abs(this.miniPlayerOrbOpacity - targetOrbOpacity) < 0.05
  838. && Math.abs(this.miniPlayerContentScaleY - 1) < 0.01
  839. && Math.abs(this.miniPlayerContentTranslateY) < 0.2
  840. && Math.abs(this.miniPlayerTranslateY) < 1
  841. }
  842. private shouldShowMiniPlayer(): boolean {
  843. return shouldShowMiniPlayerBar(this.isMultiSelect, this.mType)
  844. }
  845. // 同步播放条挂载与显示状态。
  846. // show: 直接挂载后做抖动;hide: 向下轻压并淡出。
  847. private syncMiniPlayerVisibility(animated: boolean = true): void {
  848. const shouldShow = this.shouldShowMiniPlayer()
  849. this.clearMiniPlayerAnimationTimer()
  850. if (!animated) {
  851. this.isMiniPlayerMounted = shouldShow
  852. this.resetMiniPlayerAnimationState()
  853. return
  854. }
  855. if (shouldShow) {
  856. if (this.isMiniPlayerFullyVisible()) {
  857. return
  858. }
  859. this.isMiniPlayerMounted = true
  860. this.resetMiniPlayerAnimationState()
  861. this.miniPlayerAnimationTimer = setTimeout(() => {
  862. if (!this.shouldShowMiniPlayer() || !this.isMiniPlayerMounted) {
  863. this.miniPlayerAnimationTimer = -1
  864. return
  865. }
  866. this.miniPlayerAnimationTimer = -1
  867. this.startMiniPlayerRevealShake()
  868. }, 16)
  869. return
  870. }
  871. if (!this.isMiniPlayerMounted) {
  872. this.resetMiniPlayerAnimationState()
  873. return
  874. }
  875. this.getUIContext()?.animateTo({
  876. duration: this.miniPlayerDotDuration,
  877. curve: Curve.Ease
  878. }, () => {
  879. this.miniPlayerScaleX = 0.96
  880. this.miniPlayerScaleY = 0.88
  881. this.miniPlayerOpacity = 0
  882. this.miniPlayerBorderRadius = 200
  883. this.miniPlayerProxySize = 0
  884. this.miniPlayerProxyOpacity = 0
  885. this.miniPlayerContentScaleY = 0.96
  886. this.miniPlayerContentTranslateY = 0.8
  887. this.miniPlayerTranslateY = 18
  888. })
  889. this.miniPlayerCleanupTimer = setTimeout(() => {
  890. this.miniPlayerCleanupTimer = -1
  891. if (!this.shouldShowMiniPlayer()) {
  892. this.isMiniPlayerMounted = false
  893. }
  894. this.resetMiniPlayerAnimationState()
  895. }, this.miniPlayerDotDuration + 16)
  896. }
  897. // 点击左侧封面后,把完整播放条收拢成右侧圆球,并在右侧做一次左右回弹。
  898. private collapseMiniPlayerToOrb(): void {
  899. if (this.isMiniPlayerOrbMode || !this.isMiniPlayerMounted || this.isMiniPlayerModeTransitioning) {
  900. return
  901. }
  902. const fullWidth = this.resolveMiniPlayerMorphMetrics().barWidth
  903. const orbSize = this.resolveMiniPlayerOrbSize()
  904. const collapseOvershootWidth = Math.min(fullWidth, orbSize + 10)
  905. this.clearMiniPlayerAnimationTimer()
  906. this.isMiniPlayerModeTransitioning = true
  907. this.miniPlayerScaleX = 1
  908. this.miniPlayerScaleY = 1
  909. this.miniPlayerOpacity = 1
  910. this.miniPlayerContentScaleY = 1
  911. this.miniPlayerContentTranslateY = 0
  912. this.miniPlayerFullContentTranslateX = 0
  913. this.miniPlayerTranslateY = 0
  914. this.miniPlayerOrbScale = 0.82
  915. this.miniPlayerOrbTranslateX = 10
  916. this.isMiniPlayerOrbMode = true
  917. this.miniPlayerFullContentOpacity = 1
  918. this.miniPlayerOrbOpacity = 0
  919. this.getUIContext()?.animateTo({
  920. duration: this.miniPlayerModeDuration,
  921. curve: Curve.Sharp
  922. }, () => {
  923. this.miniPlayerSurfaceWidth = orbSize
  924. this.miniPlayerSurfaceHeight = orbSize
  925. this.miniPlayerBorderRadius = orbSize / 2
  926. this.miniPlayerFullContentOpacity = 0
  927. this.miniPlayerFullContentTranslateX = 24
  928. this.miniPlayerOrbOpacity = 1
  929. this.miniPlayerOrbScale = 1.06
  930. this.miniPlayerOrbTranslateX = 0
  931. })
  932. this.miniPlayerModeAnimationTimer = setTimeout(() => {
  933. this.miniPlayerModeAnimationTimer = -1
  934. if (!this.isMiniPlayerOrbMode || !this.isMiniPlayerMounted) {
  935. return
  936. }
  937. this.getUIContext()?.animateTo({
  938. duration: this.miniPlayerModeBounceDuration,
  939. curve: Curve.Friction
  940. }, () => {
  941. this.miniPlayerSurfaceWidth = collapseOvershootWidth
  942. this.miniPlayerBorderRadius = collapseOvershootWidth / 2
  943. this.miniPlayerOrbScale = 0.97
  944. })
  945. this.miniPlayerModeSettleTimer = setTimeout(() => {
  946. this.miniPlayerModeSettleTimer = -1
  947. if (!this.isMiniPlayerOrbMode || !this.isMiniPlayerMounted) {
  948. return
  949. }
  950. this.getUIContext()?.animateTo({
  951. duration: this.miniPlayerModeBounceDuration,
  952. curve: Curve.Friction
  953. }, () => {
  954. this.miniPlayerSurfaceWidth = orbSize
  955. this.miniPlayerBorderRadius = orbSize / 2
  956. this.miniPlayerOrbScale = 1
  957. this.miniPlayerFullContentTranslateX = 0
  958. })
  959. }, this.miniPlayerModeBounceDuration + 16)
  960. this.miniPlayerModeCompleteTimer = setTimeout(() => {
  961. this.miniPlayerModeCompleteTimer = -1
  962. this.isMiniPlayerModeTransitioning = false
  963. this.miniPlayerOrbScale = 1
  964. this.miniPlayerOrbTranslateX = 0
  965. this.miniPlayerFullContentTranslateX = 0
  966. }, this.miniPlayerModeBounceDuration * 2 + 32)
  967. }, this.miniPlayerModeDuration + 16)
  968. }
  969. // 再次点击右侧圆球时,从右向左展开回完整播放条,并保留回弹的收尾。
  970. private expandMiniPlayerFromOrb(): void {
  971. if (!this.isMiniPlayerOrbMode || !this.isMiniPlayerMounted || this.isMiniPlayerModeTransitioning) {
  972. return
  973. }
  974. const fullWidth = this.resolveMiniPlayerMorphMetrics().barWidth
  975. const orbSize = this.resolveMiniPlayerOrbSize()
  976. const expandOvershootWidth = fullWidth + 16
  977. this.clearMiniPlayerAnimationTimer()
  978. this.isMiniPlayerModeTransitioning = true
  979. this.miniPlayerScaleX = 1
  980. this.miniPlayerScaleY = 1
  981. this.miniPlayerOpacity = 1
  982. this.miniPlayerContentScaleY = 1
  983. this.miniPlayerContentTranslateY = 0
  984. this.miniPlayerFullContentTranslateX = 24
  985. this.miniPlayerTranslateY = 0
  986. this.miniPlayerOrbScale = 1
  987. this.miniPlayerOrbTranslateX = 0
  988. this.isMiniPlayerOrbMode = false
  989. this.miniPlayerSurfaceWidth = orbSize
  990. this.miniPlayerSurfaceHeight = orbSize
  991. this.miniPlayerBorderRadius = orbSize / 2
  992. this.miniPlayerFullContentOpacity = 0
  993. this.miniPlayerOrbOpacity = 1
  994. this.getUIContext()?.animateTo({
  995. duration: this.miniPlayerModeDuration,
  996. curve: Curve.Friction
  997. }, () => {
  998. this.miniPlayerSurfaceWidth = expandOvershootWidth
  999. this.miniPlayerSurfaceHeight = this.bottomBarHeight
  1000. this.miniPlayerBorderRadius = 200
  1001. this.miniPlayerFullContentOpacity = 1
  1002. this.miniPlayerFullContentTranslateX = -10
  1003. this.miniPlayerOrbOpacity = 0
  1004. this.miniPlayerOrbScale = 0.82
  1005. this.miniPlayerOrbTranslateX = 12
  1006. })
  1007. this.miniPlayerModeAnimationTimer = setTimeout(() => {
  1008. this.miniPlayerModeAnimationTimer = -1
  1009. if (this.isMiniPlayerOrbMode || !this.isMiniPlayerMounted) {
  1010. return
  1011. }
  1012. this.getUIContext()?.animateTo({
  1013. duration: this.miniPlayerModeBounceDuration,
  1014. curve: Curve.Friction
  1015. }, () => {
  1016. this.miniPlayerSurfaceWidth = fullWidth
  1017. this.miniPlayerBorderRadius = 200
  1018. this.miniPlayerFullContentTranslateX = 0
  1019. })
  1020. this.miniPlayerModeCompleteTimer = setTimeout(() => {
  1021. this.miniPlayerModeCompleteTimer = -1
  1022. this.isMiniPlayerModeTransitioning = false
  1023. this.miniPlayerOrbScale = 1
  1024. this.miniPlayerOrbTranslateX = 0
  1025. this.miniPlayerFullContentTranslateX = 0
  1026. }, this.miniPlayerModeBounceDuration + 16)
  1027. }, this.miniPlayerModeDuration + 16)
  1028. }
  1029. @Builder
  1030. private MiniPlayerMorphProxyDot() {
  1031. Stack({ alignContent: Alignment.Center }) {
  1032. Image(StrUtil.isNotEmpty(this.cover) ? this.cover as string : $r('app.media.alt'))
  1033. .width('100%')
  1034. .height('100%')
  1035. .objectFit(ImageFit.Cover)
  1036. .alt($r('app.media.alt'))
  1037. }
  1038. .width(this.miniPlayerProxySize)
  1039. .height(this.miniPlayerProxySize)
  1040. .borderRadius(this.miniPlayerProxySize)
  1041. .clip(true)
  1042. .opacity(this.miniPlayerProxyOpacity)
  1043. .shadow({
  1044. radius: 18,
  1045. type: ShadowType.BLUR,
  1046. color: 'on_primary'
  1047. })
  1048. }
  1049. @Builder
  1050. ContentBuild() {
  1051. Stack() {
  1052. LocalMusic()
  1053. .visibility(this.mType === 0 ? Visibility.Visible : Visibility.None)
  1054. // 本地音乐内容区
  1055. if(this.mType === 1 ){
  1056. UserCenter()
  1057. }else if(this.mType === 2 ){
  1058. ScanFilePage()
  1059. }else if(this.mType === 3 ){
  1060. ChartsCount({
  1061. isShowDrawer:this.isShowDrawer,
  1062. offsetX:this.offsetX
  1063. })
  1064. }else if(this.mType === 4 ){
  1065. SettingPage()
  1066. }else if(this.mType === 5 ){
  1067. AboutPage()
  1068. }else if(this.mType === 8 ){//发现页
  1069. FindView()
  1070. }
  1071. if(this.mType === 6){
  1072. WebDavMainPage({
  1073. offsetX:this.offsetX,
  1074. isShowDrawer:this.isShowDrawer,
  1075. mType:this.mType,
  1076. selectedAccount:this.selectedAccount
  1077. })
  1078. .visibility(this.mType === 6 ? Visibility.Visible : Visibility.None)
  1079. }
  1080. if(this.mType === 7){
  1081. RemoteMusicPage({
  1082. offsetX:this.offsetX,
  1083. isDetailView:this.isDetailView,
  1084. isShowDrawer:this.isShowDrawer,
  1085. mType:this.mType,
  1086. selectedAccount:this.selectedAccount
  1087. })
  1088. .visibility(this.mType === 7 ? Visibility.Visible : Visibility.None)
  1089. }
  1090. // 抽屉打开时的遮罩层,用于拦截点击事件 这个只能正常尺寸的手机竖屏的才能生效
  1091. if (this.isShowDrawer&&this.isPhonePortrait()) {
  1092. Column()
  1093. .width('100%')
  1094. .height('100%')
  1095. .backgroundBlurStyle(BlurStyle.BACKGROUND_THIN)
  1096. .transition(TransitionEffect.OPACITY.animation({
  1097. curve: curves.springMotion(0, 1)
  1098. }))
  1099. .onClick(() => {
  1100. // 点击遮罩层关闭抽屉
  1101. this.getUIContext()?.animateTo({ duration: 555 }, () => {
  1102. this.isShowDrawer = false
  1103. })
  1104. })
  1105. }
  1106. }
  1107. .bindSheet($$this.isShowUpdateDialog, this.updateSheet(), {
  1108. height: '90%',
  1109. dragBar: true,
  1110. preferType: this.currentBreakpoint !== BreakpointTypeEnum.SM ? SheetType.CENTER : SheetType.BOTTOM,
  1111. showClose: true,
  1112. blurStyle: BlurStyle.Regular,
  1113. title: { title: '更新日志' }
  1114. })
  1115. }
  1116. @State private pointLightOptions: hdsEffect.PointLightOptions | undefined = undefined
  1117. // 迷你播放条挂载后统一走这个 Builder,避免 build() 里堆叠过多手机/HiCar/orb 分支。
  1118. @Builder
  1119. private MiniPlayerBarBuilder() {
  1120. MiniPlayerBar({
  1121. bottomBarHeight: this.bottomBarHeight,
  1122. bottomSafeHeight: this.bottomSafeHeight,
  1123. curDisplayIsHiCar: this.curDisplayIsHiCar,
  1124. cover: this.cover,
  1125. currentSong: this.currentSong,
  1126. themeColor: this.themeColor,
  1127. progressValue: this.progressValue,
  1128. controlPlayStatus: this.CONTROL_PlayStatus,
  1129. isShowPrecious: this.isShowPrecious,
  1130. isMiniPlayerOrbMode: this.isMiniPlayerOrbMode,
  1131. isMiniPlayerModeTransitioning: this.isMiniPlayerModeTransitioning,
  1132. miniPlayerScaleX: this.miniPlayerScaleX,
  1133. miniPlayerScaleY: this.miniPlayerScaleY,
  1134. miniPlayerOpacity: this.miniPlayerOpacity,
  1135. miniPlayerBorderRadius: this.miniPlayerBorderRadius,
  1136. miniPlayerProxySize: this.miniPlayerProxySize,
  1137. miniPlayerProxyOpacity: this.miniPlayerProxyOpacity,
  1138. miniPlayerContentScaleY: this.miniPlayerContentScaleY,
  1139. miniPlayerContentTranslateY: this.miniPlayerContentTranslateY,
  1140. miniPlayerFullContentTranslateX: this.miniPlayerFullContentTranslateX,
  1141. miniPlayerTranslateY: this.miniPlayerTranslateY,
  1142. miniPlayerSurfaceWidth: this.miniPlayerSurfaceWidth,
  1143. miniPlayerSurfaceHeight: this.miniPlayerSurfaceHeight,
  1144. miniPlayerFullContentOpacity: this.miniPlayerFullContentOpacity,
  1145. miniPlayerOrbOpacity: this.miniPlayerOrbOpacity,
  1146. miniPlayerOrbScale: this.miniPlayerOrbScale,
  1147. miniPlayerOrbTranslateX: this.miniPlayerOrbTranslateX,
  1148. onOpenPlayer: (): void => {
  1149. this.setShowPlayTrue()
  1150. },
  1151. onCollapseToOrb: (): void => {
  1152. this.collapseMiniPlayerToOrb()
  1153. },
  1154. onOrbTap: (): void => {
  1155. this.handleMiniPlayerOrbTap()
  1156. },
  1157. onPlayPrevious: (): void => {
  1158. void this.playbackController.playPrevious()
  1159. },
  1160. onPlayOrPause: (): void => {
  1161. void this.playbackController.playOrPause()
  1162. },
  1163. onPlayNext: (): void => {
  1164. void this.playbackController.playNext()
  1165. },
  1166. onOpenPlayList: (): void => {
  1167. void this.playbackController.openPlayList()
  1168. }
  1169. })
  1170. }
  1171. build() {
  1172. SideBarContainer(SideBarContainerType.AUTO) {
  1173. Column() {
  1174. this.getLeftView()
  1175. }
  1176. .backgroundColor(Color.Transparent)
  1177. Stack() {
  1178. this.ContentBuild()
  1179. if (this.isMiniPlayerMounted) {
  1180. this.MiniPlayerBarBuilder()
  1181. }
  1182. }
  1183. .alignContent(Alignment.Bottom)
  1184. .width('100%')
  1185. .height('100%')
  1186. .gesture(SwipeGesture({ direction: SwipeDirection.Horizontal }).onAction((event: GestureEvent) => {
  1187. if (event) {//手势返回
  1188. this.getUIContext()?.animateTo({ duration: 555 }, () => {
  1189. this.isShowDrawer = !this.isShowDrawer
  1190. if(this.isShowDrawer)
  1191. this.offsetX = 0
  1192. })
  1193. }
  1194. }))
  1195. }
  1196. .showControlButton(false)
  1197. .minContentWidth(0)
  1198. .sideBarWidth(260)
  1199. .autoHide(true)
  1200. .showSideBar($$this.isShowDrawer)
  1201. .onChange((value: boolean) => {
  1202. this.isShowDrawer = value
  1203. })
  1204. }
  1205. @Builder
  1206. centerName() {
  1207. Column() {
  1208. Text(this.currentSong?.name)
  1209. .fontSize(16)
  1210. .textOverflow({ overflow: TextOverflow.MARQUEE }) //超长滚动
  1211. .maxLines(1)
  1212. .fontColor(Color.White)
  1213. .fontWeight(FontWeight.Bolder);
  1214. Row() {
  1215. Text(this.currentSong?.artist)
  1216. .margin({ top: 2 })
  1217. .fontSize(13)
  1218. .textAlign(TextAlign.Start)
  1219. .maxLines(1)
  1220. .fontWeight(FontWeight.Bold)
  1221. .textOverflow({ overflow: TextOverflow.MARQUEE }) //超长滚动
  1222. .fontColor(Color.White)
  1223. }
  1224. .visibility(this.currentSong?.artist? Visibility.Visible:Visibility.None)
  1225. }
  1226. .alignItems(HorizontalAlign.Center)
  1227. }
  1228. setShowPlayTrue(){
  1229. if (this.isMiniPlayerModeTransitioning) {
  1230. return
  1231. }
  1232. this.playbackController.showPlayerView()
  1233. }
  1234. // 右侧圆球内部内容:封面、暗罩、白色环形进度和中心播放状态指示。
  1235. @Builder
  1236. private buildMiniPlayerOrbButtonContent(orbSize: number) {
  1237. Stack({ alignContent: Alignment.Center }) {
  1238. Image(StrUtil.isNotEmpty(this.cover) ? this.cover as string : $r('app.media.alt'))
  1239. .width('100%')
  1240. .height('100%')
  1241. .objectFit(ImageFit.Cover)
  1242. .alt($r('app.media.alt'))
  1243. .borderRadius(100)
  1244. Column()
  1245. .width('100%')
  1246. .height('100%')
  1247. .backgroundColor('#66000000')
  1248. Progress({
  1249. value: Math.floor(this.progressValue),
  1250. total: 100,
  1251. type: ProgressType.Ring,
  1252. })
  1253. .color(Color.White)
  1254. .width('100%')
  1255. .height('100%')
  1256. .style({ strokeWidth: 3 })
  1257. if (this.CONTROL_PlayStatus === PlayStatus.PLAY) {
  1258. PlayingIndicator({
  1259. isActive: true,
  1260. indicatorSize: Math.max(16, Math.floor(orbSize * 0.34)),
  1261. indicatorColor: '#FFFFFF'
  1262. })
  1263. }
  1264. }
  1265. .width('100%')
  1266. .height('100%')
  1267. .clip(true)
  1268. .borderRadius(100)
  1269. }
  1270. // 右侧圆球外层按钮,负责点光、缩放、位移和点击展开。
  1271. @Builder
  1272. private MiniPlayerOrbControl() {
  1273. PointLightContentButton({
  1274. pointColor: this.themeColor,
  1275. buttonRadius: this.resolveMiniPlayerOrbSize() / 2,
  1276. pointLightHeight: 96,
  1277. pressScale: 0.94,
  1278. useShadow: true,
  1279. builder: (): void => {
  1280. this.buildMiniPlayerOrbButtonContent(this.resolveMiniPlayerOrbSize())
  1281. }
  1282. })
  1283. .width(this.resolveMiniPlayerOrbSize())
  1284. .height(this.resolveMiniPlayerOrbSize())
  1285. .opacity(this.miniPlayerOrbOpacity)
  1286. .borderRadius(this.resolveMiniPlayerOrbSize() / 2)
  1287. .scale({ x: this.miniPlayerOrbScale, y: this.miniPlayerOrbScale, centerX: '50%', centerY: '50%' })
  1288. .translate({ x: this.miniPlayerOrbTranslateX })
  1289. .onClick((): void => {
  1290. this.handleMiniPlayerOrbTap()
  1291. })
  1292. }
  1293. @Builder
  1294. PlayController() {
  1295. Row() {
  1296. this.playConLeft()
  1297. this.playConRigth()
  1298. }
  1299. .width('100%')
  1300. .height(this.bottomBarHeight)
  1301. .scale({ x: 1, y: this.miniPlayerContentScaleY, centerX: '50%', centerY: '50%' })
  1302. .translate({ y: this.miniPlayerContentTranslateY })
  1303. .hitTestBehavior(HitTestMode.Transparent)
  1304. .zIndex(2)
  1305. .opacity(0.9)
  1306. .onClick(()=>{
  1307. this.setShowPlayTrue()
  1308. })
  1309. .onTouch((event: TouchEvent): void => {
  1310. if (event.type === TouchType.Down) {
  1311. this.pointLightOptions = {
  1312. color: this.themeColor,
  1313. intensity: 1,
  1314. height: 60
  1315. }
  1316. } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
  1317. this.pointLightOptions = undefined
  1318. }
  1319. })
  1320. .visualEffect( deviceInfo.sdkApiVersion >= 20
  1321. ? new hdsEffect.HdsEffectBuilder()
  1322. .pointLight({
  1323. options: this.pointLightOptions,
  1324. illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT
  1325. })
  1326. .buildEffect()
  1327. : undefined)
  1328. .padding({
  1329. left: 16,
  1330. right: 16
  1331. })
  1332. }
  1333. @Builder
  1334. private HiCarPlayController() {
  1335. Stack({ alignContent: Alignment.Center }) {
  1336. this.hiCarLeadingCluster()
  1337. this.hiCarCenterInfo()
  1338. }
  1339. .opacity(0.9)
  1340. .width('100%')
  1341. .height(this.bottomBarHeight)
  1342. .hitTestBehavior(HitTestMode.Transparent)
  1343. .zIndex(2)
  1344. .onClick(()=>{
  1345. this.setShowPlayTrue()
  1346. })
  1347. .onTouch((event: TouchEvent): void => {
  1348. if (event.type === TouchType.Down) {
  1349. this.pointLightOptions = {
  1350. color: this.themeColor,
  1351. intensity: 1,
  1352. height: 60
  1353. }
  1354. } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
  1355. this.pointLightOptions = undefined
  1356. }
  1357. })
  1358. .visualEffect(deviceInfo.sdkApiVersion >= 20
  1359. ? new hdsEffect.HdsEffectBuilder()
  1360. .pointLight({
  1361. options: this.pointLightOptions,
  1362. illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT
  1363. })
  1364. .buildEffect()
  1365. : undefined)
  1366. .padding({
  1367. left: 16,
  1368. right: 16
  1369. })
  1370. }
  1371. @Builder
  1372. private hiCarLeadingCluster() {
  1373. Row() {
  1374. this.hiCarCoverControl()
  1375. this.hiCarPlayConRigth()
  1376. Row()
  1377. .layoutWeight(1)
  1378. }
  1379. .width('100%')
  1380. .height('100%')
  1381. .justifyContent(FlexAlign.Start)
  1382. .alignItems(VerticalAlign.Center)
  1383. }
  1384. // 迷你播放条与 HiCar 模式复用同一个封面按钮,统一点击反馈与发光效果。
  1385. @Builder
  1386. private buildMiniPlayerCoverContent() {
  1387. Stack({ alignContent: Alignment.Center }) {
  1388. Image(StrUtil.isNotEmpty(this.currentSong?.pixelMapPath) ?
  1389. this.currentSong?.pixelMapPath : $r('app.media.alt'))
  1390. .width(48)
  1391. .height(48)
  1392. .objectFit(ImageFit.Contain)
  1393. .alt($r('app.media.alt'))
  1394. .fillColor(this.themeColor)
  1395. .borderRadius(8)
  1396. .shadow({
  1397. radius: 15,
  1398. type: ShadowType.BLUR,
  1399. color: 'on_primary'
  1400. })
  1401. }
  1402. .width(48)
  1403. .height(48)
  1404. }
  1405. @Builder
  1406. private miniPlayerCoverButton() {
  1407. PointLightContentButton({
  1408. pointColor: this.themeColor,
  1409. buttonRadius: 10,
  1410. pointLightHeight: 88,
  1411. pressScale: 0.92,
  1412. useShadow: false,
  1413. builder: () => {
  1414. this.buildMiniPlayerCoverContent()
  1415. }
  1416. })
  1417. .width(48)
  1418. .height(48)
  1419. .margin({ left: 5 })
  1420. .zIndex(3)
  1421. .onClick((): void => {
  1422. this.collapseMiniPlayerToOrb()
  1423. })
  1424. }
  1425. @Builder
  1426. playConLeft() {
  1427. Row() {
  1428. this.miniPlayerCoverButton()
  1429. Column() {
  1430. Text(this.currentSong?.name)
  1431. .fontSize(16)
  1432. .textOverflow({ overflow: TextOverflow.MARQUEE }) //超长滚动
  1433. .maxLines(1)
  1434. .fontColor(Color.White)
  1435. .fontWeight(FontWeight.Bolder);
  1436. Row() {
  1437. Text(this.currentSong?.artist)
  1438. .margin({ top: 2 })
  1439. .fontSize(13)
  1440. .textAlign(TextAlign.Start)
  1441. .maxLines(1)
  1442. .fontWeight(FontWeight.Bold)
  1443. .textOverflow({ overflow: TextOverflow.MARQUEE }) //超长滚动
  1444. .fontColor(Color.White)
  1445. }
  1446. .visibility(this.currentSong?.artist? Visibility.Visible:Visibility.None)
  1447. }
  1448. .padding({left:8})
  1449. .alignItems(HorizontalAlign.Start)
  1450. .margin({ right: 22 })
  1451. .onClick((): void => {
  1452. this.setShowPlayTrue()
  1453. })
  1454. }
  1455. .padding({ right: 18 })
  1456. .layoutWeight(1)
  1457. .alignItems(VerticalAlign.Center)
  1458. .justifyContent(FlexAlign.Start)
  1459. }
  1460. @Builder
  1461. private hiCarCoverControl() {
  1462. this.miniPlayerCoverButton()
  1463. }
  1464. @Builder
  1465. private hiCarCenterInfo() {
  1466. Column() {
  1467. Text(this.currentSong?.name)
  1468. .fontSize(16)
  1469. .textOverflow({ overflow: TextOverflow.MARQUEE })
  1470. .maxLines(1)
  1471. .textAlign(TextAlign.Center)
  1472. .fontColor(Color.White)
  1473. .fontWeight(FontWeight.Bolder)
  1474. Row() {
  1475. Text(this.currentSong?.artist)
  1476. .margin({ top: 2 })
  1477. .fontSize(13)
  1478. .textAlign(TextAlign.Center)
  1479. .maxLines(1)
  1480. .fontWeight(FontWeight.Bold)
  1481. .textOverflow({ overflow: TextOverflow.MARQUEE })
  1482. .fontColor(Color.White)
  1483. }
  1484. .visibility(this.currentSong?.artist ? Visibility.Visible : Visibility.None)
  1485. }
  1486. .width('42%')
  1487. .alignItems(HorizontalAlign.Center)
  1488. .onClick((): void => {
  1489. this.setShowPlayTrue()
  1490. })
  1491. }
  1492. @Builder
  1493. private buildPlayPreviousControlContent() {
  1494. Row() {
  1495. SymbolGlyph($r('sys.symbol.backward_end_fill'))
  1496. .fontSize(28)
  1497. .fontColor([Color.White])
  1498. }
  1499. .width('100%')
  1500. .height('100%')
  1501. .justifyContent(FlexAlign.Center)
  1502. .alignItems(VerticalAlign.Center)
  1503. }
  1504. @Builder
  1505. private buildPlayToggleControlContent() {
  1506. Row() {
  1507. Stack() {
  1508. Progress({
  1509. value: Math.floor(this.progressValue),
  1510. type: ProgressType.Ring,
  1511. })
  1512. .color(this.themeColor)
  1513. .height(38)
  1514. .aspectRatio(CommonConstants.ASPECT_RATIO)
  1515. Image(this.CONTROL_PlayStatus === PlayStatus.PLAY ? $r('app.media.hm_pause') : $r('app.media.hm_play2'))
  1516. .height(36)
  1517. .width(36)
  1518. .fillColor(Color.White)
  1519. }
  1520. }
  1521. .width('100%')
  1522. .height('100%')
  1523. .justifyContent(FlexAlign.Center)
  1524. .alignItems(VerticalAlign.Center)
  1525. }
  1526. @Builder
  1527. private buildPlayNextControlContent() {
  1528. Row() {
  1529. SymbolGlyph($r('sys.symbol.forward_end_fill'))
  1530. .fontSize(28)
  1531. .fontColor([Color.White])
  1532. }
  1533. .width('100%')
  1534. .height('100%')
  1535. .justifyContent(FlexAlign.Center)
  1536. .alignItems(VerticalAlign.Center)
  1537. }
  1538. @Builder
  1539. private buildPlayListControlContent() {
  1540. Row() {
  1541. SymbolGlyph($r('sys.symbol.music_note_list'))
  1542. .fontSize(28)
  1543. .fontColor([Color.White])
  1544. }
  1545. .width('100%')
  1546. .height('100%')
  1547. .justifyContent(FlexAlign.Center)
  1548. .alignItems(VerticalAlign.Center)
  1549. }
  1550. @Builder
  1551. playConRigth() {
  1552. Row() {
  1553. PointLightContentButton({
  1554. pointColor: this.themeColor,
  1555. buttonRadius: 22,
  1556. pointLightHeight: 56,
  1557. pressScale: 0.88,
  1558. builder: () => {
  1559. this.buildPlayPreviousControlContent()
  1560. }
  1561. })
  1562. .width(44)
  1563. .height(44)
  1564. .visibility(this.isShowPrecious ? Visibility.Visible : Visibility.None)
  1565. .displayPriority(2)
  1566. .onClick(() => {
  1567. if (this.isMiniPlayerModeTransitioning) {
  1568. return
  1569. }
  1570. this.getUIContext().getHostContext()!.eventHub.emit('playPrevious');
  1571. })
  1572. // 播放进度条与播放键共用一个点光点击区域
  1573. PointLightContentButton({
  1574. pointColor: this.themeColor,
  1575. buttonRadius: 23,
  1576. pointLightHeight: 62,
  1577. pressScale: 0.9,
  1578. builder: () => {
  1579. this.buildPlayToggleControlContent()
  1580. }
  1581. })
  1582. .width(46)
  1583. .height(46)
  1584. .displayPriority(3)
  1585. .onClick(() => {
  1586. if (this.isMiniPlayerModeTransitioning) {
  1587. return
  1588. }
  1589. this.getUIContext().getHostContext()!.eventHub.emit('playOrPause');
  1590. })
  1591. PointLightContentButton({
  1592. pointColor: this.themeColor,
  1593. buttonRadius: 22,
  1594. pointLightHeight: 56,
  1595. pressScale: 0.88,
  1596. builder: () => {
  1597. this.buildPlayNextControlContent()
  1598. }
  1599. })
  1600. .width(44)
  1601. .height(44)
  1602. .displayPriority(2)
  1603. .onClick(() => {
  1604. if (this.isMiniPlayerModeTransitioning) {
  1605. return
  1606. }
  1607. this.getUIContext().getHostContext()!.eventHub.emit('playNext');
  1608. })
  1609. PointLightContentButton({
  1610. pointColor: this.themeColor,
  1611. buttonRadius: 21,
  1612. pointLightHeight: 52,
  1613. pressScale: 0.88,
  1614. builder: () => {
  1615. this.buildPlayListControlContent()
  1616. }
  1617. })
  1618. .width(42)
  1619. .height(42)
  1620. .displayPriority(1)
  1621. .onClick(() => {
  1622. if (this.isMiniPlayerModeTransitioning) {
  1623. return
  1624. }
  1625. void this.playbackController.openPlayList()
  1626. })
  1627. }
  1628. .margin({left:5})
  1629. .justifyContent(FlexAlign.End)
  1630. .alignItems(VerticalAlign.Center)
  1631. }
  1632. @Builder
  1633. private hiCarPlayConRigth() {
  1634. Row({space:8}) {
  1635. PointLightContentButton({
  1636. pointColor: this.themeColor,
  1637. buttonRadius: 22,
  1638. pointLightHeight: 56,
  1639. pressScale: 0.88,
  1640. builder: () => {
  1641. this.buildPlayPreviousControlContent()
  1642. }
  1643. })
  1644. .width(44)
  1645. .height(44)
  1646. .visibility(this.isShowPrecious ? Visibility.Visible : Visibility.None)
  1647. .displayPriority(2)
  1648. .onClick(() => {
  1649. if (this.isMiniPlayerModeTransitioning) {
  1650. return
  1651. }
  1652. this.getUIContext().getHostContext()!.eventHub.emit('playPrevious');
  1653. })
  1654. PointLightContentButton({
  1655. pointColor: this.themeColor,
  1656. buttonRadius: 23,
  1657. pointLightHeight: 62,
  1658. pressScale: 0.9,
  1659. builder: () => {
  1660. this.buildPlayToggleControlContent()
  1661. }
  1662. })
  1663. .width(46)
  1664. .height(46)
  1665. .displayPriority(3)
  1666. .onClick(() => {
  1667. if (this.isMiniPlayerModeTransitioning) {
  1668. return
  1669. }
  1670. this.getUIContext().getHostContext()!.eventHub.emit('playOrPause');
  1671. })
  1672. PointLightContentButton({
  1673. pointColor: this.themeColor,
  1674. buttonRadius: 22,
  1675. pointLightHeight: 56,
  1676. pressScale: 0.88,
  1677. builder: () => {
  1678. this.buildPlayNextControlContent()
  1679. }
  1680. })
  1681. .width(44)
  1682. .height(44)
  1683. .displayPriority(2)
  1684. .onClick(() => {
  1685. if (this.isMiniPlayerModeTransitioning) {
  1686. return
  1687. }
  1688. this.getUIContext().getHostContext()!.eventHub.emit('playNext');
  1689. })
  1690. PointLightContentButton({
  1691. pointColor: this.themeColor,
  1692. buttonRadius: 21,
  1693. pointLightHeight: 52,
  1694. pressScale: 0.88,
  1695. builder: () => {
  1696. this.buildPlayListControlContent()
  1697. }
  1698. })
  1699. .width(42)
  1700. .height(42)
  1701. .displayPriority(1)
  1702. .onClick(() => {
  1703. if (this.isMiniPlayerModeTransitioning) {
  1704. return
  1705. }
  1706. void this.playbackController.openPlayList()
  1707. })
  1708. }
  1709. .margin({ left: 16 })
  1710. .justifyContent(FlexAlign.Start)
  1711. .alignItems(VerticalAlign.Center)
  1712. }
  1713. //是不是hicar的屏幕分辨率
  1714. isHiCar() {
  1715. return this.curDisplayIsHiCar;
  1716. }
  1717. //是不是正常的手机竖屏
  1718. isPhonePortrait() {
  1719. LogUtil.info('twocold this.currentHeightBreakpoint = '+this.currentHeightBreakpoint)
  1720. LogUtil.info('twocold this.currentWidthBreakpoint = '+this.currentWidthBreakpoint)
  1721. if(DeviceUtil.getDeviceType() === resourceManager.DeviceType.DEVICE_TYPE_PHONE
  1722. &&this.currentHeightBreakpoint == HeightBreakpoint.HEIGHT_LG
  1723. &&this.currentWidthBreakpoint==WidthBreakpoint.WIDTH_SM){//如果是手机横屏,返回false
  1724. return true
  1725. }
  1726. return false
  1727. }
  1728. /**
  1729. * 判断是否横屏
  1730. * @returns
  1731. */
  1732. isBigScreen() {
  1733. if (this.currentWidthBreakpoint == WidthBreakpoint.WIDTH_LG) {
  1734. return true
  1735. }
  1736. if (DeviceUtil.getDeviceType() === resourceManager.DeviceType.DEVICE_TYPE_PHONE
  1737. && this.currentWidthBreakpoint === WidthBreakpoint.WIDTH_SM
  1738. && this.currentHeightBreakpoint == HeightBreakpoint.HEIGHT_LG) {
  1739. return false
  1740. } else {
  1741. return true
  1742. }
  1743. return false
  1744. }
  1745. @Builder
  1746. getLeftView() {
  1747. Column() {
  1748. Column() {
  1749. this.getDrawerView()
  1750. }
  1751. .width('100%')
  1752. .height('100%')
  1753. .onClick((event: ClickEvent) => {
  1754. // 点击抽屉内部不关闭,通过返回值阻止事件传播
  1755. return true
  1756. })
  1757. }
  1758. // .width(this.currentBreakpoint !== BreakpointTypeEnum.SM?'36%':'70%')
  1759. .height('100%')
  1760. .borderRadius({
  1761. topLeft: 0,
  1762. topRight: 20,
  1763. bottomLeft: 0,
  1764. bottomRight: 20
  1765. })
  1766. // .backgroundColor($r('app.color.silvery'))
  1767. // .backgroundColor($r('app.color.index_background'))
  1768. .backgroundColor($r('app.color.user_center_card_background'))
  1769. .alignItems(HorizontalAlign.Start)
  1770. .translate({ x: this.offsetX, y: 0, z: 0 })
  1771. .transition({ type: TransitionType.Insert, translate: { x: -DisplayUtil.getWidth(), y: 0 } })
  1772. .transition({ type: TransitionType.Delete, translate: { x: -DisplayUtil.getWidth(), y: 0 } })
  1773. .gesture(
  1774. PanGesture()
  1775. .onActionUpdate((event: GestureEvent) => {
  1776. // 手势滑动更新抽屉偏移
  1777. if (event.offsetX < 0) {
  1778. this.offsetX = event.offsetX;
  1779. }
  1780. })
  1781. .onActionEnd(() => {
  1782. // 手势结束判断是否关闭抽屉
  1783. if (this.offsetX < -DisplayUtil.getWidth() / 15) {
  1784. this.getUIContext()?.animateTo({ duration: 555 }, () => {
  1785. this.isShowDrawer = false
  1786. })
  1787. } else {
  1788. this.getUIContext()?.animateTo({ duration: 555 }, () => {
  1789. this.isShowDrawer = true
  1790. this.offsetX = 0
  1791. })
  1792. }
  1793. })
  1794. )
  1795. }
  1796. @State isLogin: boolean = false;
  1797. @State userAvatar: Resource = $r('app.media.icon_person2');
  1798. @State userAvatarUrl: string = '';
  1799. @State isVip: boolean = false;
  1800. @State userName: string = '未登录用户';
  1801. @State userId: number = 0;
  1802. @State hasActiveSubscription: boolean = false;
  1803. @State subscriptionName: string = '';
  1804. @State subscriptionEndDate: string = '';
  1805. // 用户信息卡片
  1806. @Builder
  1807. buildUserInfoCard() {
  1808. Column() {
  1809. this.userInfoView()
  1810. Column({ space: 20 }) {
  1811. SegmentButton({ options: this.tabOptions, selectedIndexes: $tabSelectedIndexes })
  1812. }
  1813. .padding({ bottom:10 })
  1814. .width('90%')
  1815. }
  1816. }
  1817. @Builder
  1818. userInfoView() {
  1819. Stack({ alignContent: Alignment.BottomEnd }) {
  1820. Row() {
  1821. Stack({ alignContent: Alignment.BottomEnd }){
  1822. Image(this.userAvatarUrl && this.userAvatarUrl.length > 0 ? this.userAvatarUrl : this.userAvatar)
  1823. .width(55)
  1824. .height(55)
  1825. .margin({ left: 12 })
  1826. .borderRadius('50%')
  1827. .clip(true)
  1828. // .fillColor(this.themeColor)
  1829. // .backgroundColor(this.isDarkMode ? Color.Black : Color.White)
  1830. Column() {
  1831. Text('VIP')
  1832. .fontSize(9)
  1833. .padding(2)
  1834. .textAlign(TextAlign.Center)
  1835. .fontWeight(FontWeight.Bolder)
  1836. .fontColor(Color.White)
  1837. }
  1838. .width(28)
  1839. .height(16)
  1840. .visibility(Utility.isNoble()?Visibility.Visible:Visibility.None)
  1841. .borderRadius(15)
  1842. .backgroundColor(this.themeColor)
  1843. }
  1844. Column() {
  1845. Row() {
  1846. Text(this.isLogin ? this.userName : '未登录用户')
  1847. .fontSize(14)
  1848. // .fontWeight(FontWeight.Bold)
  1849. // .fontColor(this.isDarkMode ? Color.White : Color.Black)
  1850. .textAlign(TextAlign.Start)
  1851. .maxLines(1)
  1852. .width(110)
  1853. .margin({ right: 12 })
  1854. }
  1855. .justifyContent(FlexAlign.Start)
  1856. .margin({ top: 2 })
  1857. if (this.isLogin) {
  1858. if (this.hasActiveSubscription) {
  1859. // Text(this.subscriptionName)
  1860. // .fontSize(13)
  1861. // .fontColor(themeColorWithAlpha(this.themeColor, 0.8, false))
  1862. // .textAlign(TextAlign.Start)
  1863. // .padding({top:5})
  1864. this.TimeTextBuilder()
  1865. } else {
  1866. Text('普通用户')
  1867. .fontSize(13)
  1868. .fontColor(this.isDarkMode ? Color.White : Color.Grey)
  1869. .textAlign(TextAlign.Start)
  1870. .padding({top:5})
  1871. this.TimeTextBuilder()
  1872. }
  1873. } else {
  1874. this.TimeTextBuilder()
  1875. }
  1876. }
  1877. .alignItems(HorizontalAlign.Start)
  1878. .width(80)
  1879. .margin({ left: 10 })
  1880. }
  1881. .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.7})
  1882. .onClick(() => {
  1883. // if (!this.isLogin) {
  1884. this.mType = 1
  1885. if (!this.isBigScreen()) {
  1886. this.getUIContext()?.animateTo({ duration: 555 }, () => {
  1887. this.isShowDrawer = false
  1888. })
  1889. // }
  1890. }
  1891. })
  1892. .width('100%')
  1893. .padding(10)
  1894. PointLightDefaultButton({
  1895. isPx: false,
  1896. isSysBol: true,
  1897. pointColor: $r('app.color.text_color'),
  1898. imageResource: $r('sys.symbol.gearshape'),
  1899. builderHeight: 32,
  1900. builderWidth: 32,
  1901. buttonScale: 1,
  1902. canShadow: false,
  1903. })
  1904. .margin({ right: 2, bottom: 2 })
  1905. .onClick(() => {
  1906. this.mType = 4
  1907. if (!this.isBigScreen()) {
  1908. this.getUIContext()?.animateTo({ duration: 555 }, () => {
  1909. this.isShowDrawer = false
  1910. })
  1911. }
  1912. })
  1913. }
  1914. .backgroundColor($r('app.color.user_center_card_background'))
  1915. .borderRadius(24)
  1916. .width('86%')
  1917. .padding(8)
  1918. .margin({ left: 10, right: 10, top: this.topRectHeight + 10,bottom:10 })
  1919. .shadow({
  1920. radius: 8,
  1921. color: 0x11000000,
  1922. offsetX: 0,
  1923. offsetY: 2
  1924. })
  1925. }
  1926. @Builder
  1927. TimeTextBuilder() {
  1928. Text(Utility.getTimePeriod())
  1929. .fontSize(13)
  1930. .margin({ top: 5 })
  1931. .fontColor($r('app.color.text_color'))
  1932. .align(Alignment.Start)
  1933. }
  1934. @State ratioL: number = 1;
  1935. @State isRefreshing: boolean = false;
  1936. @State maxRefreshingHeight: number = 100.0;
  1937. private contentNode?: ComponentContent<Object> = undefined;
  1938. /**
  1939. * 动态生成抽屉菜单内容
  1940. * 根据 isShowSponsorship 展示不同菜单项
  1941. */
  1942. @Builder
  1943. getDrawerView() {
  1944. Refresh({ refreshing: $$this.isRefreshing, refreshingContent: this.contentNode }) {
  1945. List({ space: 0, scroller: this.scroller }) {
  1946. ListItemGroup({ header: this.buildUserInfoCard() }) {
  1947. if(this.tabSelectedIndexes[0]==0){//分类
  1948. this.buildTabCate()
  1949. }else if(this.tabSelectedIndexes[0]==1){//歌单
  1950. this.buildPlaylistTab()
  1951. }else if(this.tabSelectedIndexes[0]==2){//网盘
  1952. this.buildCloudStorageTab()
  1953. }
  1954. }
  1955. }
  1956. .scrollBar(BarState.Off)
  1957. .width('86%')
  1958. .borderRadius(20)
  1959. .margin({
  1960. bottom: 20,
  1961. left: 10,
  1962. right: 10,
  1963. top: 10
  1964. })
  1965. .alignListItem(ListItemAlign.Center)
  1966. .layoutWeight(1)
  1967. .backgroundColor($r('app.color.left_draw_bg'))
  1968. .edgeEffect(EdgeEffect.None) // 必须设置列表为滑动到边缘无效果
  1969. }
  1970. .pullDownRatio(this.ratioL)
  1971. .pullToRefresh(true)
  1972. .refreshOffset(0)
  1973. .onOffsetChange((offset: number) => {
  1974. // 越接近最大距离,下拉跟手系数越小。
  1975. this.ratioL = 1 - Math.pow((offset / this.maxRefreshingHeight), 3);
  1976. })
  1977. .onStateChange((refreshStatus: RefreshStatus) => {
  1978. console.info('onecold Refresh onStatueChange state is ' + refreshStatus);
  1979. })
  1980. .onRefreshing(async () => {
  1981. if(this.tabSelectedIndexes[0]==0){//分类
  1982. this.isRefreshing = false
  1983. }else if(this.tabSelectedIndexes[0]==1){//歌单
  1984. this.loadPlaylistList()
  1985. }else if(this.tabSelectedIndexes[0]==2){//网盘
  1986. this.loadWebDavAccounts()
  1987. }
  1988. })
  1989. }
  1990. @Builder
  1991. buildTabCate() {
  1992. ForEach(mainViewModel.getDrawerData(), (item: ItemData, index: number) => {
  1993. ListItem() {
  1994. Button({ type: ButtonType.Capsule, stateEffect: true }) {
  1995. Row() {
  1996. // 菜单图标
  1997. if (typeof item.img === 'object' && item.img !== null && (item.img as sysResource).type === 'symbol') {
  1998. SymbolGlyph((item.img as sysResource).value as Resource)// .size({ width: 22, height: 22 })
  1999. .fontSize(22)
  2000. .fontColor([this.themeColor])
  2001. .effectStrategy(SymbolEffectStrategy.HIERARCHICAL)
  2002. .alignSelf(ItemAlign.Center)
  2003. .margin({ left: 15 })
  2004. } else {
  2005. Image(item.img as Resource)
  2006. .height(22)
  2007. .alignSelf(ItemAlign.Center)
  2008. .fillColor(this.themeColor)
  2009. .margin({ left: 15 })
  2010. }
  2011. // 菜单标题
  2012. Text(item.title)
  2013. .margin({ left: 10, right: 20 })
  2014. .fontSize(15)
  2015. .fontColor(this.isTextSelected(index) ? this.themeColor : $r('app.color.index_tab_font_color'))
  2016. .fontWeight(480)
  2017. Blank()
  2018. // 右侧箭头
  2019. Image($r('app.media.arrow_right'))
  2020. .width(22)
  2021. .height(22)
  2022. .margin({ left: 20, right: 0 })
  2023. .align(Alignment.Center)
  2024. }
  2025. .width('100%')
  2026. .height(55)
  2027. }
  2028. .backgroundColor(Color.Transparent)
  2029. .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 })
  2030. .onClick(async () => {
  2031. // 根据 item.id 跳转或切换功能
  2032. switch (item.id) {
  2033. case MainViewModel.MENU_MUSIC:
  2034. this.mType = 0
  2035. this.modeType = 0
  2036. this.currentSongListID = ''
  2037. this.doShowDrawer()
  2038. break
  2039. case MainViewModel.MENU_FIND:
  2040. this.enterFindPage()
  2041. this.currentSongListID = ''
  2042. this.doShowDrawer()
  2043. break
  2044. case MainViewModel.MENU_FILE_SCAN:
  2045. this.mType = 2
  2046. this.doShowDrawer()
  2047. break
  2048. case MainViewModel.MENU_CHARTS:
  2049. this.mType = 3
  2050. this.modeType = 0
  2051. this.doShowDrawer()
  2052. break
  2053. case MainViewModel.MENU_MIEDIA_KU:
  2054. this.modeType = 1
  2055. this.mType = 0
  2056. this.currentSongListID = ''
  2057. this.doShowDrawer()
  2058. break
  2059. case MainViewModel.MENU_MIEDIA_ARTIST:
  2060. this.modeType = 2
  2061. this.mType = 0
  2062. this.currentSongListID = ''
  2063. this.doShowDrawer()
  2064. break
  2065. case MainViewModel.MENU_MIEDIA_ALBUM:
  2066. this.modeType = 3
  2067. this.mType = 0
  2068. this.currentSongListID = ''
  2069. this.doShowDrawer()
  2070. break
  2071. case MainViewModel.MENU_SETTING:
  2072. this.mType = 4
  2073. this.doShowDrawer()
  2074. break
  2075. case MainViewModel.MENU_VIP:
  2076. router.pushUrl({
  2077. url: 'pages/VipPage'
  2078. }, router.RouterMode.Single);
  2079. break
  2080. case MainViewModel.MENU_USER:
  2081. this.mType = 1
  2082. this.doShowDrawer()
  2083. break
  2084. case MainViewModel.MENU_NET_CONNECT:
  2085. this.mType = 1
  2086. this.doShowDrawer()
  2087. break
  2088. case MainViewModel.MENU_SIMI:
  2089. router.pushUrl({
  2090. url: 'pages/VerifyPage'
  2091. });
  2092. break
  2093. case MainViewModel.MENU_DUTY:
  2094. router.pushUrl({
  2095. url: 'pages/WebIndex',
  2096. params: { titleName: '用户协议', webUrl: CommonConstants.NEW_DUTY }
  2097. });
  2098. break
  2099. case MainViewModel.MENU_HAOPING:
  2100. Utility.gotoMarket(getContext(this) as common.UIAbilityContext, this.bundleName)
  2101. break
  2102. case MainViewModel.MENU_ABOUT:
  2103. this.mType = 5
  2104. this.doShowDrawer()
  2105. break
  2106. case MainViewModel.MENU_UPDATE:
  2107. AlertDialog.show({
  2108. title: '版本更新',
  2109. message: '当前版本为最新版本:' + this.versionName,
  2110. autoCancel: true,
  2111. alignment: DialogAlignment.Center,
  2112. offset: { dx: 0, dy: -20 }, //在Y轴方向上的编译量
  2113. confirm: {
  2114. value: '确定',
  2115. fontColor: Color.White,
  2116. backgroundColor: $r('app.color.title_bar_bg'),
  2117. action: () => {
  2118. }
  2119. }
  2120. })
  2121. break
  2122. case MainViewModel.MENU_YSZC:
  2123. router.pushUrl({
  2124. url: 'pages/WebIndex',
  2125. params: { titleName: '隐私政策', webUrl: CommonConstants.NEW_YS_HW }
  2126. });
  2127. break
  2128. case MainViewModel.MENU_SHARE:
  2129. gotoShare(this.context,this.appName,this.bundleName)
  2130. break
  2131. }
  2132. })
  2133. }
  2134. .transition(TransitionEffect.move(TransitionEdge.BOTTOM)
  2135. .animation({ duration: 600, curve: Curve.Ease, delay: 60 * index }))
  2136. .width('90%')
  2137. .height(55)
  2138. })
  2139. }
  2140. isTextSelected(index:number):boolean{
  2141. if(this.mType == 0){
  2142. return this.modeType == index
  2143. }
  2144. if (this.mType == 8) {
  2145. return index == 4
  2146. }
  2147. if (this.mType == 2) {
  2148. return index == 5
  2149. }
  2150. if (this.mType == 3) {
  2151. return index == 6
  2152. }
  2153. if (this.mType == 5) {
  2154. return index == 7
  2155. }
  2156. return false
  2157. }
  2158. doShowDrawer(){
  2159. if (!this.isBigScreen()) {
  2160. this.getUIContext()?.animateTo({ duration: 555 }, () => {
  2161. this.isShowDrawer = false
  2162. })
  2163. }
  2164. }
  2165. @Builder
  2166. updateSheet() {
  2167. Scroll() {
  2168. Column() {
  2169. OnlineUpdateLog()
  2170. }
  2171. }
  2172. .width('100%')
  2173. .height('100%')
  2174. }
  2175. // /**
  2176. // * ================== 穿山甲广告相关 ==================
  2177. // */
  2178. // /** Banner广告对象 */
  2179. // private declare bannerAd: CSJNativeExpressAd;
  2180. // /** 广告加载状态 */
  2181. // @State private status: string = "未加载"
  2182. // /** 是否展示广告 */
  2183. // @State private isShowAd: boolean = false
  2184. // /** 广告位配置信息 */
  2185. // private declare mAdSlot: AdSlot;
  2186. // /** 服务端bidding广告内容(可选) */
  2187. // private mBiddingAdm = ''
  2188. // /** 广告加载监听器 */
  2189. // private expressLoadAdListener: NativeExpressAdListener = {
  2190. // /**
  2191. // * 广告加载失败回调
  2192. // */
  2193. // onError: (code: number, message: string) => {
  2194. // console.error("加载广告失败,code=" + code + ",message=" + message);
  2195. // this.status = "加载广告失败,code=" + code + ",message=" + message;
  2196. // },
  2197. // /**
  2198. // * 广告加载成功回调
  2199. // * @param ads 返回的广告列表
  2200. // */
  2201. // onNativeExpressAdLoad: (ads: ArrayList<CSJNativeExpressAd>) => {
  2202. // console.log("BannerExpressAdPage==onNativeExpressAdLoad......success");
  2203. // if (ads && ads.length > 0) {
  2204. // ads.forEach((ad: CSJNativeExpressAd, idx: number) => {
  2205. // // 设置广告交互监听
  2206. // ad.setExpressInteractionListener({
  2207. // /** 广告点击回调 */
  2208. // onAdClicked: (type: number) => {
  2209. // console.log("BannerExpressAdPage==onAdClicked......");
  2210. // },
  2211. // /** 广告展示回调 */
  2212. // onAdShow: (type: number) => {
  2213. // console.log("BannerExpressAdPage==onAdShow......");
  2214. // },
  2215. // /** 模板渲染失败回调 */
  2216. // onRenderFail: (code: number, msg: string) => {
  2217. // console.log("BannerExpressAdPage==onRenderFail...code=" + code + ",msg=" + msg);
  2218. // },
  2219. // /** 模板渲染成功回调 */
  2220. // onRenderSuccess: (width: number, height: number) => {
  2221. // console.log("BannerExpressAdPage==onRenderSuccess......width=" + width + ",height=" + height);
  2222. // this.bannerAd = ad;
  2223. // this.status = "广告加载完成待展示";
  2224. // this.showBannerAd()
  2225. // }
  2226. // })
  2227. // // 设置广告轮播时间
  2228. // ad.setSlideIntervalTime(30 * 1000);//设置轮播时间长
  2229. // this.setDislikeCallback(ad); //设置dislike
  2230. // ad.render(this.getUIContext()) // 开始渲染广告
  2231. // this.status = "广告加载中...";
  2232. // });
  2233. // }
  2234. // }
  2235. // }
  2236. //
  2237. // /**
  2238. // * 设置广告 dislike 回调
  2239. // * @param ad 广告对象
  2240. // */
  2241. // private setDislikeCallback(ad: CSJNativeExpressAd) {
  2242. // ad.setDislikeCallback({
  2243. // /** dislike 弹窗显示 */
  2244. // onShow: () => {
  2245. // console.log("BannerExpressAdPage==dislike......show");
  2246. // },
  2247. // /** 选择 dislike 选项 */
  2248. // onSelected: (position: number, value: string, enforceRemove: boolean) => {
  2249. // this.isShowAd = false;
  2250. // console.log("BannerExpressAdPage==dislike......onSelected:position=" + position + ",value:" + value + ",enforce=" + enforceRemove);
  2251. // },
  2252. // /** 点击取消 dislike */
  2253. // onCancel: () => {
  2254. // console.log("BannerExpressAdPage==dislike......onCancel");
  2255. // }
  2256. // })
  2257. // }
  2258. //
  2259. // /**
  2260. // * 加载Banner广告
  2261. // * @param rit 广告位ID
  2262. // */
  2263. // loadBannerAd(rit: string) {
  2264. // // 仅在满足条件时加载广告
  2265. // if(!Utility.isNoble()){
  2266. // return
  2267. // }
  2268. // if(!Utility.isPassInstallTime(2)){
  2269. // return
  2270. // }
  2271. // this.isShowAd = false;
  2272. // let adCreator:CSJAdCreator = CSJAdSdk.getAdCreator()
  2273. // this.mAdSlot = new AdSlotBuilder()
  2274. // .setCodeId(rit)
  2275. // .setAcceptSize(CSJUtil.BANNER_WIGTH, CSJUtil.BANNER_HEIGHT)
  2276. // .setAdCount(1)
  2277. // .setBidAdm(this.mBiddingAdm)
  2278. // .build()
  2279. // PrintBiddingTokenUtils.printBiddingToken(this.mAdSlot, adCreator);
  2280. // adCreator.loadBannerAd(this.mAdSlot, this.expressLoadAdListener)
  2281. // this.status = "广告加载中..."
  2282. // }
  2283. @StorageProp('currentHeightBreakpoint') currentHeightBreakpoint: HeightBreakpoint | undefined =
  2284. HeightBreakpoint.HEIGHT_LG;
  2285. @StorageProp('currentWidthBreakpoint') currentWidthBreakpoint: WidthBreakpoint | undefined = WidthBreakpoint.WIDTH_SM;
  2286. @StorageProp('topRectHeight') topRectHeight: number = px2vp(AppUtil.getStatusBarHeight());
  2287. //是否是Pura X外屏,或者小屏幕
  2288. isPuraWP() {
  2289. return this.currentWidthBreakpoint === WidthBreakpoint.WIDTH_SM
  2290. && this.currentHeightBreakpoint === HeightBreakpoint.HEIGHT_MD
  2291. }
  2292. //是否是Pura X外屏,或者小屏幕或者手机横屏
  2293. isCoverOpacity() {
  2294. if (this.isPuraWP()) {
  2295. return true
  2296. }
  2297. if (this.isPhoneLan()) {
  2298. return true
  2299. }
  2300. if (deviceInfo.marketName.includes('Pura X')
  2301. && this.currentHeightBreakpoint === HeightBreakpoint.HEIGHT_SM) {
  2302. return true
  2303. }
  2304. return false
  2305. }
  2306. //是不是手机横屏
  2307. isPhoneLan() {
  2308. if (DeviceUtil.getDeviceType() === resourceManager.DeviceType.DEVICE_TYPE_PHONE
  2309. && this.currentBreakpoint !== BreakpointTypeEnum.SM) {
  2310. if (DisplayUtil.getFoldStatus() === 0) {
  2311. return true
  2312. }
  2313. }
  2314. return false
  2315. }
  2316. isLoginChange() {
  2317. this.refreshUserInfoState();
  2318. }
  2319. /**
  2320. * 构建歌单tab内容
  2321. */
  2322. @Builder
  2323. buildPlaylistTab() {
  2324. // 创建歌单按钮
  2325. ListItem() {
  2326. Button({ type: ButtonType.Capsule, stateEffect: true }) {
  2327. Row() {
  2328. SymbolGlyph($r('sys.symbol.plus_circle'))
  2329. .fontSize(22)
  2330. .fontColor([this.themeColor])
  2331. .effectStrategy(SymbolEffectStrategy.HIERARCHICAL)
  2332. .alignSelf(ItemAlign.Center)
  2333. .margin({ left: 25 })
  2334. Text('创建歌单')
  2335. .margin({ left: 10, right: 20 })
  2336. .fontSize(14)
  2337. .fontColor($r('app.color.index_tab_font_color'))
  2338. .fontWeight(480)
  2339. Blank()
  2340. Image($r('app.media.arrow_right'))
  2341. .width(22)
  2342. .height(22)
  2343. .margin({ left: 20, right: 0 })
  2344. .align(Alignment.Center)
  2345. }
  2346. .width('100%')
  2347. .height(55)
  2348. }
  2349. .backgroundColor(Color.Transparent)
  2350. .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 })
  2351. .onClick(() => {
  2352. this.showCreatePlaylistDialog()
  2353. })
  2354. }
  2355. // 歌单列表
  2356. ForEach(this.playlistList, (playlist: Playlist,index:number) => {
  2357. ListItem() {
  2358. Button({ type: ButtonType.Capsule, stateEffect: true }) {
  2359. Row() {
  2360. Image(playlist.coverPath || $r('app.media.hm_playlist'))
  2361. .width(22)
  2362. .height(22)
  2363. .margin({ left: 25 })
  2364. .borderRadius(4)
  2365. .fillColor(this.themeColor)
  2366. .clip(true)
  2367. Column() {
  2368. Text(playlist.name)
  2369. .margin({ left: 10, right: 20 })
  2370. .fontSize(15)
  2371. .fontColor($r('app.color.index_tab_font_color'))
  2372. .fontWeight(480)
  2373. .maxLines(1)
  2374. .textOverflow({ overflow: TextOverflow.Ellipsis })
  2375. Text(`${playlist.songCount}首`)
  2376. .margin({ left: 10, right: 20 })
  2377. .fontSize(12)
  2378. .fontColor($r('app.color.index_tab_font_color'))
  2379. .opacity(0.7)
  2380. }
  2381. .alignItems(HorizontalAlign.Start)
  2382. Blank()
  2383. Image($r('app.media.arrow_right'))
  2384. .width(22)
  2385. .height(22)
  2386. .margin({ left: 20, right: 0 })
  2387. .align(Alignment.Center)
  2388. }
  2389. .width('100%')
  2390. .height(55)
  2391. }
  2392. .backgroundColor(Color.Transparent)
  2393. .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 })
  2394. .onClick(async () => {
  2395. let targetPlaylist: Playlist = playlist;
  2396. try {
  2397. if (this.playlistTable) {
  2398. const latestPlaylist = await this.playlistTable.queryPlaylistById(playlist.id);
  2399. if (latestPlaylist) {
  2400. targetPlaylist = latestPlaylist;
  2401. const targetIndex = this.playlistList.findIndex((item: Playlist) => item.id === latestPlaylist.id);
  2402. if (targetIndex >= 0) {
  2403. this.playlistList[targetIndex] = latestPlaylist;
  2404. this.playlistList = [...this.playlistList];
  2405. }
  2406. }
  2407. }
  2408. } catch (error) {
  2409. LogUtil.warn('heanup NewIndex', `点击歌单时刷新最新数据失败: ${(error as Error).message}`);
  2410. }
  2411. if(targetPlaylist.songCount==0){
  2412. this.openPlaylist(targetPlaylist)
  2413. }else{
  2414. this.doShowDrawer();
  2415. this.mType = 0
  2416. this.modeType = 4
  2417. this.currentSongListName = targetPlaylist.name;
  2418. this.currentSongListID = targetPlaylist.id;
  2419. }
  2420. })
  2421. .bindContextMenu(this.MenuBuilder(playlist), ResponseType.LongPress,
  2422. {
  2423. preview: MenuPreviewMode.IMAGE,
  2424. previewAnimationOptions: { scale: [0.8, 1.0] },
  2425. })
  2426. .bindContextMenu(this.MenuBuilder(playlist), ResponseType.RightClick,
  2427. {
  2428. preview: MenuPreviewMode.IMAGE,
  2429. previewAnimationOptions: { scale: [0.8, 1.0] },
  2430. })
  2431. }
  2432. .transition(TransitionEffect.move(TransitionEdge.BOTTOM)
  2433. .animation({ duration: 600, curve: Curve.Ease, delay: 60 * index }))
  2434. })
  2435. }
  2436. /**
  2437. * 构建网盘tab内容 - 从数据库获取所有网盘账户
  2438. * 支持多种类型的网盘账户(目前支持WebDAV,将来可扩展其他类型)
  2439. */
  2440. @Builder
  2441. buildCloudStorageTab() {
  2442. // 当前只支持网盘账户,将来可以在这里添加其他类型的网盘账户
  2443. // 例如:OneDrive, Google Drive, Dropbox等
  2444. // 添加新账户按钮
  2445. ListItem() {
  2446. Button({ type: ButtonType.Capsule, stateEffect: true }) {
  2447. Row() {
  2448. SymbolGlyph($r('sys.symbol.plus_circle'))
  2449. .fontSize(22)
  2450. .fontColor([this.themeColor])
  2451. .effectStrategy(SymbolEffectStrategy.HIERARCHICAL)
  2452. .alignSelf(ItemAlign.Center)
  2453. .margin({ left: 25 })
  2454. Text(`添加${getRemoteDriveAccountLabel()}`)
  2455. .margin({ left: 10, right: 20 })
  2456. .fontSize(14)
  2457. .fontColor($r('app.color.index_tab_font_color'))
  2458. .fontWeight(480)
  2459. Blank()
  2460. Image($r('app.media.arrow_right'))
  2461. .width(22)
  2462. .height(22)
  2463. .margin({ left: 20, right: 0 })
  2464. .align(Alignment.Center)
  2465. }
  2466. .width('100%')
  2467. .height(55)
  2468. }
  2469. .backgroundColor(Color.Transparent)
  2470. .bindMenu(this.AddMenuBuilder)
  2471. .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 })
  2472. }
  2473. // 显示所有网盘账户
  2474. ForEach(this.webDavAccounts, (account: WebDavAccount,index:number) => {
  2475. ListItem() {
  2476. Button({ type: ButtonType.Capsule, stateEffect: true }) {
  2477. Row() {
  2478. // 账户封面或默认图标
  2479. Stack() {
  2480. Image(account.coverPath?account.coverPath:getCloudDiskIcon(account.webType))
  2481. .width(25)
  2482. .height(25)
  2483. .borderRadius(4)
  2484. .objectFit(ImageFit.Cover)
  2485. }
  2486. .margin({ left: 20 })
  2487. Column() {
  2488. // 账户名称
  2489. Text(account.name)
  2490. .margin({ left: 8, right: 20 })
  2491. .fontSize(15)
  2492. .fontColor(this.selectedAccount==account?this.themeColor:$r('app.color.text_color'))
  2493. .fontWeight(480)
  2494. .maxLines(1)
  2495. .textOverflow({ overflow: TextOverflow.Ellipsis })
  2496. Row() {
  2497. // 账户类型标签
  2498. Text(getRemoteDriveDisplayLabel(account.webType))
  2499. .fontSize(10)
  2500. .fontColor(this.selectedAccount==account?this.themeColor:$r('app.color.index_tab_font_color'))
  2501. .opacity(0.8)
  2502. .padding({ left: 8, top: 2, bottom: 2 })
  2503. .borderRadius(3)
  2504. // 服务器地址
  2505. Text(`@${account.isUseLocalHost ? account.localHost : account.host}:${account.port}`)
  2506. .fontSize(12)
  2507. .fontColor(this.selectedAccount==account?this.themeColor:$r('app.color.index_tab_font_color'))
  2508. .opacity(0.7)
  2509. .maxLines(1)
  2510. .padding({ right: 8, top: 2, bottom: 2 })
  2511. .textOverflow({ overflow: TextOverflow.Ellipsis })
  2512. }
  2513. }
  2514. .alignItems(HorizontalAlign.Start)
  2515. Blank()
  2516. }
  2517. .width('100%')
  2518. .height(60)
  2519. }
  2520. .backgroundColor(Color.Transparent)
  2521. .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.8 })
  2522. .onClick(() => {
  2523. console.info('heanup', '点击网盘账户:', account.name)
  2524. this.selectWebDavAccount(account)
  2525. })
  2526. .bindContextMenu(this.MenuDavBuilder(account), ResponseType.LongPress,
  2527. {
  2528. preview: MenuPreviewMode.IMAGE,
  2529. previewAnimationOptions: { scale: [0.8, 1.0] },
  2530. })
  2531. .bindContextMenu(this.MenuDavBuilder(account), ResponseType.RightClick,
  2532. {
  2533. preview: MenuPreviewMode.IMAGE,
  2534. previewAnimationOptions: { scale: [0.8, 1.0] },
  2535. })
  2536. }
  2537. .transition(TransitionEffect.move(TransitionEdge.BOTTOM)
  2538. .animation({ duration: 600, curve: Curve.Ease, delay: 60 * index }))
  2539. })
  2540. // 如果没有账户,显示提示信息
  2541. if (this.webDavAccounts.length === 0) {
  2542. ListItem() {
  2543. Text(`暂无${getRemoteDriveAccountLabel()},点击上方按钮添加`)
  2544. .fontSize(14)
  2545. .fontColor($r('app.color.index_tab_font_color'))
  2546. .opacity(0.6)
  2547. .textAlign(TextAlign.Center)
  2548. .width('100%')
  2549. .padding(20)
  2550. }
  2551. }
  2552. }
  2553. @Builder
  2554. AddMenuBuilder() {
  2555. Menu() {
  2556. MenuItem({
  2557. startIcon: $r('app.media.webdav'),
  2558. content: $r('app.string.webdav')
  2559. })
  2560. .onClick(async () => {
  2561. this.showRemoteDriveAccountDialog(false,undefined,RemoteDriveType.WebDav)
  2562. })
  2563. MenuItem({
  2564. startIcon: $r('app.media.smb'),
  2565. content: $r('app.string.smb')
  2566. })
  2567. .onClick(async () => {
  2568. this.showRemoteDriveAccountDialog(false,undefined,RemoteDriveType.Smb)
  2569. })
  2570. MenuItem({
  2571. startIcon: $r('app.media.ftp'),
  2572. content: $r('app.string.ftp')
  2573. })
  2574. .onClick(async () => {
  2575. this.showRemoteDriveAccountDialog(false,undefined,RemoteDriveType.Ftp)
  2576. })
  2577. MenuItem({
  2578. startIcon: $r('app.media.navidrome'),
  2579. content: $r('app.string.navidrome')
  2580. })
  2581. .onClick(async () => {
  2582. this.showRemoteDriveAccountDialog(false,undefined,RemoteDriveType.Navidrome)
  2583. })
  2584. MenuItem({
  2585. startIcon: $r('app.media.jellyfin'),
  2586. content: 'Jellyfin'
  2587. })
  2588. .onClick(async () => {
  2589. this.showRemoteDriveAccountDialog(false, undefined, RemoteDriveType.Jellyfin)
  2590. })
  2591. MenuItem({
  2592. startIcon: $r('app.media.emby'),
  2593. content: $r('app.string.emby')
  2594. })
  2595. .onClick(async () => {
  2596. this.showRemoteDriveAccountDialog(false, undefined, RemoteDriveType.Emby)
  2597. })
  2598. MenuItem({
  2599. startIcon: $r('app.media.audio_Station'),
  2600. content: 'AudioStation'
  2601. })
  2602. .onClick(async () => {
  2603. this.showRemoteDriveAccountDialog(false, undefined, RemoteDriveType.AudioStation)
  2604. })
  2605. MenuItem({
  2606. startIcon: $r('app.media.plex'),
  2607. content: 'Plex'
  2608. })
  2609. .onClick(async () => {
  2610. this.showRemoteDriveAccountDialog(false, undefined, RemoteDriveType.Plex)
  2611. })
  2612. MenuItem({
  2613. startIcon: $r('app.media.daoliyu'),
  2614. content: '道理鱼'
  2615. })
  2616. .onClick(async () => {
  2617. this.showRemoteDriveAccountDialog(false, undefined, RemoteDriveType.DaoLiYu)
  2618. })
  2619. MenuItem({
  2620. startIcon: $r('app.media.baiduwp'),
  2621. content: '百度网盘'
  2622. })
  2623. .onClick(async () => {
  2624. this.showRemoteDriveAccountDialog(false, undefined, RemoteDriveType.Baidu)
  2625. })
  2626. }.attributeModifier(new MenuModifier())
  2627. }
  2628. @Builder
  2629. MenuBuilder(playlist: Playlist) {
  2630. Menu(){
  2631. MenuItem({
  2632. symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.square_and_pencil')),
  2633. content: '编辑歌单'
  2634. })
  2635. .onClick(async() => {
  2636. this.openPlaylist(playlist)
  2637. })
  2638. MenuItem({
  2639. symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.trash')),
  2640. content: '删除歌单'
  2641. })
  2642. .onClick(async() => {
  2643. this.deletePlaylist(playlist)
  2644. })
  2645. }
  2646. }
  2647. @Builder
  2648. MenuDavBuilder(account: WebDavAccount) {
  2649. Menu(){
  2650. MenuItem({
  2651. symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.square_and_pencil')),
  2652. content: '编辑'
  2653. })
  2654. .onClick(async() => {
  2655. this.showRemoteDriveAccountDialog(true,account)
  2656. })
  2657. MenuItem({
  2658. symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.arrow_up_to_line')),
  2659. content: '置顶'
  2660. })
  2661. .onClick(async() => {
  2662. try {
  2663. await this.webdavManager.pinAccount(account)
  2664. // 重新加载网盘账户列表以更新UI排序
  2665. await this.loadWebDavAccounts()
  2666. ToastUtil.showToast('置顶成功')
  2667. LogUtil.info('heanup NewIndex', '网盘账户置顶成功:', account.name)
  2668. this.logAccount('info', `网盘账户置顶成功: ${account.name}`)
  2669. } catch (error) {
  2670. LogUtil.error('heanup NewIndex', `置顶网盘账户失败: ${(error as Error).message}`)
  2671. ToastUtil.showToast('置顶失败')
  2672. this.logAccount('error', `置顶网盘账户失败: ${(error as Error).message}`)
  2673. }
  2674. })
  2675. MenuItem({
  2676. symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.trash')),
  2677. content: '删除'
  2678. })
  2679. .onClick(async() => {
  2680. this.deleteWebDavAccount(account)
  2681. })
  2682. }
  2683. }
  2684. /**
  2685. * 删除网盘账户
  2686. */
  2687. async deleteWebDavAccount(account: WebDavAccount) {
  2688. try {
  2689. // 显示确认对话框
  2690. this.getUIContext().showAlertDialog({
  2691. title: '删除账户',
  2692. message: `确定要删除webdav账户"${account.name}"吗?`,
  2693. primaryButton: {
  2694. value: '取消',
  2695. action: () => {}
  2696. },
  2697. secondaryButton: {
  2698. value: '删除',
  2699. fontColor: Color.Red,
  2700. action: async () => {
  2701. try {
  2702. await this.webdavManager.removeAccount(account)
  2703. ToastUtil.showToast('网盘账户删除成功')
  2704. // 重新加载网盘账户列表
  2705. await this.loadWebDavAccounts()
  2706. LogUtil.info('heanup NewIndex', '网盘账户删除成功:', account.name)
  2707. this.logAccount('info', `网盘账户删除成功: ${account.name}`)
  2708. } catch (error) {
  2709. LogUtil.error('heanup NewIndex', `删除网盘账户失败: ${(error as Error).message}`)
  2710. ToastUtil.showToast('删除失败')
  2711. this.logAccount('error', `删除网盘账户失败: ${(error as Error).message}`)
  2712. }
  2713. }
  2714. }
  2715. })
  2716. } catch (error) {
  2717. LogUtil.error('heanup NewIndex', `删除网盘账户操作失败: ${(error as Error).message}`)
  2718. ToastUtil.showToast('操作失败')
  2719. this.logAccount('error', `删除网盘账户操作失败: ${(error as Error).message}`)
  2720. }
  2721. }
  2722. /**
  2723. * 删除歌单
  2724. */
  2725. async deletePlaylist(playlist: Playlist) {
  2726. if (playlist&&this.playlistTable) {
  2727. // 显示确认对话框
  2728. AlertDialog.show({
  2729. title: '删除歌单',
  2730. message: `确定要删除歌单"${playlist.name}"吗?此操作不可撤销。`,
  2731. primaryButton: {
  2732. value: '取消',
  2733. action: () => {}
  2734. },
  2735. secondaryButton: {
  2736. value: '删除',
  2737. fontColor: Color.Red,
  2738. action: async () => {
  2739. if (this.playlistTable) {
  2740. const success = await this.playlistTable.deletePlaylist(playlist.id)
  2741. if (success) {
  2742. ToastUtil.showToast('歌单删除成功')
  2743. // 发送刷新事件
  2744. emitter.emit({ eventId: EventConstants.EVENT_PLAYLIST_REFRESH }, {})
  2745. } else {
  2746. ToastUtil.showToast('歌单删除失败')
  2747. }
  2748. }
  2749. }
  2750. }
  2751. })
  2752. }
  2753. }
  2754. /**
  2755. * 显示创建歌单对话框
  2756. */
  2757. showCreatePlaylistDialog() {
  2758. if (!this.playlistTable) {
  2759. ToastUtil.showToast('歌单功能初始化中,请稍后再试')
  2760. return
  2761. }
  2762. // 使用新的歌单对话框,支持自定义封面
  2763. showCreatePlaylistDialog(
  2764. async (name: string, description: string, coverPath?: string) => {
  2765. // 创建歌单
  2766. const success = await this.playlistTable!.createPlaylist(name, description, coverPath)
  2767. if (success) {
  2768. ToastUtil.showToast('歌单创建成功')
  2769. // 刷新歌单列表
  2770. await this.loadPlaylistList()
  2771. // 发送歌单刷新事件
  2772. emitter.emit({ eventId: EventConstants.EVENT_PLAYLIST_REFRESH }, {})
  2773. } else {
  2774. ToastUtil.showToast('歌单创建失败')
  2775. }
  2776. },
  2777. () => {
  2778. // 取消创建歌单
  2779. }
  2780. )
  2781. }
  2782. /**
  2783. * 打开歌单详情
  2784. */
  2785. openPlaylist(playlist: Playlist) {
  2786. try {
  2787. // 跳转到歌单详情页面
  2788. router.pushUrl({
  2789. url: 'pages/PlaylistDetailPage',
  2790. params: {
  2791. playlist: playlist
  2792. }
  2793. }).catch((err: Error) => {
  2794. console.error('跳转到歌单详情页面失败:', err.message);
  2795. ToastUtil.showToast('打开歌单失败');
  2796. });
  2797. } catch (error) {
  2798. console.error('打开歌单失败:', error);
  2799. ToastUtil.showToast('打开歌单失败');
  2800. }
  2801. }
  2802. /**
  2803. * 初始化歌单数据库
  2804. */
  2805. async initPlaylistTable() {
  2806. try {
  2807. this.playlistTable = new PlaylistTable(this.context)
  2808. console.info('onecold 歌单数据库初始化成功')
  2809. // 等待数据库初始化完成后再加载数据
  2810. setTimeout(async () => {
  2811. await this.loadPlaylistList()
  2812. }, 500) // 延迟500ms确保数据库初始化完成
  2813. } catch (error) {
  2814. console.error('初始化歌单数据库失败:', error)
  2815. }
  2816. }
  2817. /**
  2818. * 加载歌单列表
  2819. */
  2820. async loadPlaylistList() {
  2821. try {
  2822. LogUtil.info('heanup NewIndex', '开始加载歌单列表')
  2823. if (this.playlistTable) {
  2824. const playlists = await this.playlistTable.queryAllPlaylists()
  2825. // 强制触发UI更新
  2826. this.playlistList = playlists.slice()
  2827. LogUtil.info('heanup NewIndex', `成功加载 ${playlists.length} 个歌单`)
  2828. } else {
  2829. LogUtil.warn('heanup NewIndex', '歌单表未初始化')
  2830. }
  2831. this.isRefreshing = false
  2832. } catch (error) {
  2833. LogUtil.error('heanup NewIndex', `加载歌单列表失败: ${(error as Error).message}`)
  2834. this.isRefreshing = false
  2835. }
  2836. }
  2837. /**
  2838. * 初始化WebDAV管理器
  2839. */
  2840. private logAccount(level: 'info' | 'warn' | 'error', message: string) {
  2841. const tag = 'AccountCenter';
  2842. switch (level) {
  2843. case 'info':
  2844. void ServerLogUtil.info(tag, message);
  2845. break;
  2846. case 'warn':
  2847. void ServerLogUtil.warn(tag, message);
  2848. break;
  2849. case 'error':
  2850. void ServerLogUtil.error(tag, message);
  2851. break;
  2852. default:
  2853. break;
  2854. }
  2855. }
  2856. private logHiCar(level: 'info' | 'warn' | 'error', message: string) {
  2857. const tag = 'HiCarEntry';
  2858. switch (level) {
  2859. case 'info':
  2860. void ServerLogUtil.info(tag, message);
  2861. break;
  2862. case 'warn':
  2863. void ServerLogUtil.warn(tag, message);
  2864. break;
  2865. case 'error':
  2866. void ServerLogUtil.error(tag, message);
  2867. break;
  2868. default:
  2869. break;
  2870. }
  2871. }
  2872. async initWebDavManager() {
  2873. try {
  2874. // 设置WebDAV管理器的上下文
  2875. this.webdavManager.setContext(this.context)
  2876. // 创建网盘账户表
  2877. await this.webdavManager.createWebDavTableInDB()
  2878. // 订阅WebDAV管理器事件
  2879. this.webdavManager.subscribe((event: string) => {
  2880. LogUtil.info('heanup NewIndex', '收到WebDAV事件:', event)
  2881. this.logAccount('info', `收到WebDAV事件: ${event}`)
  2882. if (event === 'QueryAccountsSucceed') {
  2883. this.webDavAccounts = this.webdavManager.getAllWebDavAccounts().slice()
  2884. }
  2885. })
  2886. // 首页EntryAblility去加载网盘账户了。所以这边不加载
  2887. // await this.loadWebDavAccounts()
  2888. LogUtil.info('heanup NewIndex', 'WebDAV管理器初始化成功')
  2889. this.logAccount('info', 'WebDAV管理器初始化成功')
  2890. } catch (error) {
  2891. LogUtil.error('heanup NewIndex', `初始化WebDAV管理器失败: ${(error as Error).message}`)
  2892. this.logAccount('error', `初始化WebDAV管理器失败: ${(error as Error).message}`)
  2893. }
  2894. }
  2895. /**
  2896. * 加载网盘账户列表
  2897. */
  2898. async loadWebDavAccounts() {
  2899. try {
  2900. LogUtil.info('heanup NewIndex', '开始加载网盘账户列表')
  2901. this.logAccount('info', '开始加载网盘账户列表')
  2902. await this.webdavManager.queryWebDavAccountsFromDB()
  2903. // 强制触发UI更新
  2904. this.webDavAccounts = this.webdavManager.getAllWebDavAccounts().slice()
  2905. this.isRefreshing = false
  2906. LogUtil.info('heanup NewIndex', `成功加载 ${this.webDavAccounts.length} 个网盘账户`)
  2907. this.logAccount('info', `成功加载 ${this.webDavAccounts.length} 个网盘账户`)
  2908. } catch (error) {
  2909. this.isRefreshing = false
  2910. LogUtil.error('heanup NewIndex', `加载网盘账户列表失败: ${(error as Error).message}`)
  2911. this.logAccount('error', `加载网盘账户列表失败: ${(error as Error).message}`)
  2912. }
  2913. }
  2914. /**
  2915. * 选择网盘账户
  2916. */
  2917. selectWebDavAccount(account: WebDavAccount) {
  2918. try {
  2919. LogUtil.info('heanup NewIndex', '选择网盘账户:', account.name)
  2920. this.logAccount('info', `选择网盘账户: ${account.name} (id=${account.id ?? 'unknown'}, webType=${account.webType})`)
  2921. // 关键修复:立即设置 selectedAccount,确保后续使用的是正确的账户
  2922. this.selectedAccount = account
  2923. console.info('heanup selectWebDavAccount account.webType='+account.webType)
  2924. // 如果账户未激活,先激活它
  2925. if (!account.isActivate) {
  2926. // 先将所有账户设为未激活
  2927. this.webDavAccounts.forEach(acc => {
  2928. acc.isActivate = false
  2929. })
  2930. // 立即激活选中的账户(不等待数据库)
  2931. account.isActivate = true
  2932. // 异步更新数据库中的激活状态(不阻塞UI)
  2933. this.webdavManager.editAccount(account).then(() => {
  2934. LogUtil.info('heanup NewIndex', '网盘账户激活成功:', account.name)
  2935. this.logAccount('info', `网盘账户激活成功: ${account.name}`)
  2936. }).catch((error: Error) => {
  2937. LogUtil.error('heanup NewIndex', `编辑网盘账户失败: ${error.message}`)
  2938. // 不显示Toast,避免干扰用户
  2939. this.logAccount('error', `编辑网盘账户失败: ${error.message}`)
  2940. })
  2941. }
  2942. // 根据账户类型切换到对应页面
  2943. if (account.webType === RemoteDriveType.Navidrome
  2944. || account.webType === RemoteDriveType.Jellyfin
  2945. || account.webType === RemoteDriveType.Emby
  2946. || account.webType === RemoteDriveType.AudioStation
  2947. || account.webType === RemoteDriveType.Plex
  2948. || account.webType === RemoteDriveType.DaoLiYu) {
  2949. this.mType = 7
  2950. LogUtil.info('heanup NewIndex', `切换到 Navidrome/Jellyfin/Emby/AudioStation/Plex/道理鱼 页面,webType=${account.webType}`)
  2951. } else {
  2952. this.mType = 6
  2953. LogUtil.info('heanup NewIndex', `切换到 WebDAV 页面,webType=${account.webType}`)
  2954. }
  2955. this.doShowDrawer()
  2956. // 打印最终确认
  2957. LogUtil.info('heanup NewIndex', `选择完成: selectedAccount=${this.selectedAccount.name}, webType=${this.selectedAccount.webType}, mType=${this.mType}`)
  2958. } catch (error) {
  2959. LogUtil.error('heanup NewIndex', `选择网盘账户失败: ${(error as Error).message}`)
  2960. ToastUtil.showToast('选择账户失败')
  2961. this.logAccount('error', `选择网盘账户失败: ${(error as Error).message}`)
  2962. }
  2963. }
  2964. /**
  2965. * 显示添加网盘账户对话框
  2966. */
  2967. @State addDavDialogId:number = 1
  2968. showRemoteDriveAccountDialog(isEditMode?: boolean,account?: WebDavAccount,driveType?:number) {
  2969. const node: FrameNode | null = this.getUIContext().getFrameNodeById("test_text") || null;
  2970. this.getUIContext().getPromptAction().openCustomDialog({
  2971. builder: () => {
  2972. this.webDavAccountBuilder(isEditMode,account,driveType)
  2973. },
  2974. levelMode: LevelMode.EMBEDDED, // 启用页面级弹出框
  2975. levelUniqueId: node?.getUniqueId(), // 设置页面级弹出框所在页面的任意节点ID
  2976. immersiveMode: ImmersiveMode.EXTEND, // 设置页面级弹出框蒙层的显示模式
  2977. }).then((dialogId: number) => {
  2978. this.addDavDialogId = dialogId;
  2979. })
  2980. }
  2981. @Builder
  2982. webDavAccountBuilder(isEditMode?: boolean,account?: WebDavAccount,driveType?:number) {
  2983. RemoteDriveAccountDialog({
  2984. isEditMode: isEditMode,
  2985. account: account,
  2986. initialDriveType: driveType,
  2987. onCancel: () => {
  2988. // 取消添加
  2989. this.getUIContext().getPromptAction().closeCustomDialog(this.addDavDialogId)
  2990. },
  2991. onConfirm: (account: WebDavAccount) => {
  2992. if(this.webDavAccounts.length>=1&&!Utility.isNoble()&&!isEditMode){
  2993. ToastUtil.showToast('普通用户只能添加一个网盘账户,请开通会员')
  2994. return
  2995. }
  2996. this.getUIContext().getPromptAction().closeCustomDialog(this.addDavDialogId)
  2997. if (isEditMode && account?.id) {
  2998. // 编辑模式:更新现有账户
  2999. this.webdavManager.editAccount(account).then(() => {
  3000. ToastUtil.showToast('修改成功')
  3001. // 重新加载网盘账户列表,观察着模式去更新了,所以这里不需要更新,先注释掉
  3002. // this.loadWebDavAccounts()
  3003. LogUtil.info('heanup NewIndex', '网盘账户修改成功:', account.name)
  3004. this.logAccount('info', `网盘账户修改成功: ${account.name}`)
  3005. }).catch((error: Error) => {
  3006. LogUtil.error('heanup NewIndex', `修改网盘账户失败: ${error.message}`)
  3007. ToastUtil.showToast('修改失败')
  3008. this.logAccount('error', `修改网盘账户失败: ${error.message}`)
  3009. })
  3010. } else {
  3011. // 添加模式:创建新账户
  3012. this.webdavManager.insertAccount(
  3013. account.name,
  3014. account.host,
  3015. account.localHost,
  3016. account.isUseLocalHost,
  3017. account.port,
  3018. account.filepath,
  3019. account.lyricFilePath,
  3020. account.uploadFilePath,
  3021. account.imageFilePath,
  3022. account.account,
  3023. account.password,
  3024. account.enableHttps,
  3025. account.coverPath,
  3026. account.webType,
  3027. account.smbShare,
  3028. account.smbDomain,
  3029. account.navidromeBasePath,
  3030. account.jellyfinBasePath,
  3031. account.embyBasePath,
  3032. account.ftpEncoding,
  3033. account.baiduAccessToken,
  3034. account.baiduRefreshToken,
  3035. account.baiduTokenExpiresAt
  3036. ).then(() => {
  3037. ToastUtil.showToast('添加成功')
  3038. // 重新加载网盘账户列表 观察着模式去更新了,所以这里不需要更新,先注释掉
  3039. // this.loadWebDavAccounts()
  3040. LogUtil.info('heanup NewIndex', '网盘账户添加成功:', account.name)
  3041. this.logAccount('info', `网盘账户添加成功: ${account.name}`)
  3042. }).catch((error: Error) => {
  3043. LogUtil.error('heanup NewIndex', `添加网盘账户失败: ${error.message}`)
  3044. ToastUtil.showToast('添加失败')
  3045. this.logAccount('error', `添加网盘账户失败: ${error.message}`)
  3046. })
  3047. }
  3048. }
  3049. });
  3050. }
  3051. }
  3052. // 1. 工具函数:将 #RRGGBB 字符串和透明度转为 'rgba(r,g,b,a)' 字符串,深色模式下可返回深色变体
  3053. function themeColorWithAlpha(themeColor: string, alpha: number, isDarkMode: boolean = false): string {
  3054. if (isDarkMode) {
  3055. // 深色模式下返回更深的灰色或半透明黑色
  3056. return `rgba(34,34,34,${alpha + 0.2 > 1 ? 1 : alpha + 0.2})`;
  3057. }
  3058. const color = themeColor.replace('#', '');
  3059. const r = parseInt(color.substring(0, 2), 16);
  3060. const g = parseInt(color.substring(2, 4), 16);
  3061. const b = parseInt(color.substring(4, 6), 16);
  3062. return `rgba(${r},${g},${b},${alpha})`;
  3063. }
  3064. // 定义接口
  3065. interface HiCarAspectRatio {
  3066. context: Context;
  3067. playlistId: string;
  3068. }