1/* 2 * Copyright (c) 2025 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16const TAG = 'AVInputCastPicker_component'; 17 18/** 19 * menuItem device info 20 */ 21interface AVInputCastPickerDeviceInfo { 22 deviceId: number | String, 23 deviceType: number, 24 deviceName: string, 25 deviceIconName: string, 26 isConnected: boolean, 27 selectedIconName: string, 28} 29 30/** 31 * Definition of av cast picker state. 32 */ 33export enum AVCastPickerState { 34 /** 35 * The picker starts showing. 36 */ 37 STATE_APPEARING, 38 39 /** 40 * The picker finishes presenting. 41 */ 42 STATE_DISAPPEARING 43} 44 45@Component 46export struct AVInputCastPicker { 47 /** 48 * Custom builder from application. 49 */ 50 @BuilderParam customPicker: (() => void); 51 /** 52 * Device list data. 53 */ 54 @State deviceInfoList: Array<AVInputCastPickerDeviceInfo> = []; 55 /** 56 * Touch item index. 57 */ 58 @State touchMenuItemIndex: number = -1; 59 /** 60 * Configuration color mode. 61 */ 62 @State isDarkMode: boolean = false; 63 /** 64 * Mirrored or not. 65 */ 66 @State isRTL: boolean = false; 67 /** 68 * Picker state change callback. 69 */ 70 private onStateChange?: (state: AVCastPickerState) => void; 71 /** 72 * UIExtensionProxy. 73 */ 74 private extensionProxy: UIExtensionProxy | null = null; 75 /** 76 * show menu start time. 77 */ 78 private pickerClickTime: number = -1; 79 /** 80 * picker count on creation. 81 */ 82 private pickerCountOnCreation: number = 0; 83 /** 84 * current picker count. 85 */ 86 private static currentPickerCount: number = 0; 87 88 aboutToAppear(): void { 89 AVInputCastPicker.currentPickerCount += 1; 90 this.pickerCountOnCreation = AVInputCastPicker.currentPickerCount; 91 } 92 93 aboutToDisappear(): void { 94 AVInputCastPicker.currentPickerCount -= 1; 95 } 96 97 @Builder 98 iconBuilder(item: AVInputCastPickerDeviceInfo, isSelected: boolean): void { 99 SymbolGlyph(!isSelected ? $r(item.deviceIconName) : $r(item.selectedIconName)) 100 .fontSize('24vp') 101 .fontColor((isSelected && !this.isDarkMode) ? 102 [$r('sys.color.comp_background_emphasize')] : [$r('sys.color.icon_primary')]) 103 .renderingStrategy(SymbolRenderingStrategy.SINGLE) 104 } 105 106 @Builder 107 textBuilder(item: AVInputCastPickerDeviceInfo) { 108 Text(item.deviceName) 109 .fontSize($r('sys.float.ohos_id_text_size_body2')) 110 .fontColor(item.isConnected ? (!this.isDarkMode ? $r('sys.color.font_emphasize') : $r('sys.color.font_primary')) : 111 (!this.isDarkMode ? $r('sys.color.font_primary') : $r('sys.color.font_secondary'))) 112 .width(254) 113 .padding({ 114 left: 8, 115 top: 11, 116 right: 8, 117 bottom: 11 118 }) 119 .textOverflow({ overflow: TextOverflow.Ellipsis }) 120 .maxLines(2) 121 .wordBreak(WordBreak.BREAK_WORD) 122 .direction(this.isRTL ? Direction.Rtl : Direction.Ltr) 123 } 124 125 @Builder 126 deviceMenu() { 127 Column() { 128 ForEach(this.deviceInfoList, (item: AVInputCastPickerDeviceInfo, index) => { 129 Flex({ 130 direction: FlexDirection.Column, 131 justifyContent: FlexAlign.SpaceBetween, 132 alignItems: ItemAlign.End 133 }) { 134 Flex({ 135 direction: FlexDirection.Row, 136 justifyContent: FlexAlign.SpaceBetween, 137 alignItems: ItemAlign.Center 138 }) { 139 Row() { 140 this.iconBuilder(item, false) 141 142 this.textBuilder(item) 143 } 144 .alignItems(VerticalAlign.Center) 145 146 if (item.isConnected && item.selectedIconName !== null && item.selectedIconName !== undefined) { 147 Row() { 148 this.iconBuilder(item, true) 149 } 150 .alignItems(VerticalAlign.Center) 151 } 152 } 153 .constraintSize({ minHeight: 40 }) 154 .padding({ left: 12, right: 12 }) 155 .onTouch((event) => { 156 if (event.type === TouchType.Down) { 157 this.touchMenuItemIndex = index; 158 } else if (event.type === TouchType.Up) { 159 this.touchMenuItemIndex = -1; 160 } 161 }) 162 .backgroundColor(this.touchMenuItemIndex === index ? $r('sys.color.interactive_click') : '#00FFFFFF') 163 .borderRadius(this.touchMenuItemIndex === index ? $r('sys.float.corner_radius_level2') : 0) 164 165 if (index !== this.deviceInfoList.length - 1) { 166 Row() 167 .width('100%') 168 .height(2) 169 } 170 } 171 .width('100%') 172 .onClick(() => { 173 console.info(TAG, ` item click ${item.isConnected}`) 174 if (this.extensionProxy !== null && !item.isConnected) { 175 this.extensionProxy.send({ 'selectedDeviceInfo': item }) 176 } 177 }) 178 }) 179 } 180 .width(326) 181 .borderRadius(8) 182 } 183 184 @Builder 185 private buildDefaultPicker(isCustomPicker: boolean) { 186 UIExtensionComponent({ 187 abilityName: 'AVInputCastPickerAbility', 188 bundleName: 'com.hmos.mediacontroller', 189 parameters: { 190 'ability.want.params.uiExtensionType': 'sysPicker/mediaControl', 191 'isCustomPicker': isCustomPicker, 192 'currentPickerCount': this.pickerCountOnCreation, 193 } 194 }) 195 .size({ width: '100%', height: '100%' }) 196 .bindMenu(this.deviceMenu(), { 197 onDisappear: () => { 198 this.touchMenuItemIndex = -1; 199 if (this.onStateChange !== null && this.onStateChange !== undefined) { 200 this.onStateChange(AVCastPickerState.STATE_DISAPPEARING); 201 } 202 }, 203 onAppear: () => { 204 if (this.onStateChange !== null && this.onStateChange !== undefined) { 205 this.onStateChange(AVCastPickerState.STATE_APPEARING); 206 } 207 if (this.extensionProxy !== null && this.pickerClickTime !== -1) { 208 this.extensionProxy.send({ 'timeCost': new Date().getTime() - this.pickerClickTime }); 209 this.pickerClickTime = -1; 210 } 211 } 212 }) 213 .onClick(() => { 214 this.pickerClickTime = new Date().getTime(); 215 }) 216 .onRemoteReady((proxy: UIExtensionProxy) => { 217 console.info(TAG, 'onRemoteReady'); 218 this.extensionProxy = proxy; 219 }) 220 .onReceive((data) => { 221 if (JSON.stringify(data['deviceInfoList']) !== undefined) { 222 this.deviceInfoList = JSON.parse(JSON.stringify(data['deviceInfoList'])); 223 } 224 225 if (JSON.stringify(data['isDarkMode']) !== undefined) { 226 console.info(TAG, `isDarkMode : ${JSON.stringify(data['isDarkMode'])}`); 227 this.isDarkMode = data['isDarkMode'] as boolean; 228 } 229 230 if (JSON.stringify(data['isRTL']) !== undefined) { 231 console.info(TAG, `isRTL : ${JSON.stringify(data['isRTL'])}`); 232 this.isRTL = data['isRTL'] as boolean; 233 } 234 }) 235 .onTerminated((info) => { 236 console.info(TAG, ` onTerminated code: ${info?.code}`); 237 }) 238 .onError((error) => { 239 console.info(TAG, ` onError code: ${error?.code} message: ${error?.message}`); 240 }) 241 } 242 243 @Builder 244 private buildCustomPicker() { 245 Stack({ alignContent: Alignment.Center }) { 246 Column() { 247 this.customPicker(); 248 } 249 .alignItems(HorizontalAlign.Center) 250 .justifyContent(FlexAlign.Center) 251 .size({ width: '100%', height: '100%' }) 252 .zIndex(0) 253 254 Column() { 255 this.buildDefaultPicker(true); 256 } 257 .alignItems(HorizontalAlign.Center) 258 .justifyContent(FlexAlign.Center) 259 .size({ width: '100%', height: '100%' }) 260 .zIndex(1) 261 } 262 .size({ width: '100%', height: '100%' }) 263 } 264 265 build() { 266 Column() { 267 if (this.customPicker === undefined) { 268 this.buildDefaultPicker(false); 269 } else { 270 this.buildCustomPicker(); 271 } 272 } 273 .size({ width: '100%', height: '100%' }) 274 } 275}