diff --git a/README.en.md b/README.en.md index c29a178a85a238866173059bfaa6efc9d83da183..94d1e6ba2c9f84d838b38b9e0a847b8c7141ac38 100644 --- a/README.en.md +++ b/README.en.md @@ -1,9 +1,10 @@ # Music Album -### Introduction +## Project Introduction This codelab implements music album pages based on the adaptive layout and responsive layout, achieving one-time development for multi-device deployment. +## Effect Preview The figure shows the effect on the mobile phone. ![](screenshots/device/phone.en.png) @@ -16,7 +17,55 @@ The figure shows the effect on the tablet. ![](screenshots/device/tablet.en.png) -### How to Use +The figure shows the effect on the wearable. + + + + +## Engineering catalogue +``` +├──commons // Public Competency Layer +│ ├──constantsCommon/src/main/ets // Public Constants +│ │ └──constants +│ └──mediaCommon/src/main/ets // Public media approach +│ └──utils +│ └──viewmodel +├──features // Basic Feature Layer +│ ├──live/src/main/ets // Live Stream Page +│ │ ├──constants +│ │ ├──view +│ │ └──viewmodel +│ ├──live/src/main/resources // Resource file directory +│ ├──musicComment/src/main/ets // Music Review Page +│ │ ├──constants +│ │ ├──view +│ │ └──viewmodel +│ ├──musicComment/src/main/resources // Resource file directory +│ ├──musicList/src/main/ets // Song List Page +│ │ ├──components +│ │ ├──constants +│ │ ├──lyric +│ │ ├──view +│ │ └──viewmodel +│ └──musicList/src/main/resources // Resource file directory +└──products // Product Customization Layer + ├──phone/src/main/ets // Supports smartphones, foldable screens, tablets, and PCs/2in1 devices + │ ├──common + │ ├──entryability + │ ├──pages + │ ├──phonebackupextability + │ └──viewmodel + ├──phone/src/main/resources // Resource file directory + ├──watch/src/main/ets // Support for smart wearables + │ ├──constants + │ ├──pages + │ ├──view + │ ├──watchability + │ └──watchbackupability + └──watch/src/main/resources // Resource file directory +``` + +## How to Use 1. Install and open an app on a mobile phone, foldable phone, or tablet. The responsive layout and adaptive layout are used to display different effects on the app pages over different devices. 2. Touch the Play/Pause, Previous, or Next icon on the screen to control music playback. @@ -30,7 +79,7 @@ N/A ### Constraints -1. The sample is only supported on Huawei phones, tablets with standard systems. -2. HarmonyOS: HarmonyOS 5.0.5 Release or later. -3. DevEco Studio: DevEco Studio 5.0.5 Release or later. -4. HarmonyOS SDK: HarmonyOS 5.0.5 Release SDK or later. +1. The sample is only supported on Huawei phones, tablets and smart wearables with standard systems. +2. HarmonyOS: HarmonyOS 5.1.0 Release or later. +3. DevEco Studio: DevEco Studio 5.1.0 Release or later. +4. HarmonyOS SDK: HarmonyOS 5.1.0 Release SDK or later. diff --git a/README.md b/README.md index 74e5c1f713b9b2a53c915da4f1008cbece6fddad..8fea407484a1ff7fcadcb44b6cd9a788f6a47101 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ # 多设备音乐界面 -### 简介 +## 项目简介 基于自适应和响应式布局,实现一次开发、多端部署音乐专辑。 +## 效果预览 手机效果图如下: ![](screenshots/device/phone.png) @@ -15,22 +16,76 @@ 平板效果图如下: ![](screenshots/device/tablet.png) +![](screenshots/device/tablet.png) + +智能穿戴效果图如下: + + -### 使用说明 +## 工程目录 +``` +├──commons // 公共能力层 +│ ├──constantsCommon/src/main/ets // 公共常量 +│ │ └──constants +│ └──mediaCommon/src/main/ets // 公共媒体方法 +│ └──utils +│ └──viewmodel +├──features // 基础特性层 +│ ├──live/src/main/ets // 直播页 +│ │ ├──constants +│ │ ├──view +│ │ └──viewmodel +│ ├──live/src/main/resources // 资源文件目录 +│ ├──musicComment/src/main/ets // 音乐评论页 +│ │ ├──constants +│ │ ├──view +│ │ └──viewmodel +│ ├──musicComment/src/main/resources // 资源文件目录 +│ ├──musicList/src/main/ets // 歌曲列表页 +│ │ ├──components +│ │ ├──constants +│ │ ├──lyric +│ │ ├──view +│ │ └──viewmodel +│ └──musicList/src/main/resources // 资源文件目录 +└──products // 产品定制层 + ├──phone/src/main/ets // 支持手机、折叠屏、平板、PC/2in1 + │ ├──common + │ ├──entryability + │ ├──pages + │ ├──phonebackupextability + │ └──viewmodel + ├──phone/src/main/resources // 资源文件目录 + ├──watch/src/main/ets // 支持智能穿戴 + │ ├──constants + │ ├──pages + │ ├──view + │ ├──watchability + │ └──watchbackupability + └──watch/src/main/resources // 资源文件目录 +``` -1. 分别在手机、折叠屏、平板安装并打开应用,不同设备的应用页面通过响应式布局和自适应布局呈现不同的效果。 +## 使用说明 + +1. 分别在手机、折叠屏、平板、智能穿戴安装并打开应用,不同设备的应用页面通过响应式布局和自适应布局呈现不同的效果。 2. 点击界面上播放/暂停、上一首、下一首图标控制音乐播放功能。 3. 点击界面上播放控制区空白处或列表歌曲跳转到播放页面。 4. 点击界面上评论按钮跳转到对应的评论页面。 5. 其他按钮无实际点击事件或功能。 -### 相关权限 +## 具体实现 +1. 使用栅格布局监听断点变化,实现不同断点下的差异显示。 +2. 通过Tabs组件或Swiper组件进行区域的切换。 +3. 使用Blank组件实现中间空格自适应拉伸。 +4. 智能穿戴设备设置borderRadius实现圆形表盘。 + +## 相关权限 不涉及 -### 约束与限制 +## 约束与限制 -1. 本示例仅支持标准系统上运行,支持设备:华为手机、平板。 -2. HarmonyOS系统:HarmonyOS 5.0.5 Release及以上。 -3. DevEco Studio版本:DevEco Studio 5.0.5 Release及以上。 -4. HarmonyOS SDK版本:HarmonyOS 5.0.5 Release SDK及以上。 \ No newline at end of file +1. 本示例仅支持标准系统上运行,支持设备:华为手机、平板、智能穿戴。 +2. HarmonyOS系统:HarmonyOS 5.1.0 Release及以上。 +3. DevEco Studio版本:DevEco Studio 5.1.0 Release及以上。 +4. HarmonyOS SDK版本:HarmonyOS 5.1.0 Release SDK及以上。 \ No newline at end of file diff --git a/build-profile.json5 b/build-profile.json5 index 3d0f99af14199f64903e884cfc9a389996a09759..de53589d8883948be10dbf11bb60e97deed3f21a 100644 --- a/build-profile.json5 +++ b/build-profile.json5 @@ -5,8 +5,8 @@ { "name": "default", "signingConfig": "default", - "compatibleSdkVersion": "5.0.5(17)", - "targetSdkVersion": "5.0.5(17)", + "compatibleSdkVersion": "5.1.0(18)", + "targetSdkVersion": "5.1.0(18)", "runtimeOS": "HarmonyOS" } ], @@ -75,6 +75,18 @@ { "name": "constantsCommon", "srcPath": "./common/constantsCommon" + }, + { + "name": "watch", + "srcPath": "./products/watch", + "targets": [ + { + "name": "default", + "applyToProducts": [ + "default" + ] + } + ] } ] } \ No newline at end of file diff --git a/common/mediaCommon/src/main/ets/utils/BackgroundUtil.ets b/common/mediaCommon/src/main/ets/utils/BackgroundUtil.ets index 1ac92b37f02f31b7492d9b4eedb320bb0a450f93..bb85812b2801a121f9af637b49b9fdf871a6e169 100644 --- a/common/mediaCommon/src/main/ets/utils/BackgroundUtil.ets +++ b/common/mediaCommon/src/main/ets/utils/BackgroundUtil.ets @@ -27,7 +27,7 @@ export class BackgroundUtil { public static startContinuousTask(context?: common.UIAbilityContext): void { if (!context) { Logger.error('this avPlayer: ', `context undefined`); - return + return; } let wantAgentInfo: wantAgent.WantAgentInfo = { wants: [ @@ -55,7 +55,9 @@ export class BackgroundUtil { Logger.error('this avPlayer: ', `startBackgroundRunning failed. code ${(error as BusinessError).code} message ${(error as BusinessError).message}`); } - }); + }).catch((err: BusinessError) => { + Logger.error(`getWantAgent failed, code: ${JSON.stringify(err.code)}, message: ${JSON.stringify(err.message)}`); + }) } /** diff --git a/common/mediaCommon/src/main/ets/utils/BreakpointSystem.ets b/common/mediaCommon/src/main/ets/utils/BreakpointSystem.ets index ceb41c74234c05ca6f1c08b6c2a7cad080a01149..5b802ced25445e6a650aeb14478f6a23911624cb 100644 --- a/common/mediaCommon/src/main/ets/utils/BreakpointSystem.ets +++ b/common/mediaCommon/src/main/ets/utils/BreakpointSystem.ets @@ -60,17 +60,17 @@ export class BreakpointSystem { if (mediaQueryResult.matches) { this.updateCurrentBreakpoint(BreakpointConstants.BREAKPOINT_SM); } - } + }; private isBreakpointMD = (mediaQueryResult: mediaquery.MediaQueryResult): void => { if (mediaQueryResult.matches) { this.updateCurrentBreakpoint(BreakpointConstants.BREAKPOINT_MD); } - } + }; private isBreakpointLG = (mediaQueryResult: mediaquery.MediaQueryResult): void => { if (mediaQueryResult.matches) { this.updateCurrentBreakpoint(BreakpointConstants.BREAKPOINT_LG); } - } + }; public register(): void { this.smListener = uiContext?.getMediaQuery().matchMediaSync(BreakpointConstants.RANGE_SM); diff --git a/common/mediaCommon/src/main/ets/utils/MediaService.ets b/common/mediaCommon/src/main/ets/utils/MediaService.ets index a6a4e714a1399d249cff9902e1fef263157a0de1..d07f5411c9451bfe2b5a8a4cae3f7123843badbd 100644 --- a/common/mediaCommon/src/main/ets/utils/MediaService.ets +++ b/common/mediaCommon/src/main/ets/utils/MediaService.ets @@ -46,6 +46,7 @@ export class MediaService { private songList: SongItem[] = []; private formIds: string[] = []; private isCurrent: boolean = true; + private isNeedPlay: boolean = true; private seekCall: (seekDoneTime: number) => void = (seekDoneTime: number) => { this.isCurrent = true; @@ -97,10 +98,11 @@ export class MediaService { this.isPrepared = true; AppStorage.setOrCreate('totalTime', MediaTools.msToCountdownTime(this.getDuration())); AppStorage.setOrCreate('progressMax', this.getDuration()); - if (this.avPlayer) { + if (this.avPlayer && this.isNeedPlay) { this.avPlayer.play(); } this.setAVMetadata(); + this.isNeedPlay = true; Logger.info(TAG, 'AVPlayer prepared succeeded.'); break; case 'playing': @@ -155,6 +157,10 @@ export class MediaService { Logger.info(TAG, `on playPrevious , do playPrevious task`); this.playPrevious(); }; + private volumeChangeCall: (volume: number) => void = (volume: number) => { + Logger.info(TAG, `on volumeChangeCall, do volumeChange task`); + AppStorage.setOrCreate('volume', volume); + }; constructor() { let list: SongItem[] | undefined = AppStorage.get('songList'); @@ -182,8 +188,8 @@ export class MediaService { this.createSession(); } }) .catch((error: BusinessError) => { - Logger.error(TAG, 'this avPlayer: ', `catch error happened,error code is ${error.code}`) - }) + Logger.error(TAG, 'this avPlayer: ', `catch error happened,error code is ${error.code}`); + }); } private setAVPlayerCallback() { @@ -196,16 +202,22 @@ export class MediaService { this.avPlayer.on('timeUpdate', this.updateTimeCall); - this.avPlayer.on('stateChange', this.stateCall) + this.avPlayer.on('stateChange', this.stateCall); + + this.avPlayer.on('volumeChange', this.volumeChangeCall); } async createSession() { if (!this.context) { return; } - this.session = await avSession.createAVSession(this.context, 'SESSION_NAME', 'audio'); - this.session.activate(); - Logger.info(TAG, `session create done : sessionId : ${this.session.sessionId}`); + try { + this.session = await avSession.createAVSession(this.context, 'SESSION_NAME', 'audio'); + Logger.info(TAG, `session create done : sessionId : ${this.session.sessionId}`); + this.session.activate(); + } catch (error) { + Logger.error(TAG, `${error.code} + ${error.message}`); + } this.setAVMetadata(); let wantAgentInfo: wantAgent.WantAgentInfo = { wants: [ @@ -217,12 +229,15 @@ export class MediaService { operationType: wantAgent.OperationType.START_ABILITIES, requestCode: 0, wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG] - } + }; wantAgent.getWantAgent(wantAgentInfo).then((agent) => { if (this.session) { this.session.setLaunchAbility(agent); } }) + .catch((err: BusinessError) => { + console.error(`getWantAgent failed, code: ${JSON.stringify(err.code)}, message: ${JSON.stringify(err.message)}`); + }); this.setListenerForMesFromController(); } @@ -230,20 +245,28 @@ export class MediaService { if (!this.session) { return; } - this.session.on('play', this.playCall); - this.session.on('pause', this.pauseCall); - this.session.on('playNext', this.playNextCall); - this.session.on('playPrevious', this.playPreviousCall); + try { + this.session.on('play', this.playCall); + this.session.on('pause', this.pauseCall); + this.session.on('playNext', this.playNextCall); + this.session.on('playPrevious', this.playPreviousCall); + } catch (error) { + Logger.error(TAG, `${error.code} + ${error.message}`); + } } async unregisterSessionListener() { if (!this.session) { return; } - this.session.off('play'); - this.session.off('pause'); - this.session.off('playNext'); - this.session.off('playPrevious'); + try { + this.session.off('play'); + this.session.off('pause'); + this.session.off('playNext'); + this.session.off('playPrevious'); + } catch (error) { + Logger.error(TAG, `${error.code} + ${error.message}`); + } } async setAVMetadata() { @@ -278,8 +301,9 @@ export class MediaService { * Play music by index. * * @param musicIndex + * @param isNeedPlay */ - async loadAssent(musicIndex: number) { + async loadAssent(musicIndex: number, loadOnly?: boolean) { if (musicIndex >= this.songList.length) { Logger.error(TAG, `current musicIndex ${musicIndex}`); return; @@ -291,6 +315,9 @@ export class MediaService { this.songItem = await this.songItemBuilder.build(this.songList[this.musicIndex]); let url = this.songItemBuilder.getRealUrl(); if (url) { + if (loadOnly) { + this.isNeedPlay = false; + } let avFileDescriptor: media.AVFileDescriptor = { fd: url.fd, offset: url.offset, length: url.length }; this.avPlayer.fdSrc = avFileDescriptor; Logger.info(TAG, 'loadAsset avPlayer.url:' + this.avPlayer.fdSrc); @@ -372,7 +399,7 @@ export class MediaService { if (this.avPlayer) { this.avPlayer.prepare().then(() => { }).catch((error: BusinessError) => { - Logger.error(TAG, `start error ${JSON.stringify(error)}`) + Logger.error(TAG, `start error ${JSON.stringify(error)}`); this.state = AudioPlayerState.ERROR; this.updateIsPlay(false); this.isPrepared = false; @@ -389,12 +416,16 @@ export class MediaService { if (!this.isPrepared) { this.start(0); } else if (this.avPlayer) { - this.avPlayer.play().then(() => { - Logger.info(TAG, 'progressTime play() current time:' + this.getCurrentTime()); - this.seek(this.getCurrentTime()); - this.updateIsPlay(true); - this.state = AudioPlayerState.PLAY; - }) + try { + this.avPlayer.play().then(() => { + Logger.info(TAG, 'progressTime play() current time:' + this.getCurrentTime()); + this.seek(this.getCurrentTime()); + this.updateIsPlay(true); + this.state = AudioPlayerState.PLAY; + }); + } catch (error) { + Logger.error(TAG, `${error.code} + ${error.message}`); + } } } @@ -410,7 +441,9 @@ export class MediaService { if (this.context) { BackgroundUtil.stopContinuousTask(this.context); } - }); + }).catch((err: BusinessError) => { + Logger.error(TAG, 'Failed to pause,error message is :' + err.message); + }) } } @@ -426,7 +459,11 @@ export class MediaService { if (isFromControl) { this.playNext(); } else if (this.avPlayer) { - this.avPlayer.play(); + try { + this.avPlayer.play(); + } catch (error) { + Logger.error(TAG, `${error.code} + ${error.message}`); + } } break; case MusicPlayMode.ORDER: @@ -489,11 +526,15 @@ export class MediaService { public async stop() { Logger.info(TAG, 'stop()'); if (this.isPrepared && this.avPlayer) { - await this.avPlayer.stop(); - this.updateIsPlay(false); - this.state = AudioPlayerState.PAUSE; - if (this.context) { - BackgroundUtil.stopContinuousTask(this.context); + try { + await this.avPlayer.stop(); + this.updateIsPlay(false); + this.state = AudioPlayerState.PAUSE; + if (this.context) { + BackgroundUtil.stopContinuousTask(this.context); + } + } catch (error) { + Logger.error(TAG, `${error.code} + ${error.message}`); } } } @@ -502,7 +543,11 @@ export class MediaService { Logger.info(TAG, 'reset()'); await this.songItemBuilder.release(); if (this.avPlayer) { - await this.avPlayer.reset(); + try { + await this.avPlayer.reset(); + } catch (error) { + Logger.error(TAG, `${error.code} + ${error.message}`); + } } this.isPrepared = false; } @@ -512,20 +557,24 @@ export class MediaService { */ public release() { if (this.avPlayer && this.session && this.context) { - this.updateIsPlay(false); - this.stop(); - this.reset(); - this.avPlayer.release(); - this.state = AudioPlayerState.IDLE; - BackgroundUtil.stopContinuousTask(this.context); - this.unregisterSessionListener(); - this.session.destroy((err: BusinessError) => { - if (err) { - Logger.error(TAG, `Failed to destroy session. Code: ${err.code}, message: ${err.message}`); - } else { - Logger.info(TAG, `Destroy : SUCCESS `); - } - }); + try { + this.updateIsPlay(false); + this.stop(); + this.reset(); + this.avPlayer.release(); + this.state = AudioPlayerState.IDLE; + BackgroundUtil.stopContinuousTask(this.context); + this.unregisterSessionListener(); + this.session.destroy((err: BusinessError) => { + if (err) { + Logger.error(TAG, `Failed to destroy session. Code: ${err.code}, message: ${err.message}`); + } else { + Logger.info(TAG, `Destroy : SUCCESS `); + } + }); + } catch (error) { + Logger.error(TAG, `${error.code} + ${error.message}`); + } } } @@ -589,7 +638,7 @@ export class MediaService { musicCover: this.songList[this.musicIndex].label, musicSinger: this.songList[this.musicIndex].singer, cardSongList: cardSongList - } + }; let formInfo = formBindingData.createFormBindingData(formData); this.formIds.forEach(formId => { @@ -600,8 +649,8 @@ export class MediaService { if (error.code === SongConstants.ID_NO_EXIT && this.context) { PreferencesUtil.getInstance().removeFormId(this.context, formId); } - }) - }) + }); + }); } catch (error) { Logger.error(TAG, `updateCardData err: ${(error as BusinessError).code}`); } @@ -617,10 +666,14 @@ export class MediaService { } let formData: Record = { 'isPlay': false - } + }; let formInfo = formBindingData.createFormBindingData(formData); for (let index = 0; index < this.formIds.length; index++) { - await formProvider.updateForm(this.formIds[index], formInfo); + try { + await formProvider.updateForm(this.formIds[index], formInfo); + } catch (error) { + Logger.error(TAG, `${error.code} + ${error.message}`); + } } } } \ No newline at end of file diff --git a/common/mediaCommon/src/main/ets/utils/PreferencesUtil.ets b/common/mediaCommon/src/main/ets/utils/PreferencesUtil.ets index be10d451eaf9a941edb60ff77a7e7dbd2520ae43..77d67c0058d0d773042e2244940aead1d38189d1 100644 --- a/common/mediaCommon/src/main/ets/utils/PreferencesUtil.ets +++ b/common/mediaCommon/src/main/ets/utils/PreferencesUtil.ets @@ -40,8 +40,8 @@ export class PreferencesUtil { return; } resolve(pref); - }) - }) + }); + }); } preferencesFlush(preferences: preferences.Preferences) { @@ -49,7 +49,7 @@ export class PreferencesUtil { if (err) { Logger.error(TAG, `Failed to flush. Code:${err.code}, message:${err.message}`); } - }) + }); } preferencesPut(preferences: preferences.Preferences, formIds: Array): Promise { @@ -63,7 +63,7 @@ export class PreferencesUtil { } Logger.info(TAG, `preferencesPut succeed,formIds: ${JSON.stringify(formIds)}`); resolve(true); - }) + }); } catch (error) { Logger.error(TAG, `Failed to put data. Code: ${(error as BusinessError).code}, message:${(error as BusinessError).message}`); @@ -81,11 +81,13 @@ export class PreferencesUtil { } resolve(value); }); - }) + }); } removePreferencesFromCache(context: Context): void { - preferences.removePreferencesFromCache(context, MY_STORE); + preferences.removePreferencesFromCache(context, MY_STORE).catch((err: BusinessError) => { + console.error("Failed to remove preferences. code =" + err.code + ", message =" + err.message); + }) } async getFormIds(context: Context): Promise> { @@ -104,13 +106,13 @@ export class PreferencesUtil { } resolve(value as Array); Logger.info(TAG, `Succeeded in getting value of 'formIds'. val: ${value}.`); - }) - }) + }); + }); } catch (error) { Logger.error(TAG, `WANG Failed to get value of 'formIds'. Code:${(error as BusinessError).code}, message:${(error as BusinessError).message}`); } - return new Promise((resolve, reject) => {}) + return new Promise((resolve, reject) => {}); } async addFormId(context: Context, formId: string) { diff --git a/common/mediaCommon/src/main/ets/utils/SongItemBuilder.ets b/common/mediaCommon/src/main/ets/utils/SongItemBuilder.ets index 500224b53738fdc28a3c95c956cd71bf6e208b5b..3e31b9ea2c0bd9bd55174df7fc0c46b28f9697dd 100644 --- a/common/mediaCommon/src/main/ets/utils/SongItemBuilder.ets +++ b/common/mediaCommon/src/main/ets/utils/SongItemBuilder.ets @@ -32,24 +32,28 @@ export default class SongItemBuilder { let rawfileFd = await this.context.resourceManager.getRawFd(songItem.src) .catch((error: BusinessError) => { Logger.error(`resourceManager error code ${error.code} message ${error.message}`); - }) + }); if (rawfileFd) { this.realUrl = rawfileFd; } else { - Logger.error('get rawfileFd failed') + Logger.error('get rawfileFd failed'); } Logger.info('MediaAssetBuilder build realUrl:' + this.realUrl); return this.songItem; } public getRealUrl(): resourceManager.RawFileDescriptor | undefined { - Logger.info(`url ${this.realUrl}`) + Logger.info(`url ${this.realUrl}`); return this.realUrl; } public async release(): Promise { if (this.context && this.context !== null && this.songItem !== null) { - this.context.resourceManager.closeRawFd(this.songItem.src); + try { + this.context.resourceManager.closeRawFd(this.songItem.src); + } catch (error) { + Logger.error('Failed to closeRawFd'); + } } this.songItem = null; } diff --git a/common/mediaCommon/src/main/ets/viewmodel/MusicData.ets b/common/mediaCommon/src/main/ets/viewmodel/MusicData.ets index c99528f39c427e2c513ba6c1c1410661f907e9d4..67e8a98f668cd4aff532f493b3a2cb97151d6a82 100644 --- a/common/mediaCommon/src/main/ets/viewmodel/MusicData.ets +++ b/common/mediaCommon/src/main/ets/viewmodel/MusicData.ets @@ -36,4 +36,4 @@ enum AudioPlayerState { UNKNOWN } -export { MusicPlayMode, AudioPlayerState } \ No newline at end of file +export { MusicPlayMode, AudioPlayerState }; \ No newline at end of file diff --git a/features/live/src/main/ets/view/LiveList.ets b/features/live/src/main/ets/view/LiveList.ets index df4f8b48759614b5881e9b0d88a5a8bb65c2ec4d..74f778cfa706bb09292059e47aedd68cd1509bda 100644 --- a/features/live/src/main/ets/view/LiveList.ets +++ b/features/live/src/main/ets/view/LiveList.ets @@ -37,7 +37,7 @@ export struct LiveList { Scroll(this.scroller) { Column() { ForEach(this.liveStreams, (item: LiveStream, index: number) => { - this.LiveItem(item, index) + this.LiveItem(item, index); }, (item: LiveStream, index: number) => index + JSON.stringify(item)) } .width(LiveConstants.FULL_WIDTH_PERCENT) diff --git a/features/musicComment/src/main/ets/view/MusicCommentPage.ets b/features/musicComment/src/main/ets/view/MusicCommentPage.ets index cbfac26929b2ce9dd661416726b30425270a850c..6a0a2032694a9d734a4e4ed8d1360a312feea7bc 100644 --- a/features/musicComment/src/main/ets/view/MusicCommentPage.ets +++ b/features/musicComment/src/main/ets/view/MusicCommentPage.ets @@ -87,7 +87,7 @@ export struct MusicCommentPage { $r('app.float.margin_right_sm') : $r('app.float.margin_right') }) - this.ShowTitle($r('app.string.wonderful_comment')) + this.ShowTitle($r('app.string.wonderful_comment')); List() { ForEach(this.wonderfulComment, (comment: Comment, index?: number) => { @@ -142,7 +142,7 @@ export struct MusicCommentPage { $r('app.float.margin_right_sm') : $r('app.float.margin_right_list') }) - this.ShowTitle($r('app.string.new_comment')) + this.ShowTitle($r('app.string.new_comment')); List() { ForEach(this.newComment, (comment: Comment, index: number) => { diff --git a/features/musicList/Index.ets b/features/musicList/Index.ets index 917bff724095291c5613e4b6fb5c6d40a5c5aa16..c91b68b13ef892f0810c9fb21afb14e6f0090ea1 100644 --- a/features/musicList/Index.ets +++ b/features/musicList/Index.ets @@ -2,3 +2,4 @@ export { Content } from './src/main/ets/components/ListContent'; export { Header } from './src/main/ets/components/Header'; export { Player } from './src/main/ets/components/Player'; export { MusicListPage } from './src/main/ets/view/MusicListPage'; +export {songList} from '../musicList/src/main/ets/viewmodel/SongListData'; \ No newline at end of file diff --git a/features/musicList/src/main/ets/components/AlbumComponent.ets b/features/musicList/src/main/ets/components/AlbumComponent.ets index ca2427f72f6efe9101831b933af8bfcb2c8c8ef0..cd31cd89ef31a8084b011191bb42e7c6ab74ecec 100644 --- a/features/musicList/src/main/ets/components/AlbumComponent.ets +++ b/features/musicList/src/main/ets/components/AlbumComponent.ets @@ -140,19 +140,19 @@ export struct AlbumComponent { GridCol({ span: { sm: GridConstants.SPAN_FOUR, md: GridConstants.SPAN_TWELVE, lg: GridConstants.SPAN_TWELVE } }) { - this.CoverImage() + this.CoverImage(); } GridCol({ span: { sm: GridConstants.SPAN_EIGHT, md: GridConstants.SPAN_TWELVE, lg: GridConstants.SPAN_TWELVE } }) { - this.CoverIntroduction() + this.CoverIntroduction(); } GridCol({ span: { sm: GridConstants.SPAN_TWELVE, md: GridConstants.SPAN_TWELVE, lg: GridConstants.SPAN_TWELVE } }) { - this.CoverOptions() + this.CoverOptions(); } .padding({ top: this.currentBreakpoint === BreakpointConstants.BREAKPOINT_SM ? $r('app.float.option_margin') : 0, diff --git a/features/musicList/src/main/ets/components/ControlAreaComponent.ets b/features/musicList/src/main/ets/components/ControlAreaComponent.ets index 6c96ad716120b87e88a7c8b74bc529ec1a4002b7..51d096acb8513ff41cc0ec303065a174084e3b54 100644 --- a/features/musicList/src/main/ets/components/ControlAreaComponent.ets +++ b/features/musicList/src/main/ets/components/ControlAreaComponent.ets @@ -165,5 +165,5 @@ export struct ControlAreaComponent { function controlImageBuilder() { .aspectRatio(1) .opacity(0.86) - .objectFit(ImageFit.Contain) + .objectFit(ImageFit.Contain); } diff --git a/features/musicList/src/main/ets/components/LyricsComponent.ets b/features/musicList/src/main/ets/components/LyricsComponent.ets index 0a0d2f0559309902b43d7cb2b16d6e62a8fced99..0018b1bd0effd859895b8f2db149639591dfdde5 100644 --- a/features/musicList/src/main/ets/components/LyricsComponent.ets +++ b/features/musicList/src/main/ets/components/LyricsComponent.ets @@ -15,7 +15,7 @@ import { common } from '@kit.AbilityKit'; import { util } from '@kit.ArkTS'; -import { BreakpointType, SongItem } from 'mediacommon'; +import { BreakpointType, Logger, SongItem } from 'mediacommon'; import { BreakpointConstants, StyleConstants } from 'constantscommon'; import { LrcEntry } from '../lyric/LrcEntry'; import { parseKrcLyric, parseLrcLyric } from '../lyric/LrcUtils'; @@ -25,6 +25,8 @@ import { ControlAreaComponent } from './ControlAreaComponent'; import { LyricFile } from '../lyric/LyricConst'; import { PlayerConstants } from '../constants/PlayerConstants'; +const TAG = 'LyricsComponent'; + @Component export struct LyricsComponent { @StorageProp('currentBreakpoint') currentBreakpoint: string = BreakpointConstants.BREAKPOINT_SM; @@ -52,16 +54,20 @@ export struct LyricsComponent { if (!this.context) { return; } - this.context .resourceManager .getRawFileContent(this.songList[this.selectIndex].lyric) - .then((value: Uint8Array) => { - let textDecoder = util.TextDecoder.create(PlayerConstants.ENCODING, { ignoreBOM: true }); - let stringData = textDecoder.decodeToString(value, { stream: false }); - if (this.songList[this.selectIndex].lyric.endsWith(LyricFile.KRC)) { - this.mLrcEntryList = parseKrcLyric(stringData); - } else if (this.songList[this.selectIndex].lyric.endsWith(LyricFile.LRC)) { - this.mLrcEntryList = parseLrcLyric(stringData); - } - }) + try { + this.context.resourceManager.getRawFileContent(this.songList[this.selectIndex].lyric) + .then((value: Uint8Array) => { + let textDecoder = util.TextDecoder.create(PlayerConstants.ENCODING, { ignoreBOM: true }); + let stringData = textDecoder.decodeToString(value, { stream: false }); + if (this.songList[this.selectIndex].lyric.endsWith(LyricFile.KRC)) { + this.mLrcEntryList = parseKrcLyric(stringData); + } else if (this.songList[this.selectIndex].lyric.endsWith(LyricFile.LRC)) { + this.mLrcEntryList = parseLrcLyric(stringData); + } + }); + } catch (error) { + Logger.error(TAG, `${error.code} + ${error.message}`); + } } build() { diff --git a/features/musicList/src/main/ets/components/MusicControlComponent.ets b/features/musicList/src/main/ets/components/MusicControlComponent.ets index 557736299db42580e6a14c5ee31bc4ce4a732f63..72d6894072ffb7747c5a4f7794e03cb8753ea72f 100644 --- a/features/musicList/src/main/ets/components/MusicControlComponent.ets +++ b/features/musicList/src/main/ets/components/MusicControlComponent.ets @@ -20,12 +20,16 @@ import { common } from '@kit.AbilityKit'; import { BusinessError, Callback } from '@kit.BasicServicesKit'; import { BreakpointConstants, StyleConstants } from 'constantscommon'; import { ColorConversion, Logger, SongItem } from 'mediacommon'; +import { BreakpointConstants, StyleConstants } from 'constantscommon'; +import { ColorConversion, Logger, SongItem } from 'mediacommon'; import { LyricsComponent } from './LyricsComponent'; import { MusicInfoComponent } from './MusicInfoComponent'; import { ControlAreaComponent } from './ControlAreaComponent'; import { TopAreaComponent } from './TopAreaComponent'; import { PlayerConstants } from '../constants/PlayerConstants'; +const TAG = 'MusicControlComponent'; + @Preview @Component export struct MusicControlComponent { @@ -72,7 +76,11 @@ export struct MusicControlComponent { aboutToDisappear(): void { if (canIUse('SystemCapability.Window.SessionManager')) { - display.off('foldDisplayModeChange', this.callback); + try { + display.off('foldDisplayModeChange', this.callback); + } catch (error) { + Logger.error(TAG, `${error.code} + ${error.message}`); + } this.isFoldFull = false; } } @@ -259,21 +267,21 @@ export struct MusicControlComponent { let colorArr = ColorConversion.dealColor(color.red, color.green, color.blue); this.imageColor = `rgba(${colorArr[0]}, ${colorArr[1]}, ${colorArr[2]}, 1)`; } - }) + }); let headFilter = effectKit.createEffect(pixelMap); if (headFilter !== null) { headFilter.blur(PlayerConstants.IMAGE_BLUR); headFilter.getEffectPixelMap().then((value) => { this.imageLabel = value; - }) + }); } }) .catch((error: BusinessError) => { - Logger.error(`${error.code} + ${error.message}`) - }) + Logger.error(`${error.code} + ${error.message}`); + }); }) .catch((error: BusinessError) => { - Logger.error(`${error.code} + ${error.message}`) - }) + Logger.error(`${error.code} + ${error.message}`); + }); } } \ No newline at end of file diff --git a/features/musicList/src/main/ets/components/MusicInfoComponent.ets b/features/musicList/src/main/ets/components/MusicInfoComponent.ets index b85f98f3237947c5bac67302a2fe7bcce6d53815..6f19935dff57c85d1f14b76a46679e421f5a082f 100644 --- a/features/musicList/src/main/ets/components/MusicInfoComponent.ets +++ b/features/musicList/src/main/ets/components/MusicInfoComponent.ets @@ -47,8 +47,8 @@ export struct MusicInfoComponent { offset: { md: BreakpointConstants.OFFSET_MD } }) { Column() { - this.CoverInfo() - this.MusicInfo() + this.CoverInfo(); + this.MusicInfo(); Blank() ControlAreaComponent() } diff --git a/features/musicList/src/main/ets/components/PlayList.ets b/features/musicList/src/main/ets/components/PlayList.ets index 718aed16e523d7998334b71b0fbc433047970b10..8afebdc7863eb36386cf9cab04b49dbab3a4b495 100644 --- a/features/musicList/src/main/ets/components/PlayList.ets +++ b/features/musicList/src/main/ets/components/PlayList.ets @@ -1,3 +1,4 @@ +/* eslint-disable */ /* * Copyright (c) 2023 Huawei Device Co., Ltd. * Licensed under the Apache License, Version 2.0 (the "License"); @@ -101,7 +102,7 @@ export struct PlayList { .width($r('app.float.order_icon_size')) } .onClick(() => { - MediaService.getInstance().loadAssent(index) + MediaService.getInstance().loadAssent(index); this.isShowPlay = true; }) .height($r('app.float.list_item_height')) @@ -110,19 +111,19 @@ export struct PlayList { build() { Column() { - this.PlayAll() + this.PlayAll(); List() { LazyForEach(new SongDataSource(this.songList), (item: SongItem, index: number) => { ListItem() { Column() { - this.SongItem(item, index) + this.SongItem(item, index); } .padding({ left: $r('app.float.list_item_padding'), right: $r('app.float.list_item_padding') }) } - }, (item: SongItem, index?: number) => JSON.stringify(item) + index) + }, (item: SongItem, index?: number) => (item).toString() + index) } .width(StyleConstants.FULL_WIDTH) .backgroundColor(Color.White) @@ -130,6 +131,7 @@ export struct PlayList { .lanes(this.currentBreakpoint === BreakpointConstants.BREAKPOINT_LG ? ContentConstants.COL_TWO : ContentConstants.COL_ONE) .layoutWeight(1) + .cachedCount(2) .divider({ color: $r('app.color.list_divider'), strokeWidth: $r('app.float.stroke_width'), diff --git a/features/musicList/src/main/ets/components/Player.ets b/features/musicList/src/main/ets/components/Player.ets index a6c77cb7cd2b3d7c71945d016be5206b00ec4386..aaad3ffb501f1cd4f093efba3055c84eb8beacc7 100644 --- a/features/musicList/src/main/ets/components/Player.ets +++ b/features/musicList/src/main/ets/components/Player.ets @@ -219,7 +219,7 @@ export struct Player { }; windowStage.setWindowSystemBarProperties(sysBarProps); }).catch((error: BusinessError) => { - Logger.error(`${error.code} + ${error.message}`) + Logger.error(`${error.code} + ${error.message}`); }); }) .gesture( diff --git a/features/musicList/src/main/ets/lyric/LrcUtils.ets b/features/musicList/src/main/ets/lyric/LrcUtils.ets index 56a9e27c7751d0aa42b582eaf58cd4a51460ada2..d86b460285c3d000bc8664667f95926bc0ecae8b 100644 --- a/features/musicList/src/main/ets/lyric/LrcUtils.ets +++ b/features/musicList/src/main/ets/lyric/LrcUtils.ets @@ -33,10 +33,15 @@ const krcWordRegex2 = new RegExp('<(\\d+),(\\d+),(\\d+)>(.*)'); export async function getRawStringData(context: Context, rawFilePath: string): Promise { - let value = await context.resourceManager.getRawFileContent(rawFilePath); - let textDecoder = util.TextDecoder.create('utf-8', { ignoreBOM: true }); - let stringData = textDecoder.decodeToString(value, { stream: false }); - return stringData; + try { + let value = await context.resourceManager.getRawFileContent(rawFilePath); + let textDecoder = util.TextDecoder.create('utf-8', { ignoreBOM: true }); + let stringData = textDecoder.decodeToString(value, { stream: false }); + return stringData; + } catch (error) { + Logger.error('Failed to getRawStringData', `${error.code} + ${error.message}`); + return '' + } } /** @@ -68,7 +73,7 @@ export function parseLrcLyric(text: string): Array { if (lrc && lrc.length > 0) { lrc.sort((a, b) => { return a.lineStartTime - b.lineStartTime; - }) + }); for (let i = 0; i < lrc.length; i++) { if (i === lrc.length - 1) { lrc[i].lineDuration = Number.MAX_VALUE; @@ -125,7 +130,7 @@ export function parseKrcLyric(lyricText: string): LrcEntry[] { } lyricLines.sort((a, b) => { return a.lineStartTime - b.lineStartTime; - }) + }); return lyricLines; } diff --git a/features/musicList/src/main/ets/lyric/LrcView.ets b/features/musicList/src/main/ets/lyric/LrcView.ets index 640d29d2de2f7a5b239981acf8ac1445fd788647..bf0f49a3514bd407f944166dd72bd9c86ac3b9a0 100644 --- a/features/musicList/src/main/ets/lyric/LrcView.ets +++ b/features/musicList/src/main/ets/lyric/LrcView.ets @@ -481,7 +481,7 @@ export default struct shiLrcView { this.context.fillText(text, this.lrcX, this.lrcY, this.lrcWidth); } else { - this.context.textAlign = 'center' + this.context.textAlign = 'center'; this.context.fillText(text, this.lrcX, this.lrcY, this.lrcWidth); } break; diff --git a/features/musicList/src/main/ets/view/MusicListPage.ets b/features/musicList/src/main/ets/view/MusicListPage.ets index 18d86a88f95c500ce2160fc13d1071959221d025..43ec4e9d38626b338d0989862813b0e17a5b1f98 100644 --- a/features/musicList/src/main/ets/view/MusicListPage.ets +++ b/features/musicList/src/main/ets/view/MusicListPage.ets @@ -18,7 +18,7 @@ import { StyleConstants, BreakpointConstants, SongConstants } from 'constantscom import { Header } from '../components/Header'; import { Player } from '../components/Player'; import { Content } from '../components/ListContent'; -import { songList } from '../viewmodel/SongListData' +import { songList } from '../viewmodel/SongListData'; @Component export struct MusicListPage { diff --git a/features/musicList/src/main/ets/viewmodel/SongDataSource.ets b/features/musicList/src/main/ets/viewmodel/SongDataSource.ets index d9a8238e58c596e0db3ce37d2df14cde235b34ec..95694cb7fded0de337080579e70d5a91712f98b9 100644 --- a/features/musicList/src/main/ets/viewmodel/SongDataSource.ets +++ b/features/musicList/src/main/ets/viewmodel/SongDataSource.ets @@ -65,24 +65,24 @@ export class SongDataSource implements IDataSource { notifyDataAdd(index: number): void { this.listeners.forEach(listener => { listener.onDataAdd(index); - }) + }); } notifyDataChange(index: number): void { this.listeners.forEach(listener => { listener.onDataChange(index); - }) + }); } notifyDataDelete(index: number): void { this.listeners.forEach(listener => { listener.onDataDelete(index); - }) + }); } notifyDataMove(from: number, to: number): void { this.listeners.forEach(listener => { listener.onDataMove(from, to); - }) + }); } } \ No newline at end of file diff --git a/features/musicList/src/main/ets/viewmodel/SongListData.ets b/features/musicList/src/main/ets/viewmodel/SongListData.ets index 5362023ce7790b1dccddb9ee32a1ad7b79bdd6c0..058c88220d2fe06553b89555b840c5fae719dd08 100644 --- a/features/musicList/src/main/ets/viewmodel/SongListData.ets +++ b/features/musicList/src/main/ets/viewmodel/SongListData.ets @@ -86,7 +86,7 @@ const songList: SongItem[] = [ label: $r('app.media.ic_avatar17'), src: 'power.wav', index:32, lyric: '' }, { id: 34, title: '无归', singer: '小安安', mark: $r('app.media.ic_sq'), label: $r('app.media.ic_avatar11'), src: 'power.wav', index:33, lyric: '' } -] +]; const optionList : OptionItem[] = [ { image: $r('app.media.ic_collect'), text: $r('app.string.collect') }, @@ -95,7 +95,7 @@ const optionList : OptionItem[] = [ pageIndexInfos.pushPathByName(RouterUrlConstants.MUSIC_COMMENT, null); }}, { image: $r('app.media.ic_share'), text: $r('app.string.share') } -] +]; class OptionItem { image: Resource = $r('app.media.ic_collect'); @@ -103,4 +103,4 @@ class OptionItem { action?: (pageIndexInfos: NavPathStack) => void; } -export { optionList, OptionItem, songList } \ No newline at end of file +export { optionList, OptionItem, songList }; \ No newline at end of file diff --git a/products/phone/src/main/ets/entryability/EntryAbility.ets b/products/phone/src/main/ets/entryability/EntryAbility.ets index 40e406e35f978b0d584f0b98eeb32a0a35f66d12..670dea9caf5867edf48fcf00b49d193ed9211e16 100644 --- a/products/phone/src/main/ets/entryability/EntryAbility.ets +++ b/products/phone/src/main/ets/entryability/EntryAbility.ets @@ -22,7 +22,7 @@ export default class EntryAbility extends UIAbility { private windowObj?: window.Window; onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) { - AppStorage.setOrCreate('context', this.context) + AppStorage.setOrCreate('context', this.context); hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate'); } @@ -56,6 +56,9 @@ export default class EntryAbility extends UIAbility { avoidArea = data.getWindowAvoidArea(type); let topRectHeight = avoidArea.topRect.height; AppStorage.setOrCreate('topRectHeight', topRectHeight); + }).catch((error: BusinessError) => { + hilog.error(0x0000, 'testTag', 'Failed to getMainWindow. Cause: ', + JSON.stringify(error.code) + JSON.stringify(error.message)); }) windowStage.loadContent('pages/Index', (err) => { @@ -69,16 +72,20 @@ export default class EntryAbility extends UIAbility { } private updateBreakpoint(windowWidth: number): void{ - let windowWidthVp = windowWidth / display.getDefaultDisplaySync().densityPixels; - let curBp: string = ''; - if (windowWidthVp < BreakpointConstants.BREAKPOINT_VALUE_NUMBER[1]) { - curBp = BreakpointConstants.BREAKPOINT_SM; - } else if (windowWidthVp < BreakpointConstants.BREAKPOINT_VALUE_NUMBER[2]) { - curBp = BreakpointConstants.BREAKPOINT_MD; - } else { - curBp = BreakpointConstants.BREAKPOINT_LG; + try { + let windowWidthVp = windowWidth / display.getDefaultDisplaySync().densityPixels; + let curBp: string = ''; + if (windowWidthVp < BreakpointConstants.BREAKPOINT_VALUE_NUMBER[1]) { + curBp = BreakpointConstants.BREAKPOINT_SM; + } else if (windowWidthVp < BreakpointConstants.BREAKPOINT_VALUE_NUMBER[2]) { + curBp = BreakpointConstants.BREAKPOINT_MD; + } else { + curBp = BreakpointConstants.BREAKPOINT_LG; + } + AppStorage.setOrCreate('currentBreakpoint', curBp); + } catch (error) { + hilog.error(0x0000, 'testTag', 'Failed to updateBreakpoint. Cause: %{public}s', JSON.stringify(error) ?? ''); } - AppStorage.setOrCreate('currentBreakpoint', curBp); } onWindowStageDestroy() { diff --git a/products/watch/build-profile.json5 b/products/watch/build-profile.json5 new file mode 100644 index 0000000000000000000000000000000000000000..4d611879c7913fb0610c686e2399258ab3a6dad1 --- /dev/null +++ b/products/watch/build-profile.json5 @@ -0,0 +1,28 @@ +{ + "apiType": "stageMode", + "buildOption": { + }, + "buildOptionSet": [ + { + "name": "release", + "arkOptions": { + "obfuscation": { + "ruleOptions": { + "enable": false, + "files": [ + "./obfuscation-rules.txt" + ] + } + } + } + }, + ], + "targets": [ + { + "name": "default" + }, + { + "name": "ohosTest", + } + ] +} \ No newline at end of file diff --git a/products/watch/hvigorfile.ts b/products/watch/hvigorfile.ts new file mode 100644 index 0000000000000000000000000000000000000000..b0e3a1ab98a91bc918d6404b2413111a5011f14a --- /dev/null +++ b/products/watch/hvigorfile.ts @@ -0,0 +1,6 @@ +import { hapTasks } from '@ohos/hvigor-ohos-plugin'; + +export default { + system: hapTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ + plugins: [] /* Custom plugin to extend the functionality of Hvigor. */ +} \ No newline at end of file diff --git a/products/watch/obfuscation-rules.txt b/products/watch/obfuscation-rules.txt new file mode 100644 index 0000000000000000000000000000000000000000..272efb6ca3f240859091bbbfc7c5802d52793b0b --- /dev/null +++ b/products/watch/obfuscation-rules.txt @@ -0,0 +1,23 @@ +# Define project specific obfuscation rules here. +# You can include the obfuscation configuration files in the current module's build-profile.json5. +# +# For more details, see +# https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/source-obfuscation-V5 + +# Obfuscation options: +# -disable-obfuscation: disable all obfuscations +# -enable-property-obfuscation: obfuscate the property names +# -enable-toplevel-obfuscation: obfuscate the names in the global scope +# -compact: remove unnecessary blank spaces and all line feeds +# -remove-log: remove all console.* statements +# -print-namecache: print the name cache that contains the mapping from the old names to new names +# -apply-namecache: reuse the given cache file + +# Keep options: +# -keep-property-name: specifies property names that you want to keep +# -keep-global-name: specifies names that you want to keep in the global scope + +-enable-property-obfuscation +-enable-toplevel-obfuscation +-enable-filename-obfuscation +-enable-export-obfuscation \ No newline at end of file diff --git a/products/watch/oh-package.json5 b/products/watch/oh-package.json5 new file mode 100644 index 0000000000000000000000000000000000000000..1edae348bc0ae542154e24101e6e2a0b3a988678 --- /dev/null +++ b/products/watch/oh-package.json5 @@ -0,0 +1,17 @@ +{ + "name": "watch", + "version": "1.0.0", + "description": "Please describe the basic information.", + "main": "", + "author": "", + "license": "", + "dependencies": { + "musicList": "file:../../features/musicList", + "@ohos/constantsCommon": "file:../../common/constantsCommon", + "@ohos/live": "file:../../features/live", + "@ohos/musicComment": "file:../../features/musicComment", + "@ohos/musicList": "file:../../features/musicList", + "@ohos/mediaCommon": "file:../../common/mediaCommon", + } +} + diff --git a/products/watch/src/main/ets/constants/StyleConstants.ets b/products/watch/src/main/ets/constants/StyleConstants.ets new file mode 100644 index 0000000000000000000000000000000000000000..aa6725807601e82c63f4b3fb841f792985a3343a --- /dev/null +++ b/products/watch/src/main/ets/constants/StyleConstants.ets @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Common constants for all styles. + */ +export class StyleConstants { + /** + * Component width percentage: 100%. + */ + static readonly FULL_WIDTH: string = '100%'; + /** + * Component height percentage: 100%. + */ + static readonly FULL_HEIGHT: string = '100%'; + /** + * Circle Border Radius. + */ + static readonly CIRCLE_BORDER_RADIUS: string = '50%'; +} \ No newline at end of file diff --git a/products/watch/src/main/ets/pages/Index.ets b/products/watch/src/main/ets/pages/Index.ets new file mode 100644 index 0000000000000000000000000000000000000000..295e77c4650b6ef5eab327866e90e9daff2a3636 --- /dev/null +++ b/products/watch/src/main/ets/pages/Index.ets @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + ArcSwiper, + ArcSwiperAttribute, // The properties of ArcSwiper depend on the ArcSwiperAttribute object for import. + ArcDotIndicator, + ArcDirection, + ArcSwiperController +} from '@kit.ArkUI'; +import { Home } from '../view/Home'; +import { PlayList } from '../view/PlayList'; +import { MediaService } from '@ohos/mediaCommon'; +import { songList } from 'musicList'; +import { StyleConstants } from '../constants/StyleConstants'; + +@Entry +@Component +struct Index { + @Provide pageStack: NavPathStack = new NavPathStack(); + innerSelectedIndex: number = 0; + private wearableSwiperController: ArcSwiperController = new ArcSwiperController(); + private arcDotIndicator: ArcDotIndicator = new ArcDotIndicator(); + + aboutToAppear(): void { + AppStorage.setOrCreate('songList', songList); + MediaService.getInstance(); + } + + build() { + Navigation(this.pageStack) { + // [Start home_swiper] + Column() { + Row() { + ArcSwiper(this.wearableSwiperController) { + Home() + PlayList() + } + .duration(400) + .indicator(this.arcDotIndicator + .arcDirection(ArcDirection.SIX_CLOCK_DIRECTION) + .selectedItemColor('#FE1B48') + ) + // [StartExclude home_swiper] + .onAnimationStart((index: number, targetIndex: number) => { + this.innerSelectedIndex = targetIndex; + }) + .onGestureRecognizerJudgeBegin((event: BaseGestureEvent, current: GestureRecognizer, + others: Array): GestureJudgeResult => { // When the recognizer is about to succeed, set the recognizer enable status based on the current component state. + if (current) { + let target = current.getEventTargetInfo(); + if (target && current.isBuiltIn() && current.getType() == GestureControl.GestureType.PAN_GESTURE) { + // Here, the condition `swiperTarget.isBegin()` or `innerSelectedIndex === 0` indicates that the ArcSwiper has been swiped to the beginning. + let swiperTarget = target as ScrollableTargetInfo; + if (swiperTarget instanceof ScrollableTargetInfo && + (swiperTarget.isBegin() || this.innerSelectedIndex === 0)) { + let panEvent = event as PanGestureEvent; + if (panEvent && panEvent.offsetX > 0 && (swiperTarget.isBegin() || this.innerSelectedIndex === 0)) { + return GestureJudgeResult.REJECT; + } + } + } + } + return GestureJudgeResult.CONTINUE; + }) + + // [EndExclude home_swiper] + } + .height(StyleConstants.FULL_HEIGHT) + } + .width(StyleConstants.FULL_WIDTH) + + // [End home_swiper] + } + .hideTitleBar(true) + } +} \ No newline at end of file diff --git a/products/watch/src/main/ets/view/Home.ets b/products/watch/src/main/ets/view/Home.ets new file mode 100644 index 0000000000000000000000000000000000000000..42309b04c6927bc72e903cce3f9e040d3538ae62 --- /dev/null +++ b/products/watch/src/main/ets/view/Home.ets @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { LengthMetrics } from '@kit.ArkUI'; +import { + ArcList, + ArcListItem, + ArcListAttribute // The properties of ArcList depend on the ArcListAttribute object for import. +} from '@kit.ArkUI'; +import { StyleConstants } from '../constants/StyleConstants'; + +class Menu { + icon: Resource; + text: Resource; + pathName: string; + + constructor(icon: Resource, text: Resource, pathName: string) { + this.icon = icon; + this.text = text; + this.pathName = pathName; + } +} + +@Preview +@Component +export struct Home { + @Consume pageStack: NavPathStack; + readonly HOME_BTN_WIDTH: string = '90%'; + private menuList: Menu[] = [ + new Menu( + $r('app.media.hottest_playlists'), + $r('app.string.home_hottest_playlists'), + 'playList' + ), + new Menu( + $r('app.media.my_favorite'), + $r('app.string.home_my_favorite'), + 'songList' + ), + new Menu( + $r('app.media.is_playing'), + $r('app.string.home_is_playing'), + 'songPage' + ), + new Menu( + $r('app.media.setting'), + $r('app.string.home_setting'), + 'setting' + ) + ]; + + build() { + // [Start home_list] + Column() { + ArcList({ initialIndex: 0 }) { + ForEach(this.menuList, (item: Menu) => { + ArcListItem() { + Row() { + Image(item.icon) + .width($r('app.float.home_icon_width')) + .height($r('app.float.home_icon_width')) + .borderRadius(StyleConstants.CIRCLE_BORDER_RADIUS) + .backgroundColor($r('app.color.home_icon_background')) + .padding($r('app.float.home_icon_padding')) + + Text(item.text) + .fontColor($r('app.color.font_color')) + .fontSize($r('app.float.home_font_size')) + + Image($r('app.media.chevron_right')) + .width($r('app.float.home_icon_jump_width')) + } + .width(this.HOME_BTN_WIDTH) + .height($r('app.float.home_btn_height')) + .padding({ left: $r('app.float.list_btn_padding'), right: $r('app.float.list_btn_padding') }) + .justifyContent(FlexAlign.SpaceBetween) + .borderRadius(StyleConstants.CIRCLE_BORDER_RADIUS) + .backgroundColor($r('app.color.home_btn_background')) + // [StartExclude home_list] + .onClick(() => { + if (item.pathName === 'setting') { + return; + } + this.pageStack.replacePathByName(item.pathName, null); + }) + + // [EndExclude home_list] + } + }, (item: Menu, index: number) => JSON.stringify(item) + index) + } + .scrollBar(BarState.Off) + .space(LengthMetrics.vp(5)) + .borderRadius(StyleConstants.CIRCLE_BORDER_RADIUS) + .focusable(true) + .focusOnTouch(true) + .defaultFocus(true) + } + .align(Alignment.Center) + .width(StyleConstants.FULL_WIDTH) + .height(StyleConstants.FULL_HEIGHT) + .borderRadius(StyleConstants.CIRCLE_BORDER_RADIUS) + + // [End home_list] + } +} \ No newline at end of file diff --git a/products/watch/src/main/ets/view/PlayList.ets b/products/watch/src/main/ets/view/PlayList.ets new file mode 100644 index 0000000000000000000000000000000000000000..db5b87ebd079d915ea2cd0dcaa7c7981201ec400 --- /dev/null +++ b/products/watch/src/main/ets/view/PlayList.ets @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + ArcSwiper, + ArcSwiperAttribute, // The properties of ArcSwiper depend on the ArcSwiperAttribute object for import. + ArcSwiperController +} from '@kit.ArkUI'; +import { StyleConstants } from '../constants/StyleConstants'; + +@Builder +export function PlayListBuilder() { + PlayList(); +} + +class PlayListSheet { + name: Resource; + background: Resource; + title: Resource; + + constructor(name: Resource, background: Resource, title: Resource) { + this.name = name; + this.background = background; + this.title = title; + } +} + +@Preview +@Component +export struct PlayList { + @Consume pageStack: NavPathStack; + @State itemSimpleColor: Color | number | string = ''; + @State selectedItemSimpleColor: Color | number | string = ''; + innerSelectedIndex: number = 0; + private wearableSwiperController: ArcSwiperController = new ArcSwiperController(); + private playList: PlayListSheet[] = [ + new PlayListSheet( + $r('app.string.playlist_name_one'), + $r('app.media.playlist_bg1'), + $r('app.string.playlist_title_one') + ), + new PlayListSheet( + $r('app.string.playlist_name_two'), + $r('app.media.playlist_bg2'), + $r('app.string.playlist_title_two') + ) + ]; + + build() { + NavDestination() { + // [Start play_list] + Column() { + ArcSwiper(this.wearableSwiperController) { + ForEach(this.playList, (item: PlayListSheet) => { + Column({ space: 10 }) { + Row() { + Text(item.name) + .fontWeight(FontWeight.Bold) + .fontColor($r('app.color.font_color')) + .fontSize($r('app.float.home_font_size')) + Image($r('app.media.chevron_right')) + .width($r('app.float.home_icon_jump_width')) + .margin({ left: $r('app.float.playlist_padding') }) + } + + Image($r('app.media.play_btn_fill')) + .width($r('app.float.playlist_icon')) + .height($r('app.float.playlist_icon')) + .position({ x: '25%', y: '65%' }) + Text(item.title) + } + .width(StyleConstants.FULL_WIDTH) + .height(StyleConstants.FULL_HEIGHT) + .backgroundImage(item.background, ImageRepeat.NoRepeat) + .backgroundImageSize({ width: StyleConstants.FULL_WIDTH, height: StyleConstants.FULL_HEIGHT }) + .justifyContent(FlexAlign.SpaceBetween) + .padding({ top: $r('app.float.playlist_row_padding'), bottom: $r('app.float.playlist_row_padding') }) + // [StartExclude play_list] + .onClick(() => { + this.pageStack.replacePathByName('songList', null); + }) + + // [EndExclude play_list] + }, (item: PlayListSheet, index?: number) => index + JSON.stringify(item)) + } + .index(0) + .duration(400) + .focusable(true) + .focusOnTouch(true) + .defaultFocus(true) + .vertical(true) + .indicator(false) + // [StartExclude play_list] + .disableSwipe(false) + .digitalCrownSensitivity(CrownSensitivity.MEDIUM) + .disableTransitionAnimation(false) + .onAnimationStart((index: number, targetIndex: number) => { + this.innerSelectedIndex = targetIndex; + }) + .onGestureRecognizerJudgeBegin((event: BaseGestureEvent, current: GestureRecognizer, + ): GestureJudgeResult => { // When the recognizer is about to succeed, set the recognizer enable status based on the current component state. + if (current) { + let target = current.getEventTargetInfo(); + if (target && current.isBuiltIn() && current.getType() == GestureControl.GestureType.PAN_GESTURE) { + // Here, the condition `swiperTarget.isBegin()` or `innerSelectedIndex === 0` indicates that the ArcSwiper has been swiped to the beginning. + let swiperTarget = target as ScrollableTargetInfo; + if (swiperTarget instanceof ScrollableTargetInfo && + (swiperTarget.isBegin() || this.innerSelectedIndex === 0)) { + let panEvent = event as PanGestureEvent; + if (panEvent && panEvent.offsetX > 0 && (swiperTarget.isBegin() || this.innerSelectedIndex === 0)) { + return GestureJudgeResult.REJECT; + } + } + } + } + return GestureJudgeResult.CONTINUE; + }) + + // [EndExclude play_list] + } + .width(StyleConstants.FULL_WIDTH) + .height(StyleConstants.FULL_HEIGHT) + + // [End play_list] + } + .hideTitleBar(true) + } +} \ No newline at end of file diff --git a/products/watch/src/main/ets/view/SongList.ets b/products/watch/src/main/ets/view/SongList.ets new file mode 100644 index 0000000000000000000000000000000000000000..46d52a0c099f3e941d9502875564072c1c7ba178 --- /dev/null +++ b/products/watch/src/main/ets/view/SongList.ets @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { LengthMetrics } from '@kit.ArkUI'; +import { + ArcList, + ArcListItem, + ArcListAttribute, // The properties of ArcList depend on ArcListAttribute and ArcListItemAttribute objects for import. + ArcListItemAttribute +} from '@kit.ArkUI'; +import { MediaService, SongItem } from '@ohos/mediaCommon'; +import { StyleConstants } from '../constants/StyleConstants'; + +@Builder +export function SongListBuilder() { + SongList(); +} + +@Preview +@Component +struct SongList { + readonly HOME_BTN_WIDTH: string = '90%'; + @Consume pageStack: NavPathStack; + @StorageLink('songList') songList: SongItem[] = []; + + aboutToAppear(): void { + MediaService.getInstance(); + } + + build() { + NavDestination() { + // [Start song_list] + Column() { + ArcList({ initialIndex: 0 }) { + ForEach(this.songList, (item: SongItem, index: number) => { + ArcListItem() { + Row() { + Image(item.label) + .width($r('app.float.home_icon_width')) + .height($r('app.float.home_icon_width')) + .borderRadius(StyleConstants.CIRCLE_BORDER_RADIUS) + + Column() { + Text(item.title) + .fontWeight(FontWeight.Bold) + .fontColor($r('app.color.font_color')) + Text(item.singer) + .fontColor($r('app.color.text_color')) + } + .layoutWeight(1) + } + .width(this.HOME_BTN_WIDTH) + .height($r('app.float.home_btn_height')) + .padding({ left: $r('app.float.list_btn_padding'), right: $r('app.float.list_btn_padding') }) + .borderRadius(StyleConstants.CIRCLE_BORDER_RADIUS) + .focusable(true) + .focusOnTouch(true) + .backgroundColor($r('app.color.home_btn_background')) + } + .align(Alignment.Center) + // [StartExclude song_list] + .onClick(async () => { + await MediaService.getInstance().loadAssent(index); + this.pageStack.replacePathByName('songPage', null); + }) + + // [EndExclude song_list] + }, (item: SongItem, index: number) => JSON.stringify(item) + index) + } + .scrollBar(BarState.Off) + .space(LengthMetrics.vp(5)) + .borderRadius(StyleConstants.CIRCLE_BORDER_RADIUS) + .focusable(true) + .focusOnTouch(true) + .defaultFocus(true) + } + .align(Alignment.Center) + .width(StyleConstants.FULL_WIDTH) + .height(StyleConstants.FULL_HEIGHT) + .borderRadius(StyleConstants.CIRCLE_BORDER_RADIUS) + + // [End song_list] + } + .hideTitleBar(true) + } +} \ No newline at end of file diff --git a/products/watch/src/main/ets/view/SongPage.ets b/products/watch/src/main/ets/view/SongPage.ets new file mode 100644 index 0000000000000000000000000000000000000000..cec3f63136797f521d80449bbe84e58e1578188c --- /dev/null +++ b/products/watch/src/main/ets/view/SongPage.ets @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { MediaService, SongItem } from '@ohos/mediaCommon'; +import { StyleConstants } from '../constants/StyleConstants'; +import { VolumeSlider } from './VolumeSliderComponent'; + +@Builder +export function SongPageBuilder() { + SongPage(); +} + +@Preview +@Component +struct SongPage { + @StorageLink('songList') songList: SongItem[] = []; + @StorageLink('selectIndex') selectIndex: number = 0; + @StorageLink('isPlay') isPlay: boolean = false; + @StorageLink('progress') time: number = 0; + @StorageLink('progressMax') max: number = 0; + @StorageLink('isFirstLaunch') isFirstLaunch: boolean = true; + readonly HALF_WIDTH: string = '50%'; + + aboutToAppear(): void { + if (this.isFirstLaunch) { + MediaService.getInstance().loadAssent(0, true); + } + AppStorage.setOrCreate('isFirstLaunch', false); + } + + build() { + NavDestination() { + // [Start song_page] + Column() { + Column() { + Text(this.songList[this.selectIndex].title) + .fontWeight(FontWeight.Bold) + .fontColor($r('app.color.font_color')) + Text(this.songList[this.selectIndex].singer) + .fontColor($r('app.color.play_singer_color')) + } + + Row() { + Column() { + Image($r('app.media.previous_btn')) + .width($r('app.float.play_song_img')) + } + // [StartExclude song_page] + .onClick(() => { + MediaService.getInstance().playPrevious(); + }) + + // [EndExclude song_page] + Stack() { + Image(this.songList[this.selectIndex].label) + .width($r('app.float.play_circle_img')) + .height($r('app.float.play_circle_img')) + .borderRadius(StyleConstants.CIRCLE_BORDER_RADIUS) + + Progress({ value: this.time, total: this.max, type: ProgressType.Ring }) + .width($r('app.float.play_progress_width')) + .backgroundColor(Color.Transparent) + .color($r('app.color.font_color')) + + Image($r('app.media.play_btn')) + .width($r('app.float.play_song_img')) + .visibility(this.isPlay === true ? Visibility.None : Visibility.Visible) + // [StartExclude song_page] + .onClick(() => { + if (MediaService.getInstance().getFirst()) { + MediaService.getInstance().loadAssent(0); + } else { + this.isPlay ? MediaService.getInstance().pause() : MediaService.getInstance().play(); + } + }) + // [EndExclude song_page] + + Image($r('app.media.pause_btn')) + .width($r('app.float.play_song_img')) + .visibility(this.isPlay === true ? Visibility.Visible : Visibility.None) + // [StartExclude song_page] + .onClick(() => { + MediaService.getInstance().pause(); + }) + // [EndExclude song_page] + } + .width(this.HALF_WIDTH) + .align(Alignment.Center) + + Column() { + Image($r('app.media.next_btn')) + .width($r('app.float.play_song_img')) + } + // [StartExclude song_page] + .onClick(() => { + MediaService.getInstance().playNextAuto(true); + }) + + // [EndExclude song_page] + } + .justifyContent(FlexAlign.SpaceAround) + .width('85%') + + Row() { + Image($r('app.media.download')) + .width($r('app.float.play_icon_width')) + Image($r('app.media.repeat')) + .width($r('app.float.play_icon_width')) + Image($r('app.media.full_screen')) + .width($r('app.float.play_icon_width')) + } + .width('60%') + .justifyContent(FlexAlign.SpaceAround) + } + .width(StyleConstants.FULL_WIDTH) + .height(StyleConstants.FULL_HEIGHT) + .padding({ top: $r('app.float.play_column_padding'), bottom: $r('app.float.play_column_padding') }) + .justifyContent(FlexAlign.SpaceAround) + + // [End song_page] + VolumeSlider() + } + .hideTitleBar(true) + .focusable(true) + .focusOnTouch(true) + .defaultFocus(true) + .linearGradient({ + direction: GradientDirection.Bottom, + colors: [ + ['#5E4C4B', 1.0], + ['#695951', 1.0] + ] + }) + } +} \ No newline at end of file diff --git a/products/watch/src/main/ets/view/VolumeSliderComponent.ets b/products/watch/src/main/ets/view/VolumeSliderComponent.ets new file mode 100644 index 0000000000000000000000000000000000000000..1ceb26e8e992d068203d52121122d8335c9d9d61 --- /dev/null +++ b/products/watch/src/main/ets/view/VolumeSliderComponent.ets @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + ArcSlider, + ArcSliderLayoutOptions, + ArcSliderLayoutOptionsConstructorOptions, + ArcSliderOptions, + ArcSliderOptionsConstructorOptions, + ArcSliderPosition, + ArcSliderStyleOptions, + ArcSliderStyleOptionsConstructorOptions, + ArcSliderValueOptions, + ArcSliderValueOptionsConstructorOptions +} from '@kit.ArkUI'; +import { BusinessError } from '@kit.BasicServicesKit'; +import { MediaService } from '@ohos/mediaCommon'; +import { hilog } from '@kit.PerformanceAnalysisKit'; + +@Preview +@Component +export struct VolumeSlider { + @StorageLink('volume') volume: number = 0.4; + valueOptionsConstructorOptions: ArcSliderValueOptionsConstructorOptions = { + progress: this.volume * 100, + min: 0, + max: 100 + }; + layoutOptionsConstructorOptions: ArcSliderLayoutOptionsConstructorOptions = { + reverse: true, + position: ArcSliderPosition.RIGHT + }; + styleOptionsConstructorOptions: ArcSliderStyleOptionsConstructorOptions = { + trackThickness: 5, + activeTrackThickness: 8, + trackColor: '#33f1ebeb', + selectedColor: '#FE1B48', + trackBlur: 20 + }; + valueOptions: ArcSliderValueOptions = new ArcSliderValueOptions(this.valueOptionsConstructorOptions); + layoutOptions: ArcSliderLayoutOptions = new ArcSliderLayoutOptions(this.layoutOptionsConstructorOptions); + styleOptions: ArcSliderStyleOptions = new ArcSliderStyleOptions(this.styleOptionsConstructorOptions); + arcSliderOptionsConstructorOptions: ArcSliderOptionsConstructorOptions = { + valueOptions: this.valueOptions, + layoutOptions: this.layoutOptions, + styleOptions: this.styleOptions, + digitalCrownSensitivity: CrownSensitivity.LOW, + onChange: (progress: number) => { + this.setAVPlayerVolume(progress); + }, + }; + arcSliderOptions: ArcSliderOptions = new ArcSliderOptions(this.arcSliderOptionsConstructorOptions); + + setAVPlayerVolume(progress: number) { + try { + MediaService.getInstance().avPlayer?.setVolume(progress / 100); + } catch (error) { + hilog.error(0x0000, 'volumeSlider', 'ArcSlider setVolume failed', (error as BusinessError).code); + } + } + + build() { + // [Start volume_slider] + Column() { + ArcSlider({ options: this.arcSliderOptions }) + .focusable(true) + .focusOnTouch(true) + .defaultFocus(true) + .zIndex(999) + .onDigitalCrown((event: CrownEvent) => { + event.stopPropagation(); + const STEP_DEGREE = 20; + let newVolume = this.volume + event.degree / STEP_DEGREE; + newVolume = Math.max(0, Math.min(100, newVolume)); + this.setAVPlayerVolume(newVolume); + }) + Image($r('app.media.speaker_fill')) + .width($r('app.float.volume_icon_width')) + .height($r('app.float.volume_icon_width')) + .rotate({ angle: '-30deg' }) + .position({ + right: $r('app.float.volume_icon_right'), + top: $r('app.float.volume_icon_top'), + }) + } + .hitTestBehavior(HitTestMode.Transparent) + .position({ + top: 0, + right: 0 + }) + + // [End volume_slider] + } +} diff --git a/products/watch/src/main/ets/watchability/WatchAbility.ets b/products/watch/src/main/ets/watchability/WatchAbility.ets new file mode 100644 index 0000000000000000000000000000000000000000..c9e33b769ae30730569fba4c3f2e9ce7714215a6 --- /dev/null +++ b/products/watch/src/main/ets/watchability/WatchAbility.ets @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit'; +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { window } from '@kit.ArkUI'; + +const DOMAIN = 0x0000; + +export default class WatchAbility extends UIAbility { + onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { + AppStorage.setOrCreate('context', this.context); + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate'); + } + + onDestroy(): void { + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy'); + } + + onWindowStageCreate(windowStage: window.WindowStage): void { + // Main window is created, set main page for this ability + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate'); + + windowStage.loadContent('pages/Index', (err) => { + if (err.code) { + hilog.error(DOMAIN, 'testTag', `${err.code} + ${err.message}`); + return; + } + AppStorage.setOrCreate('uiContext', windowStage.getMainWindowSync().getUIContext()); + hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.'); + }); + } + + onWindowStageDestroy(): void { + // Main window is destroyed, release UI related resources + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageDestroy'); + } + + onForeground(): void { + // Ability has brought to foreground + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onForeground'); + } + + onBackground(): void { + // Ability has back to background + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground'); + } +} diff --git a/products/watch/src/main/ets/watchbackupability/WatchBackupAbility.ets b/products/watch/src/main/ets/watchbackupability/WatchBackupAbility.ets new file mode 100644 index 0000000000000000000000000000000000000000..2a9f646b70f1ff739aa1f995490bbd9e0aeca74f --- /dev/null +++ b/products/watch/src/main/ets/watchbackupability/WatchBackupAbility.ets @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { BackupExtensionAbility, BundleVersion } from '@kit.CoreFileKit'; + +const DOMAIN = 0x0000; + +export default class WatchBackupAbility extends BackupExtensionAbility { + async onBackup() { + hilog.info(DOMAIN, 'testTag', 'onBackup ok'); + await Promise.resolve(); + } + + async onRestore(bundleVersion: BundleVersion) { + hilog.info(DOMAIN, 'testTag', 'onRestore ok %{public}s', JSON.stringify(bundleVersion)); + await Promise.resolve(); + } +} \ No newline at end of file diff --git a/products/watch/src/main/module.json5 b/products/watch/src/main/module.json5 new file mode 100644 index 0000000000000000000000000000000000000000..99c543c0991fe3d32f63f66926dc96b292ca3110 --- /dev/null +++ b/products/watch/src/main/module.json5 @@ -0,0 +1,51 @@ +{ + "module": { + "name": "watch", + "type": "entry", + "description": "$string:module_desc", + "mainElement": "WatchAbility", + "deviceTypes": [ + "wearable", + ], + "deliveryWithInstall": true, + "installationFree": false, + "pages": "$profile:main_pages", + "routerMap": "$profile:router_map", + "abilities": [ + { + "name": "WatchAbility", + "srcEntry": "./ets/watchability/WatchAbility.ets", + "description": "$string:WatchAbility_desc", + "icon": "$media:layered_image", + "label": "$string:WatchAbility_label", + "startWindowIcon": "$media:startIcon", + "startWindowBackground": "$color:start_window_background", + "exported": true, + "skills": [ + { + "entities": [ + "entity.system.home" + ], + "actions": [ + "ohos.want.action.home" + ] + } + ] + } + ], + "extensionAbilities": [ + { + "name": "WatchBackupAbility", + "srcEntry": "./ets/watchbackupability/WatchBackupAbility.ets", + "type": "backup", + "exported": false, + "metadata": [ + { + "name": "ohos.extension.backup", + "resource": "$profile:backup_config" + } + ], + } + ] + } +} \ No newline at end of file diff --git a/products/watch/src/main/resources/base/element/color.json b/products/watch/src/main/resources/base/element/color.json new file mode 100644 index 0000000000000000000000000000000000000000..834cb898dc34373415e6571cb055404e74fe262b --- /dev/null +++ b/products/watch/src/main/resources/base/element/color.json @@ -0,0 +1,36 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#FFFFFF" + }, + { + "name": "font_color", + "value": "#F6F5F6" + }, + { + "name": "text_color", + "value": "#767674" + }, + { + "name": "home_icon_background", + "value": "#FC1B46" + }, + { + "name": "home_btn_background", + "value": "#2A2A29" + }, + { + "name": "playlist_color_one", + "value": "#4C5363" + }, + { + "name": "playlist_color_two", + "value": "#5D544D" + }, + { + "name": "play_singer_color", + "value": "#BAAEAD" + } + ] +} \ No newline at end of file diff --git a/products/watch/src/main/resources/base/element/float.json b/products/watch/src/main/resources/base/element/float.json new file mode 100644 index 0000000000000000000000000000000000000000..58ea5f8f08aefc7f6d98d70d9081349aa4e81bdf --- /dev/null +++ b/products/watch/src/main/resources/base/element/float.json @@ -0,0 +1,84 @@ +{ + "float": [ + { + "name": "home_font_size", + "value": "18vp" + }, + { + "name": "home_icon_width", + "value": "45vp" + }, + { + "name": "home_icon_jump_width", + "value": "16vp" + }, + { + "name": "home_icon_padding", + "value": "6vp" + }, + { + "name": "home_btn_space", + "value": "10vp" + }, + { + "name": "home_btn_height", + "value": "66vp" + }, + { + "name": "list_btn_padding", + "value": "10vp" + }, + { + "name": "playlist_padding", + "value": "6vp" + }, + { + "name": "playlist_icon", + "value": "30vp" + }, + { + "name": "playlist_img_border_radius", + "value": "12vp" + }, + { + "name": "playlist_margin", + "value": "4vp" + }, + { + "name": "playlist_row_padding", + "value": "16vp" + }, + { + "name": "play_song_img", + "value": "36vp" + }, + { + "name": "play_icon_width", + "value": "24vp" + }, + { + "name": "play_circle_img", + "value": "90vp" + }, + { + "name": "play_progress_width", + "value": "100vp" + }, + { + "name": "play_column_padding", + "value": "16vp" + }, + { + "name": "volume_icon_width", + "value": "15vp" + }, + { + "name": "volume_icon_right", + "value": "22vp" + }, + { + "name": "volume_icon_top", + "value": "60vp" + } + ] +} diff --git a/products/watch/src/main/resources/base/element/string.json b/products/watch/src/main/resources/base/element/string.json new file mode 100644 index 0000000000000000000000000000000000000000..0e6d8920ab281b964c6180983b67665bb939f012 --- /dev/null +++ b/products/watch/src/main/resources/base/element/string.json @@ -0,0 +1,48 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "module description" + }, + { + "name": "WatchAbility_desc", + "value": "description" + }, + { + "name": "WatchAbility_label", + "value": "MusicHome" + }, + { + "name": "home_hottest_playlists", + "value": "hottest_playlists" + }, + { + "name": "home_my_favorite", + "value": "My Favorite" + }, + { + "name": "home_is_playing", + "value": "Is Playing" + }, + { + "name": "home_setting", + "value": "Setting" + }, + { + "name": "playlist_name_one", + "value": "Popular Playlists" + }, + { + "name": "playlist_name_two", + "value": "Exercise Music" + }, + { + "name": "playlist_title_one", + "value": "New independent singer-songwriter" + }, + { + "name": "playlist_title_two", + "value": "Pleasant jogging " + } + ] +} \ No newline at end of file diff --git a/products/watch/src/main/resources/base/media/chevron_right.svg b/products/watch/src/main/resources/base/media/chevron_right.svg new file mode 100644 index 0000000000000000000000000000000000000000..8dc4eb7efe15b8a6f24dabed3f23ce4de8a4009f --- /dev/null +++ b/products/watch/src/main/resources/base/media/chevron_right.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/products/watch/src/main/resources/base/media/download.svg b/products/watch/src/main/resources/base/media/download.svg new file mode 100644 index 0000000000000000000000000000000000000000..71509bddc564c5dc30a0e7ce6d78ee0be02d2fc6 --- /dev/null +++ b/products/watch/src/main/resources/base/media/download.svg @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/products/watch/src/main/resources/base/media/full_screen.svg b/products/watch/src/main/resources/base/media/full_screen.svg new file mode 100644 index 0000000000000000000000000000000000000000..ae7f22ccca468bd7901c02ea2383264d6a246089 --- /dev/null +++ b/products/watch/src/main/resources/base/media/full_screen.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/products/watch/src/main/resources/base/media/hottest_playlists.svg b/products/watch/src/main/resources/base/media/hottest_playlists.svg new file mode 100644 index 0000000000000000000000000000000000000000..27c8c75c77a06a3600247a3f7fc56c1be0099b9d --- /dev/null +++ b/products/watch/src/main/resources/base/media/hottest_playlists.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/products/watch/src/main/resources/base/media/is_playing.svg b/products/watch/src/main/resources/base/media/is_playing.svg new file mode 100644 index 0000000000000000000000000000000000000000..45111b3022fd893b700dd2ae45e5cd62c7c25040 --- /dev/null +++ b/products/watch/src/main/resources/base/media/is_playing.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/products/watch/src/main/resources/base/media/layered_image.json b/products/watch/src/main/resources/base/media/layered_image.json new file mode 100644 index 0000000000000000000000000000000000000000..fb49920440fb4d246c82f9ada275e26123a2136a --- /dev/null +++ b/products/watch/src/main/resources/base/media/layered_image.json @@ -0,0 +1,7 @@ +{ + "layered-image": + { + "background" : "$media:background", + "foreground" : "$media:foreground" + } +} \ No newline at end of file diff --git a/products/watch/src/main/resources/base/media/my_favorite.svg b/products/watch/src/main/resources/base/media/my_favorite.svg new file mode 100644 index 0000000000000000000000000000000000000000..837bf06c5176aa9258275c1e0370d1a59edb221a --- /dev/null +++ b/products/watch/src/main/resources/base/media/my_favorite.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/products/watch/src/main/resources/base/media/next_btn.svg b/products/watch/src/main/resources/base/media/next_btn.svg new file mode 100644 index 0000000000000000000000000000000000000000..873a96a3f3934747a8c2482ea0b9ae7e7880a6f8 --- /dev/null +++ b/products/watch/src/main/resources/base/media/next_btn.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/products/watch/src/main/resources/base/media/pause_btn.svg b/products/watch/src/main/resources/base/media/pause_btn.svg new file mode 100644 index 0000000000000000000000000000000000000000..6b931769ca31ecf78dbeae95e6506e00dbc70fa0 --- /dev/null +++ b/products/watch/src/main/resources/base/media/pause_btn.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/products/watch/src/main/resources/base/media/play_btn.svg b/products/watch/src/main/resources/base/media/play_btn.svg new file mode 100644 index 0000000000000000000000000000000000000000..78098f400d6bb2d3ebdabc651c57b0d89ee72ead --- /dev/null +++ b/products/watch/src/main/resources/base/media/play_btn.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/products/watch/src/main/resources/base/media/play_btn_fill.svg b/products/watch/src/main/resources/base/media/play_btn_fill.svg new file mode 100644 index 0000000000000000000000000000000000000000..0b004278380e9481e96763c2bf379eb2eb0e50e8 --- /dev/null +++ b/products/watch/src/main/resources/base/media/play_btn_fill.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/products/watch/src/main/resources/base/media/playlist_bg1.png b/products/watch/src/main/resources/base/media/playlist_bg1.png new file mode 100644 index 0000000000000000000000000000000000000000..3743af2116d88ad8ec06725c6b48db252106b8e4 Binary files /dev/null and b/products/watch/src/main/resources/base/media/playlist_bg1.png differ diff --git a/products/watch/src/main/resources/base/media/playlist_bg2.png b/products/watch/src/main/resources/base/media/playlist_bg2.png new file mode 100644 index 0000000000000000000000000000000000000000..ec5ebf0553df24f59e5fba17f8e6f15b51a21600 Binary files /dev/null and b/products/watch/src/main/resources/base/media/playlist_bg2.png differ diff --git a/products/watch/src/main/resources/base/media/previous_btn.svg b/products/watch/src/main/resources/base/media/previous_btn.svg new file mode 100644 index 0000000000000000000000000000000000000000..1740ea4acdd38df4f726459d5fbbf8f1487fcba6 --- /dev/null +++ b/products/watch/src/main/resources/base/media/previous_btn.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/products/watch/src/main/resources/base/media/repeat.svg b/products/watch/src/main/resources/base/media/repeat.svg new file mode 100644 index 0000000000000000000000000000000000000000..e146ead47915e642eb5f4362413cf497a43a52ee --- /dev/null +++ b/products/watch/src/main/resources/base/media/repeat.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/products/watch/src/main/resources/base/media/setting.svg b/products/watch/src/main/resources/base/media/setting.svg new file mode 100644 index 0000000000000000000000000000000000000000..98cc6d8a4bda4a0402f4fd89d1ad5179f10a490c --- /dev/null +++ b/products/watch/src/main/resources/base/media/setting.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/products/watch/src/main/resources/base/media/speaker_fill.svg b/products/watch/src/main/resources/base/media/speaker_fill.svg new file mode 100644 index 0000000000000000000000000000000000000000..0e9b38170c4310d099988184772eec6ef023cc29 --- /dev/null +++ b/products/watch/src/main/resources/base/media/speaker_fill.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/products/watch/src/main/resources/base/media/startIcon.png b/products/watch/src/main/resources/base/media/startIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..205ad8b5a8a42e8762fbe4899b8e5e31ce822b8b Binary files /dev/null and b/products/watch/src/main/resources/base/media/startIcon.png differ diff --git a/products/watch/src/main/resources/base/profile/backup_config.json b/products/watch/src/main/resources/base/profile/backup_config.json new file mode 100644 index 0000000000000000000000000000000000000000..78f40ae7c494d71e2482278f359ec790ca73471a --- /dev/null +++ b/products/watch/src/main/resources/base/profile/backup_config.json @@ -0,0 +1,3 @@ +{ + "allowToBackupRestore": true +} \ No newline at end of file diff --git a/products/watch/src/main/resources/base/profile/main_pages.json b/products/watch/src/main/resources/base/profile/main_pages.json new file mode 100644 index 0000000000000000000000000000000000000000..55c3f007f87b7ce5206d325f968cc56f2f79441f --- /dev/null +++ b/products/watch/src/main/resources/base/profile/main_pages.json @@ -0,0 +1,5 @@ +{ + "src": [ + "pages/Index" + ] +} \ No newline at end of file diff --git a/products/watch/src/main/resources/base/profile/router_map.json b/products/watch/src/main/resources/base/profile/router_map.json new file mode 100644 index 0000000000000000000000000000000000000000..12b3ff12fa24cf000ef6408cda5748fd00cd7c22 --- /dev/null +++ b/products/watch/src/main/resources/base/profile/router_map.json @@ -0,0 +1,19 @@ +{ + "routerMap" : [ + { + "name" : "playList", + "pageSourceFile" : "src/main/ets/view/PlayList.ets", + "buildFunction" : "PlayListBuilder" + }, + { + "name" : "songList", + "pageSourceFile" : "src/main/ets/view/SongList.ets", + "buildFunction" : "SongListBuilder" + }, + { + "name" : "songPage", + "pageSourceFile" : "src/main/ets/view/SongPage.ets", + "buildFunction" : "SongPageBuilder" + } + ] +} \ No newline at end of file diff --git a/products/watch/src/main/resources/en_US/element/string.json b/products/watch/src/main/resources/en_US/element/string.json new file mode 100644 index 0000000000000000000000000000000000000000..75c40fb622988ae44ed8fb440f09aaf1e80f34f5 --- /dev/null +++ b/products/watch/src/main/resources/en_US/element/string.json @@ -0,0 +1,48 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "module description" + }, + { + "name": "WatchAbility_desc", + "value": "description" + }, + { + "name": "WatchAbility_label", + "value": "MusicHome" + }, + { + "name": "home_hottest_playlists", + "value": "hottest Playlists" + }, + { + "name": "home_my_favorite", + "value": "My Favorite" + }, + { + "name": "home_is_playing", + "value": "Is Playing" + }, + { + "name": "home_setting", + "value": "Setting" + }, + { + "name": "playlist_name_one", + "value": "Popular Playlists " + }, + { + "name": "playlist_name_two", + "value": "Exercise Music " + }, + { + "name": "playlist_title_one", + "value": "New independent singer-songwriter" + }, + { + "name": "playlist_title_two", + "value": "Pleasant jogging " + } + ] +} \ No newline at end of file diff --git a/products/watch/src/main/resources/zh_CN/element/string.json b/products/watch/src/main/resources/zh_CN/element/string.json new file mode 100644 index 0000000000000000000000000000000000000000..d6af1dc5aabcc1bda25b0d8a53fc919d5ea1aafa --- /dev/null +++ b/products/watch/src/main/resources/zh_CN/element/string.json @@ -0,0 +1,48 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "module description" + }, + { + "name": "WatchAbility_desc", + "value": "description" + }, + { + "name": "WatchAbility_label", + "value": "多设备界面音乐" + }, + { + "name": "home_hottest_playlists", + "value": "热门歌单" + }, + { + "name": "home_my_favorite", + "value": "我喜欢" + }, + { + "name": "home_is_playing", + "value": "正在播放" + }, + { + "name": "home_setting", + "value": "设置" + }, + { + "name": "playlist_name_one", + "value": "热门歌单" + }, + { + "name": "playlist_name_two", + "value": "运动音乐" + }, + { + "name": "playlist_title_one", + "value": "新锐独立唱作人" + }, + { + "name": "playlist_title_two", + "value": "惬意慢跑" + } + ] +} \ No newline at end of file diff --git a/screenshots/device/wearable.png b/screenshots/device/wearable.png new file mode 100644 index 0000000000000000000000000000000000000000..be1a81e2c985f0a444c673ce5458e4b2158c25d6 Binary files /dev/null and b/screenshots/device/wearable.png differ