import { ToastUtil } from '@pura/harmony-utils'; import { CommonConstants } from '../common/constants/CommonConstants'; import { RemoteDriveType } from '../common/enums/RemoteDriveType'; import { WebDavAccount } from '../viewmodel/WebDavAccount'; import { ImagePickerUtil } from '../common/util/ImagePickerUtil'; import Logger from '../common/util/Logger'; import { ConfigurationConstant, Context } from '@kit.AbilityKit'; import { BaiduConstants } from '../common/constants/BaiduConstants'; import { BaiduDeviceTokenResult, pollDeviceToken as pollBaiduDeviceToken, requestDeviceCode as requestBaiduDeviceCode } from '../common/network/BaiduPanClient'; import { webview } from '@kit.ArkWeb'; interface ParsedConnectionParts { protocol: string; username?: string; password?: string; host: string; port?: number; path?: string; } interface BaiduTokenResultShape { access_token?: string; refresh_token?: string; expires_in?: number; error?: string; error_description?: string; } // 工具函数:将 #RRGGBB 字符串和透明度转为 'rgba(r,g,b,a)' 字符串,深色模式下可返回深色变体 function themeColorWithAlpha(themeColor: string, alpha: number, isDarkMode: boolean = false): string { if (isDarkMode) { // 深色模式下返回更深的灰色或半透明黑色 return `rgba(34,34,34,${alpha + 0.2 > 1 ? 1 : alpha + 0.2})`; } const color = themeColor.replace('#', ''); const r = parseInt(color.substring(0, 2), 16); const g = parseInt(color.substring(2, 4), 16); const b = parseInt(color.substring(4, 6), 16); return `rgba(${r},${g},${b},${alpha})`; } // 网盘账户对话框 @Component export struct RemoteDriveAccountDialog { @Prop isEditMode: boolean = false; @Prop account: WebDavAccount; onConfirm?: (account: WebDavAccount) => void; onCancel?: () => void; @State accountName: string = '我的网盘'; @State host: string = ''; @State port: number = -1; @State filepath: string = '/'; @State username: string = ''; @State password: string = ''; @State enableHttps: boolean = false; @Prop initialDriveType: RemoteDriveType = RemoteDriveType.WebDav; @State driveType: RemoteDriveType = RemoteDriveType.WebDav; @State shareName: string = ''; @State domain: string = ''; @State navidromeBasePath: string = '/rest'; @State jellyfinBasePath: string = ''; @State embyBasePath: string = ''; @State coverPath: string = ''; @State isDarkMode: boolean = false; @State ftpEncoding: string = 'utf-8'; @State baiduAuthStatus: string = ''; @State baiduAccessToken: string = ''; @State baiduRefreshToken: string = ''; @State baiduTokenExpiresAt: number = 0; @State showBaiduAuthDialog: boolean = false; @State baiduAuthorizeUrl: string = ''; @State baiduAuthProgress: number = 0; @State baiduAuthMode: 'web' | 'device' = 'web'; @State baiduDeviceCode: string = ''; @State baiduDeviceUserCode: string = ''; @State baiduDeviceVerifyUrl: string = ''; @State baiduDeviceQrUrl: string = ''; @State baiduDeviceExpireAt: number = 0; @State baiduDeviceInterval: number = 5; @State baiduDevicePolling: boolean = false; @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR; @StorageProp('currentColorMode') @Watch('onColorModeChange') currentMode: number = ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT; @State portInput: string = ''; private nameCustomized: boolean = false; private portCustomized: boolean = false; private navBasePathCustomized: boolean = false; private jellyfinBasePathCustomized: boolean = false; private embyBasePathCustomized: boolean = false; private baiduAuthStateToken: string = ''; private baiduWebController: webview.WebviewController = new webview.WebviewController(); private baiduDevicePollTimer: number = 0; onColorModeChange() { this.isDarkMode = this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK } aboutToAppear(): void { console.info('heanup driveType = ' + this.driveType); this.isDarkMode = this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK; if (this.isEditMode) { this.accountName = this.account.name; this.host = this.account.host; this.updatePortState(this.account.port, true); this.filepath = this.account.filepath; this.username = this.account.account; this.password = this.account.password; this.enableHttps = this.account.enableHttps; this.driveType = this.account.webType ?? RemoteDriveType.WebDav; this.shareName = this.account.smbShare ?? ''; this.domain = this.account.smbDomain ?? ''; this.coverPath = this.account.coverPath || ''; this.ftpEncoding = this.account.ftpEncoding ?? 'utf-8'; this.baiduAccessToken = this.account.baiduAccessToken ?? ''; this.baiduRefreshToken = this.account.baiduRefreshToken ?? ''; this.baiduTokenExpiresAt = this.account.baiduTokenExpiresAt ?? 0; this.nameCustomized = true; if (this.driveType === RemoteDriveType.Navidrome) { this.navidromeBasePath = this.normalizeNavidromeBasePath(this.account.navidromeBasePath ?? '/rest'); this.navBasePathCustomized = true; } if (this.driveType === RemoteDriveType.Jellyfin) { this.jellyfinBasePath = this.normalizeJellyfinBasePath(this.account.jellyfinBasePath ?? ''); this.jellyfinBasePathCustomized = true; } if (this.driveType === RemoteDriveType.Emby) { this.embyBasePath = this.normalizeEmbyBasePath(this.account.embyBasePath ?? ''); this.embyBasePathCustomized = true; } Logger.info('heanup RemoteDriveAccountDialog', `编辑模式加载账户: ${this.accountName}, 封面路径: ${this.coverPath}`); if (this.coverPath) { Logger.info('heanup RemoteDriveAccountDialog', `封面文件是否存在: ${ImagePickerUtil.imageExists(this.coverPath)}`); } } else { this.driveType = this.initialDriveType ?? RemoteDriveType.WebDav; this.baiduAccessToken = ''; this.baiduRefreshToken = ''; this.baiduTokenExpiresAt = 0; this.jellyfinBasePathCustomized = false; this.navBasePathCustomized = false; this.handleDriveTypeChange(this.driveType, true); } } aboutToDisappear(): void { this.closeBaiduAuthDialog(); } build() { Stack() { Scroll() { this.contentBuilder(); } .height('100%') .width('100%') .scrollBar(BarState.Off); if (this.showBaiduAuthDialog) { this.buildBaiduAuthDialogOverlay(); } } .height('100%') .width('100%'); } @Builder contentBuilder() { Column({ space: 16 }) { // 标题 Text(this.isEditMode ? '编辑账户' : '添加账户') .fontSize(18) .fontWeight(FontWeight.Bold) .fontColor($r('app.color.index_tab_font_color')); // 类型选择 + 封面 // this.buildTypeSelector(); // 账户封面选择 Row({ space: 8 }) { Text('账户封面') .fontSize(14) .fontColor(this.isDarkMode ? '#EBEBF5' : '#666666') .fontWeight(FontWeight.Medium) .alignSelf(ItemAlign.Center); // 封面选择区域 Row({ space: 12 }) { // 封面预览 Stack() { if (this.coverPath) { Image(this.coverPath) .width(50) .height(50) .borderRadius(8) .objectFit(ImageFit.Cover); } else { // 默认封面图标 Column() { Image(getCloudDiskIcon(this.driveType)) .width(30) .height(30) } .width(50) .height(50) .backgroundColor(this.isDarkMode ? '#2C2C2E' : $r('app.color.input_background')) .borderRadius(8) .justifyContent(FlexAlign.Center); } } .onClick(() => { this.handleSelectCover(); }); // 操作按钮 Column({ space: 8 }) { Button('选择封面(可选)') .width(120) .height(36) .fontSize(11) .fontColor(this.isDarkMode ? '#FFFFFF' : $r('app.color.text_color')) .backgroundColor(this.isDarkMode ? '#2C2C2E' : $r('app.color.input_background')) .onClick(() => { this.handleSelectCover(); }); if (this.coverPath) { Button('移除封面') .width(120) .height(36) .fontSize(12) .fontColor(this.isDarkMode ? '#FF453A' : Color.Red) .backgroundColor(this.isDarkMode ? 'rgba(255,69,58,0.2)' : '#FFE5E5') .onClick(() => { this.handleRemoveCover(); }); } } .alignItems(HorizontalAlign.Start); } .width('100%') .justifyContent(FlexAlign.Start); } .width('100%') .alignItems(VerticalAlign.Center); // 账户名称 Row({ space: 8 }) { Text('名称') .fontSize(14) .fontColor($r('app.color.index_tab_font_color')); TextArea({ placeholder: '请输入账户名称', text: this.accountName }) .layoutWeight(1) .maxLines(1) .onChange((value: string) => { this.accountName = value; this.nameCustomized = true; }); } .width('100%') .alignItems(VerticalAlign.Center); if (this.driveType === RemoteDriveType.Baidu) { this.buildBaiduAuthSection(); } else { // 服务器地址 Row({ space: 8 }) { Text(this.getServerLabel()) .fontSize(14) .maxLines(2) .fontColor($r('app.color.index_tab_font_color')); TextArea({ placeholder: '服务器地址或连接串', text: this.host }) .layoutWeight(1) .onChange((value: string) => { const trimmed = value.trim(); if (trimmed.includes('://') && this.tryParseConnectionString(trimmed)) { return; } this.host = trimmed; }); } .alignItems(VerticalAlign.Center); this.buildHelperText(this.getServerHint()) // 端口 Row({ space: 8 }) { Text('端口') .fontSize(14) .fontColor($r('app.color.index_tab_font_color')); TextInput({ placeholder: this.getPortPlaceholder(), text: this.portInput }) .layoutWeight(1) .maxLines(1) .type(InputType.Number) .onChange((value: string) => { const trimmed = value.trim(); this.portInput = trimmed; const defaultPort = this.getDefaultPort(); if (trimmed.length === 0) { this.portCustomized = false; this.port = defaultPort; return; } const parsed = parseInt(trimmed); if (Number.isNaN(parsed) || parsed === -1) { return; } this.updatePortState(parsed, true, false); }); } .alignItems(VerticalAlign.Center); // this.buildHelperText('若留空将自动使用协议默认端口') if (this.driveType === RemoteDriveType.Smb) { // SMB 共享名称 Row({ space: 8 }) { Text('共享名称') .fontSize(14) .fontColor($r('app.color.index_tab_font_color')); TextInput({ placeholder: '共享名称', text: this.shareName }) .layoutWeight(1) .maxLines(1) .onChange((value: string) => { this.shareName = value; }); } .alignItems(VerticalAlign.Center) .visibility(Visibility.Visible) // this.buildHelperText('NAS 上共享根目录的名称,例如 music、share') Row({ space: 8 }) { Text('域/工作组') .fontSize(14) .fontColor($r('app.color.index_tab_font_color')); TextInput({ placeholder: '可选项', text: this.domain }) .layoutWeight(1) .maxLines(1) .onChange((value: string) => { this.domain = value; }); } .alignItems(VerticalAlign.Center) .visibility(Visibility.Visible) // this.buildHelperText('可选,用于需要域/工作组认证的 SMB 服务器') } if (this.driveType === RemoteDriveType.Ftp) { Row({ space: 8 }) { Text('编码') .fontSize(14) .fontColor($r('app.color.index_tab_font_color')); TextInput({ placeholder: '默认 utf-8,可填 gbk 等', text: this.ftpEncoding }) .layoutWeight(1) .maxLines(1) .onChange((value: string) => { this.ftpEncoding = value.trim(); }); } .alignItems(VerticalAlign.Center); this.buildHelperText('若目录或文件名为中文,可根据服务器设置调整编码'); } if (this.driveType === RemoteDriveType.Jellyfin) { Row({ space: 8 }) { Text('API路径') .fontSize(14) .fontColor($r('app.color.index_tab_font_color')); TextInput({ placeholder: '可选,如 /jellyfin', text: this.jellyfinBasePath }) .layoutWeight(1) .maxLines(1) .onChange((value: string) => { this.jellyfinBasePath = this.normalizeJellyfinBasePath(value); this.jellyfinBasePathCustomized = true; }); } .alignItems(VerticalAlign.Center); this.buildHelperText('Jellyfin 部署在子路径时填写,默认留空表示根路径'); } if (this.driveType === RemoteDriveType.Emby) { Row({ space: 8 }) { Text('API路径') .fontSize(14) .fontColor($r('app.color.index_tab_font_color')); TextInput({ placeholder: '可选,如 /emby', text: this.embyBasePath }) .layoutWeight(1) .maxLines(1) .onChange((value: string) => { this.embyBasePath = this.normalizeEmbyBasePath(value); this.embyBasePathCustomized = true; }); } .alignItems(VerticalAlign.Center); this.buildHelperText('Emby 部署在子路径时填写,默认留空表示根路径'); } // if (this.driveType === RemoteDriveType.Navidrome) { // Row({ space: 8 }) { // Text('API路径') // .fontSize(14) // .fontColor($r('app.color.index_tab_font_color')); // TextInput({ placeholder: '/rest', text: this.navidromeBasePath }) // .layoutWeight(1) // .maxLines(1) // .onChange((value: string) => { // this.navidromeBasePath = this.normalizeNavidromeBasePath(value); // this.navBasePathCustomized = true; // }); // } // .alignItems(VerticalAlign.Center); // this.buildHelperText('Navidrome/Subsonic REST 前缀,默认 /rest,可填 /navidrome/rest 等路径') // } // 文件目录 Row({ space: 8 }) { Text('文件目录') .fontSize(14) .fontColor($r('app.color.index_tab_font_color')); TextInput({ placeholder: '远程目录', text: this.filepath }) .layoutWeight(1) .maxLines(1) .onChange((value: string) => { this.filepath = value; }); } .alignItems(VerticalAlign.Center) .visibility(this.driveType === RemoteDriveType.Navidrome || this.driveType === RemoteDriveType.Jellyfin || this.driveType === RemoteDriveType.Emby ? Visibility.None : Visibility.Visible) // this.buildHelperText('从共享根开始的路径,例如 /music 或 /音乐/歌单1') Row({ space: 8 }) { Text('用户名') .fontSize(14) .fontColor($r('app.color.index_tab_font_color')); TextInput({ placeholder: '请输入用户名', text: this.username }) .layoutWeight(1) .maxLines(1) .onChange((value: string) => { this.username = value; }); } .alignItems(VerticalAlign.Center); Row({ space: 8 }) { Text('密码') .fontSize(14) .fontColor($r('app.color.index_tab_font_color')); TextInput({ placeholder: '请输入密码', text: this.password }) .type(InputType.Password) .layoutWeight(1) .maxLines(2) .onChange((value: string) => { this.password = value; }); } .alignItems(VerticalAlign.Center); if (this.driveType === RemoteDriveType.WebDav || this.driveType === RemoteDriveType.Jellyfin || this.driveType === RemoteDriveType.Emby) { Row({ space: 12 }) { Text('启用HTTPS') .fontSize(14) .fontColor($r('app.color.index_tab_font_color')); Toggle({ type: ToggleType.Switch, isOn: this.enableHttps }) .selectedColor(this.themeColor) .onChange((isOn: boolean) => { this.enableHttps = isOn; if (!this.portCustomized && (this.driveType === RemoteDriveType.WebDav || this.driveType === RemoteDriveType.Jellyfin || this.driveType === RemoteDriveType.Emby)) { this.updatePortState(this.getDefaultPort(), false); } }); } .width('100%') .justifyContent(FlexAlign.SpaceBetween) .alignItems(VerticalAlign.Center); } } // end non-baidu section // 按钮 Row({ space: 12 }) { Button('取消', { type: ButtonType.Capsule }) .backgroundColor($r('app.color.cancel_button_background')) .fontColor($r('app.color.cancel_button_text')) .layoutWeight(1) .onClick(() => { this.onCancel?.(); }); Button(this.isEditMode ? '保存' : '添加', { type: ButtonType.Capsule }) .backgroundColor(this.themeColor) .layoutWeight(1) .onClick(() => { const needHost = this.driveType !== RemoteDriveType.Baidu; if (!this.accountName || (needHost && !this.host)) { ToastUtil.showToast('请填写账户名称和服务器地址'); return; } if (this.driveType === RemoteDriveType.Baidu && !this.baiduAccessToken) { ToastUtil.showToast('请先完成百度网盘授权'); return; } const updatedAccount = new WebDavAccount(); if (this.isEditMode) { updatedAccount.id = this.account.id; } updatedAccount.name = this.accountName; updatedAccount.host = this.driveType === RemoteDriveType.Baidu ? 'pan.baidu.com' : this.host; updatedAccount.port = this.driveType === RemoteDriveType.Baidu ? 443 : this.port; updatedAccount.filepath = this.filepath; updatedAccount.account = this.driveType === RemoteDriveType.Baidu ? '' : this.username; updatedAccount.password = this.driveType === RemoteDriveType.Baidu ? '' : this.password; updatedAccount.enableHttps = this.enableHttps; updatedAccount.coverPath = this.coverPath; updatedAccount.isActivate = true; updatedAccount.localHost = ''; updatedAccount.isUseLocalHost = false; updatedAccount.lyricFilePath = ''; updatedAccount.uploadFilePath = ''; updatedAccount.imageFilePath = ''; updatedAccount.webType = this.driveType; updatedAccount.smbShare = this.shareName; updatedAccount.smbDomain = this.domain; updatedAccount.navidromeBasePath = this.normalizeNavidromeBasePath(this.navidromeBasePath); updatedAccount.jellyfinBasePath = this.normalizeJellyfinBasePath(this.jellyfinBasePath); updatedAccount.embyBasePath = this.normalizeEmbyBasePath(this.embyBasePath); updatedAccount.ftpEncoding = this.ftpEncoding && this.ftpEncoding.length > 0 ? this.ftpEncoding : 'utf-8'; updatedAccount.baiduAccessToken = this.baiduAccessToken; updatedAccount.baiduRefreshToken = this.baiduRefreshToken; updatedAccount.baiduTokenExpiresAt = this.baiduTokenExpiresAt; this.onConfirm?.(updatedAccount); }); } .width('100%') .margin({ top: 8 }); } .padding(24) .width('90%') .borderRadius(16); } @Builder private buildTypeSelector() { Column({ space: 8 }) { Text('协议类型') .fontSize(14) .fontColor($r('app.color.index_tab_font_color')) .fontWeight(FontWeight.Medium) .alignSelf(ItemAlign.Start); Row({ space: 12 }) { this.buildTypeButton('WebDAV', RemoteDriveType.WebDav); this.buildTypeButton('SMB', RemoteDriveType.Smb); this.buildTypeButton('Navidrome', RemoteDriveType.Navidrome); this.buildTypeButton('Jellyfin', RemoteDriveType.Jellyfin); this.buildTypeButton('Emby', RemoteDriveType.Emby); this.buildTypeButton('FTP', RemoteDriveType.Ftp); this.buildTypeButton('百度网盘', RemoteDriveType.Baidu); } .width('100%'); } .width('100%'); } @Builder private buildTypeButton(label: string, type: RemoteDriveType) { Button(label) .type(ButtonType.Capsule) .backgroundColor(this.driveType === type ? this.themeColor : (this.isDarkMode ? '#2C2C2E' : $r('app.color.input_background'))) .fontColor(this.driveType === type ? Color.White : $r('app.color.index_tab_font_color')) .onClick(() => { this.handleDriveTypeChange(type); }); } private handleDriveTypeChange(type: RemoteDriveType, forceApply: boolean = false) { const changed = this.driveType !== type; if (!changed && !forceApply) { return; } if (this.driveType === RemoteDriveType.Baidu && type !== RemoteDriveType.Baidu) { this.closeBaiduAuthDialog(); } this.driveType = type; if (type === RemoteDriveType.Smb || type === RemoteDriveType.Ftp) { this.enableHttps = false; } this.applyTypeDefaults(type, true); } private applyTypeDefaults(type: RemoteDriveType, forcePort: boolean = false) { if (!this.nameCustomized) { this.accountName = this.getDefaultAccountName(type); } if (type === RemoteDriveType.Navidrome && !this.navBasePathCustomized) { this.navidromeBasePath = '/rest'; } if (type === RemoteDriveType.Jellyfin && !this.jellyfinBasePathCustomized) { this.jellyfinBasePath = ''; } if (type === RemoteDriveType.Emby && !this.embyBasePathCustomized) { this.embyBasePath = ''; } if ((type === RemoteDriveType.Jellyfin || type === RemoteDriveType.Emby) && (!this.filepath || this.filepath.length === 0)) { this.filepath = '/'; } if (type === RemoteDriveType.Ftp && (!this.ftpEncoding || this.ftpEncoding.length === 0)) { this.ftpEncoding = 'utf-8'; } if (type === RemoteDriveType.Baidu) { this.enableHttps = true; this.host = 'pan.baidu.com'; this.username = ''; this.password = ''; this.shareName = ''; this.domain = ''; if (!this.filepath || this.filepath.length === 0) { this.filepath = '/'; } } this.updatePortForType(type, forcePort); } private updatePortForType(type: RemoteDriveType, force: boolean = false) { if (!force && this.portCustomized) { return; } this.updatePortState(this.getDefaultPort(), false); } private getDefaultAccountName(type: RemoteDriveType): string { switch (type) { case RemoteDriveType.Smb: return '新建SMB'; case RemoteDriveType.Navidrome: return '新建Navidrome'; case RemoteDriveType.Jellyfin: return '新建Jellyfin'; case RemoteDriveType.Emby: return '新建Emby'; case RemoteDriveType.Ftp: return '新建FTP'; case RemoteDriveType.Baidu: return '百度网盘'; default: return '新建WebDAV'; } } private getServerLabel(): string { return '服务器'; } private getServerHint(): string { switch (this.driveType) { case RemoteDriveType.Smb: return '支持 smb://user:pass@host/share 输入,自动拆分账户、共享、路径'; case RemoteDriveType.Navidrome: return '可粘贴 Navidrome/Subsonic 连接(如 https://user:pass@host:4533/rest),自动填充参数'; case RemoteDriveType.Jellyfin: return '可粘贴 Jellyfin 连接(如 https://user:pass@host:8096/jellyfin),自动填充参数'; case RemoteDriveType.Emby: return '可粘贴 Emby 连接(如 https://user:pass@host:8096/emby),自动填充参数'; case RemoteDriveType.Ftp: return '支持 ftp://user:pass@host:21/path 输入,自动填充账户和目录'; case RemoteDriveType.Baidu: return '百度网盘无需服务器地址,请使用下方按钮完成授权'; default: return '支持 https://user:pass@host:port/path WebDAV 连接串,自动填充账户、端口和目录'; } } private getPortPlaceholder(): string { return `默认: ${this.getDefaultPort()}`; } private getDefaultPort(): number { if (this.driveType === RemoteDriveType.Smb) { return 445; } if (this.driveType === RemoteDriveType.Navidrome) { return this.enableHttps ? 443 : 4533; } if (this.driveType === RemoteDriveType.Jellyfin) { return this.enableHttps ? 8920 : 8096; } if (this.driveType === RemoteDriveType.Emby) { return this.enableHttps ? 8920 : 8096; } if (this.driveType === RemoteDriveType.Ftp) { return 21; } if (this.driveType === RemoteDriveType.Baidu) { return 443; } return this.enableHttps ? 443 : 5005; } private updatePortState(value: number, customized: boolean, syncInput: boolean = true) { this.port = value; this.portCustomized = customized; if (syncInput) { this.portInput = value > 0 ? value.toString() : ''; } } @Builder private buildHelperText(text: string) { if (text && text.trim().length > 0) { Text(text) .fontSize(12) .fontColor(this.isDarkMode ? '#8E8E93' : '#999999') .opacity(0.9) .alignSelf(ItemAlign.Start) } } @Builder private buildFileDirectoryRow() { Row({ space: 8 }) { Text('文件目录') .fontSize(14) .fontColor($r('app.color.index_tab_font_color')); TextInput({ placeholder: '远程目录', text: this.filepath }) .layoutWeight(1) .maxLines(1) .onChange((value: string) => { this.filepath = value; }); } .alignItems(VerticalAlign.Center); } private tryParseConnectionString(input: string): boolean { const parsed = this.parseConnectionString(input); if (!parsed) { return false; } const protocol = parsed.protocol.toLowerCase(); if (protocol === 'smb' || protocol === 'cifs') { if (this.driveType !== RemoteDriveType.Smb) { this.handleDriveTypeChange(RemoteDriveType.Smb); } this.applyParsedSmb(parsed); ToastUtil.showToast('已解析 SMB 连接'); return true; } if (protocol === 'http' || protocol === 'https') { if (Number(this.driveType) === RemoteDriveType.Jellyfin) { this.applyParsedJellyfin(parsed); ToastUtil.showToast('已解析 Jellyfin 连接'); return true; } if (Number(this.driveType) === RemoteDriveType.Emby) { this.applyParsedEmby(parsed); ToastUtil.showToast('已解析 Emby 连接'); return true; } if (Number(this.driveType) === RemoteDriveType.Navidrome) { this.applyParsedNavidrome(parsed); ToastUtil.showToast('已解析 Navidrome 连接'); return true; } const looksNav = this.guessNavidromePath(parsed.path); if (looksNav) { if (Number(this.driveType) !== RemoteDriveType.Navidrome) { this.handleDriveTypeChange(RemoteDriveType.Navidrome); } this.applyParsedNavidrome(parsed); ToastUtil.showToast('已解析 Navidrome 连接'); return true; } const looksJellyfin = this.guessJellyfinPath(parsed.path); if (looksJellyfin) { if (Number(this.driveType) !== RemoteDriveType.Jellyfin) { this.handleDriveTypeChange(RemoteDriveType.Jellyfin); } this.applyParsedJellyfin(parsed); ToastUtil.showToast('已解析 Jellyfin 连接'); return true; } const looksEmby = this.guessEmbyPath(parsed.path); if (looksEmby) { if (Number(this.driveType) !== RemoteDriveType.Emby) { this.handleDriveTypeChange(RemoteDriveType.Emby); } this.applyParsedEmby(parsed); ToastUtil.showToast('已解析 Emby 连接'); return true; } if (this.driveType !== RemoteDriveType.WebDav) { this.handleDriveTypeChange(RemoteDriveType.WebDav); } this.applyParsedWebDav(parsed); ToastUtil.showToast('已解析 WebDAV 连接'); return true; } if (protocol === 'ftp' || protocol === 'ftps') { if (this.driveType !== RemoteDriveType.Ftp) { this.handleDriveTypeChange(RemoteDriveType.Ftp); } this.applyParsedFtp(parsed); ToastUtil.showToast('已解析 FTP 连接'); return true; } return false; } private applyParsedWebDav(parsed: ParsedConnectionParts): void { this.enableHttps = parsed.protocol === 'https'; this.host = parsed.host; if (parsed.port) { this.updatePortState(parsed.port, true); } else { this.updatePortForType(RemoteDriveType.WebDav, true); } if (parsed.username) { this.username = parsed.username; } if (parsed.password) { this.password = parsed.password; } this.filepath = parsed.path?.length ? parsed.path : '/'; } private applyParsedSmb(parsed: ParsedConnectionParts): void { this.host = parsed.host; if (parsed.port) { this.updatePortState(parsed.port, true); } else { this.updatePortForType(RemoteDriveType.Smb, true); } if (parsed.username) { this.username = parsed.username; } if (parsed.password) { this.password = parsed.password; } const segments = parsed.path?.replace(/^\/+/, '').split('/') ?? []; this.shareName = segments.length > 0 && segments[0].length > 0 ? segments[0] : this.shareName; const relative = segments.length > 1 ? segments.slice(1).join('/') : ''; this.filepath = relative ? `/${relative}` : '/'; } private applyParsedNavidrome(parsed: ParsedConnectionParts): void { this.enableHttps = parsed.protocol === 'https'; this.host = parsed.host; if (parsed.port) { this.updatePortState(parsed.port, true); } else { this.updatePortForType(RemoteDriveType.Navidrome, true); } if (parsed.username) { this.username = parsed.username; } if (parsed.password) { this.password = parsed.password; } this.navidromeBasePath = this.normalizeNavidromeBasePath(parsed.path ?? this.navidromeBasePath); this.navBasePathCustomized = true; this.filepath = '/'; } private applyParsedJellyfin(parsed: ParsedConnectionParts): void { this.enableHttps = parsed.protocol === 'https'; this.host = parsed.host; if (parsed.port) { this.updatePortState(parsed.port, true); } else { this.updatePortForType(RemoteDriveType.Jellyfin, true); } if (parsed.username) { this.username = parsed.username; } if (parsed.password) { this.password = parsed.password; } this.jellyfinBasePath = this.normalizeJellyfinBasePath(parsed.path ?? this.jellyfinBasePath); this.jellyfinBasePathCustomized = true; this.filepath = '/'; } private applyParsedEmby(parsed: ParsedConnectionParts): void { this.enableHttps = parsed.protocol === 'https'; this.host = parsed.host; if (parsed.port) { this.updatePortState(parsed.port, true); } else { this.updatePortForType(RemoteDriveType.Emby, true); } if (parsed.username) { this.username = parsed.username; } if (parsed.password) { this.password = parsed.password; } this.embyBasePath = this.normalizeEmbyBasePath(parsed.path ?? this.embyBasePath); this.embyBasePathCustomized = true; this.filepath = '/'; } private applyParsedFtp(parsed: ParsedConnectionParts): void { this.enableHttps = false; this.host = parsed.host; if (parsed.port) { this.updatePortState(parsed.port, true); } else { this.updatePortForType(RemoteDriveType.Ftp, true); } if (parsed.username) { this.username = parsed.username; } if (parsed.password) { this.password = parsed.password; } this.filepath = parsed.path && parsed.path.length > 0 ? parsed.path : '/'; } private guessNavidromePath(path?: string): boolean { if (!path) { return false; } const lower = path.toLowerCase(); return lower.includes('/rest') || lower.includes('navidrome'); } private guessJellyfinPath(path?: string): boolean { if (!path) { return false; } return path.toLowerCase().includes('jellyfin'); } private guessEmbyPath(path?: string): boolean { if (!path) { return false; } return path.toLowerCase().includes('emby'); } private normalizeNavidromeBasePath(value: string): string { if (!value || value.trim().length === 0) { return '/rest'; } let normalized = value.trim(); if (!normalized.startsWith('/')) { normalized = `/${normalized}`; } if (normalized.length > 1 && normalized.endsWith('/')) { normalized = normalized.slice(0, -1); } return normalized.length === 0 ? '/rest' : normalized; } private normalizeJellyfinBasePath(value: string): string { if (!value || value.trim().length === 0) { return ''; } let normalized = value.trim(); if (!normalized.startsWith('/')) { normalized = `/${normalized}`; } if (normalized.length > 1 && normalized.endsWith('/')) { normalized = normalized.slice(0, -1); } return normalized === '/' ? '' : normalized; } private normalizeEmbyBasePath(value: string): string { if (!value || value.trim().length === 0) { return ''; } let normalized = value.trim(); if (!normalized.startsWith('/')) { normalized = `/${normalized}`; } if (normalized.length > 1 && normalized.endsWith('/')) { normalized = normalized.slice(0, -1); } return normalized === '/' ? '' : normalized; } private parseConnectionString(input: string): ParsedConnectionParts | null { const pattern = /^([a-z][a-z0-9+\-.]*):\/\/(?:([^:@\/]+)(?::([^@\/]*))?@)?([^\/:]+)(?::(\d+))?(\/.*)?$/i; const match = input.match(pattern); if (!match) { Logger.warn('heanup RemoteDriveAccountDialog', `连接字符串解析失败: ${input}`); return null; } const protocol = match[1].toLowerCase(); const username = match[2] ? this.safeDecode(match[2]) : undefined; const password = match[3] ? this.safeDecode(match[3]) : undefined; const host = match[4]; const port = match[5] ? Number(match[5]) : undefined; const path = match[6] && match[6].length > 0 ? match[6] : undefined; return { protocol, username, password, host, port, path }; } private safeDecode(value: string): string { try { return decodeURIComponent(value); } catch (_error) { return value; } } @Builder private buildBaiduAuthSection() { Column({ space: 12 }) { Text('百度账号授权') .fontSize(14) .fontWeight(FontWeight.Medium) .fontColor($r('app.color.index_tab_font_color')) .alignSelf(ItemAlign.Start); Row({ space: 8 }) { Button('扫码授权') .type(ButtonType.Capsule) .backgroundColor(this.isDarkMode ? '#2C2C2E' : $r('app.color.input_background')) .fontColor($r('app.color.index_tab_font_color')) .onClick(() => { this.handleBaiduDeviceAuthorize(); }); Button('打开百度授权页') .type(ButtonType.Capsule) .backgroundColor(this.themeColor) .fontColor(Color.White) .onClick(() => { this.handleBaiduImplicitAuthorize(); }); if (this.baiduAccessToken) { Button('清除授权') .type(ButtonType.Capsule) .backgroundColor(this.isDarkMode ? '#2C2C2E' : $r('app.color.input_background')) .fontColor($r('app.color.index_tab_font_color')) .onClick(() => { this.baiduAccessToken = ''; this.baiduRefreshToken = ''; this.baiduTokenExpiresAt = 0; this.baiduAuthStatus = '已清除授权信息'; this.closeBaiduAuthDialog(); }); } } Text('可扫码授权或打开百度网盘授权网页,请登录并允许访问网盘文件。') .fontSize(13) .fontColor(this.isDarkMode ? '#C7C7CC' : '#666666') .maxLines(2); if (this.baiduAuthStatus) { Text(this.baiduAuthStatus) .fontSize(13) .fontColor(this.isDarkMode ? '#EBEBF5' : '#666666') .maxLines(2); } if (this.baiduAccessToken) { Text(`已获取 Access Token,${this.baiduTokenExpiresAt > 0 ? `将在 ${this.getBaiduExpireText()} 过期` : '有效期未知'}`) .fontSize(13) .fontColor(this.isDarkMode ? '#EBEBF5' : '#666666'); } } .width('100%'); } @Builder private buildBaiduAuthDialogOverlay() { Stack() { Column() .width('100%') .height('100%') .backgroundColor('rgba(0,0,0,0.5)') .onClick(() => { this.closeBaiduAuthDialog(); }); Column({ space: 8 }) { Row() { Text('百度网盘授权') .fontSize(16) .fontWeight(FontWeight.Medium) .fontColor(this.isDarkMode ? Color.White : Color.Black); Text('关闭') .fontSize(14) .fontColor(this.themeColor) .onClick(() => { this.closeBaiduAuthDialog(); }); } .width('100%') .justifyContent(FlexAlign.SpaceBetween) .alignItems(VerticalAlign.Center) .padding({ bottom: 4 }); this.buildBaiduAuthModeSwitch(); Divider() .color(this.isDarkMode ? '#3A3A3C' : '#E5E5EA'); if (this.baiduAuthMode === 'device') { this.buildBaiduDeviceAuthContent(); } else { if (this.baiduAuthProgress > 0 && this.baiduAuthProgress < 100) { Text(`页面加载中 ${this.baiduAuthProgress}%`) .fontSize(12) .fontColor(this.isDarkMode ? '#C7C7CC' : '#666666'); } Web({ src: this.baiduAuthorizeUrl, controller: this.baiduWebController }) .layoutWeight(1) .width('100%') .javaScriptAccess(true) .domStorageAccess(true) .onPageEnd((event) => { if (event && event.url) { this.handleBaiduAuthNavigation(event.url); } }) .onProgressChange((event) => { if (event) { this.baiduAuthProgress = event.newProgress; } }); } } .width('92%') .height('85%') .padding(16) .backgroundColor(this.isDarkMode ? '#1C1C1E' : Color.White) .borderRadius(16); } .width('100%') .height('100%'); } @Builder private buildBaiduAuthModeSwitch() { Row({ space: 8 }) { this.buildBaiduAuthModeChip('网页登录', 'web'); this.buildBaiduAuthModeChip('扫码登录', 'device'); } .width('100%') .padding({ top: 4, bottom: 4 }); } @Builder private buildBaiduAuthModeChip(label: string, mode: 'web' | 'device') { Button(label) .type(ButtonType.Capsule) .backgroundColor(this.baiduAuthMode === mode ? this.themeColor : (this.isDarkMode ? '#2C2C2E' : $r('app.color.input_background'))) .fontColor(this.baiduAuthMode === mode ? Color.White : $r('app.color.index_tab_font_color')) .onClick(() => { this.switchBaiduAuthMode(mode); }); } @Builder private buildBaiduDeviceAuthContent() { Column({ space: 12 }) { if (this.baiduDeviceQrUrl) { Image(this.baiduDeviceQrUrl) .width(220) .height(220) .borderRadius(12) .objectFit(ImageFit.Contain) .alignSelf(ItemAlign.Center); } else { Column() { Text('正在获取二维码...') .fontColor(this.isDarkMode ? '#C7C7CC' : '#666666'); } .width(220) .height(220) .alignSelf(ItemAlign.Center) .backgroundColor(this.isDarkMode ? '#2C2C2E' : $r('app.color.input_background')) .borderRadius(12) .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center); } if (this.baiduDeviceUserCode) { Text(`设备码: ${this.baiduDeviceUserCode}`) .fontSize(14) .fontWeight(FontWeight.Medium) .fontColor($r('app.color.index_tab_font_color')); } if (this.baiduDeviceVerifyUrl) { Text(`或在浏览器访问 ${this.baiduDeviceVerifyUrl} 输入设备码完成授权`) .fontSize(12) .fontColor(this.isDarkMode ? '#C7C7CC' : '#666666') .maxLines(2); } if (this.baiduDeviceExpireAt) { Text(`二维码将于 ${this.getBaiduDeviceRemainText()} 过期`) .fontSize(12) .fontColor(this.isDarkMode ? '#C7C7CC' : '#666666'); } if (this.baiduAuthStatus) { Text(this.baiduAuthStatus) .fontSize(13) .fontColor(this.isDarkMode ? '#EBEBF5' : '#666666') .maxLines(2); } Row({ space: 12 }) { Button('刷新二维码') .type(ButtonType.Capsule) .backgroundColor(this.themeColor) .fontColor(Color.White) .onClick(() => { this.handleBaiduDeviceAuthorize(); }); Button('改用网页登录') .type(ButtonType.Capsule) .backgroundColor(this.isDarkMode ? '#2C2C2E' : $r('app.color.input_background')) .fontColor($r('app.color.index_tab_font_color')) .onClick(() => { this.handleBaiduImplicitAuthorize(); }); } .width('100%') .justifyContent(FlexAlign.Start); } .layoutWeight(1) .width('100%'); } private getBaiduExpireText(): string { if (!this.baiduTokenExpiresAt) { return ''; } const remain = Math.max(0, this.baiduTokenExpiresAt - Date.now()); const hours = Math.floor(remain / 3600000); const minutes = Math.floor((remain % 3600000) / 60000); return hours > 0 ? `${hours}小时${minutes}分后` : `${minutes}分钟后`; } private switchBaiduAuthMode(mode: 'web' | 'device'): void { if (mode === this.baiduAuthMode && this.showBaiduAuthDialog) { return; } if (mode === 'device') { this.handleBaiduDeviceAuthorize(); } else { this.handleBaiduImplicitAuthorize(); } } private async handleBaiduDeviceAuthorize(): Promise { this.stopBaiduDevicePolling(); this.baiduAuthMode = 'device'; this.resetBaiduDeviceState(false); this.baiduAuthorizeUrl = ''; this.baiduAuthProgress = 0; this.baiduAuthStateToken = ''; this.baiduAuthStatus = '正在获取扫码授权信息...'; try { const codeInfo = await requestBaiduDeviceCode(); this.baiduDeviceCode = codeInfo.device_code; this.baiduDeviceUserCode = codeInfo.user_code; this.baiduDeviceVerifyUrl = codeInfo.verification_url; this.baiduDeviceQrUrl = codeInfo.qrcode_url ?? ''; this.baiduDeviceExpireAt = Date.now() + (codeInfo.expires_in ?? 0) * 1000; this.baiduDeviceInterval = Math.max(3, codeInfo.interval ?? 5); this.baiduAuthStatus = '请在百度网盘App中扫码确认或输入设备码授权'; this.showBaiduAuthDialog = true; this.startBaiduDevicePolling(); } catch (error) { const message = (error as Error).message ?? '请求失败'; this.baiduAuthStatus = `获取设备码失败: ${message}`; ToastUtil.showToast('获取百度扫码授权失败'); } } private startBaiduDevicePolling(): void { this.stopBaiduDevicePolling(); if (!this.baiduDeviceCode) { return; } const intervalMs = Math.max(3000, this.baiduDeviceInterval * 1000); this.baiduDevicePollTimer = setInterval(() => { this.queryBaiduDeviceToken(); }, intervalMs) as number; } private stopBaiduDevicePolling(): void { if (this.baiduDevicePollTimer) { clearInterval(this.baiduDevicePollTimer); this.baiduDevicePollTimer = 0; } this.baiduDevicePolling = false; } private async queryBaiduDeviceToken(): Promise { if (!this.baiduDeviceCode || this.baiduDevicePolling) { return; } this.baiduDevicePolling = true; try { const result: BaiduDeviceTokenResult = await pollBaiduDeviceToken(this.baiduDeviceCode); const tokenResult = result as BaiduTokenResultShape; if (tokenResult.access_token !== undefined && tokenResult.access_token.length > 0) { this.baiduAccessToken = tokenResult.access_token; this.baiduRefreshToken = tokenResult.refresh_token ?? ''; this.baiduTokenExpiresAt = tokenResult.expires_in ? Date.now() + tokenResult.expires_in * 1000 : 0; this.baiduAuthStatus = '授权成功,可保存账户'; ToastUtil.showToast('百度授权成功'); this.closeBaiduAuthDialog(); return; } const errorResult = result as BaiduTokenResultShape; if (errorResult.error === undefined) { return; } switch (errorResult.error) { case 'authorization_pending': this.baiduAuthStatus = '请在百度网盘App扫码并确认'; break; case 'slow_down': this.baiduAuthStatus = '请求过于频繁,稍后自动重试'; this.baiduDeviceInterval += 2; this.startBaiduDevicePolling(); break; case 'expired_token': case 'invalid_grant': this.baiduAuthStatus = '二维码已过期,请刷新重新扫码'; this.stopBaiduDevicePolling(); break; default: this.baiduAuthStatus = `授权失败: ${errorResult.error_description ?? errorResult.error}`; this.stopBaiduDevicePolling(); break; } } catch (error) { const message = (error as Error).message ?? '未知错误'; this.baiduAuthStatus = `授权轮询失败: ${message}`; } finally { this.baiduDevicePolling = false; } } private getBaiduDeviceRemainText(): string { if (!this.baiduDeviceExpireAt) { return '稍后'; } const remain = Math.max(0, this.baiduDeviceExpireAt - Date.now()); const minutes = Math.floor(remain / 60000); const seconds = Math.floor((remain % 60000) / 1000); return minutes > 0 ? `${minutes}分${seconds}秒后` : `${seconds}秒后`; } private resetBaiduDeviceState(resetMode: boolean = true): void { if (resetMode) { this.baiduAuthMode = 'web'; } this.baiduDeviceCode = ''; this.baiduDeviceUserCode = ''; this.baiduDeviceVerifyUrl = ''; this.baiduDeviceQrUrl = ''; this.baiduDeviceExpireAt = 0; this.baiduDeviceInterval = 5; this.baiduDevicePolling = false; } private handleBaiduImplicitAuthorize(): void { this.stopBaiduDevicePolling(); this.resetBaiduDeviceState(false); this.baiduAuthMode = 'web'; this.baiduAuthStatus = '正在打开百度授权页面...'; this.baiduAuthStateToken = `${Date.now()}`; this.baiduAuthProgress = 0; this.baiduAuthorizeUrl = this.buildBaiduAuthorizeUrl(this.baiduAuthStateToken); this.showBaiduAuthDialog = true; } private closeBaiduAuthDialog(): void { this.showBaiduAuthDialog = false; this.baiduAuthorizeUrl = ''; this.baiduAuthProgress = 0; this.baiduAuthStateToken = ''; this.stopBaiduDevicePolling(); this.resetBaiduDeviceState(); } private buildBaiduAuthorizeUrl(state: string): string { const params = [ `response_type=token`, `client_id=${encodeURIComponent(BaiduConstants.APP_KEY)}`, `redirect_uri=${encodeURIComponent(BaiduConstants.OOB_REDIRECT_PARAM)}`, `scope=${encodeURIComponent(BaiduConstants.AUTH_SCOPE)}`, `display=mobile`, `state=${encodeURIComponent(state)}` ]; return `${BaiduConstants.AUTHORIZE_URL}?${params.join('&')}`; } private handleBaiduAuthNavigation(url: string): void { if (!url) { return; } const loginSuccessHttps = BaiduConstants.LOGIN_SUCCESS_URL.replace('http://', 'https://'); if (!url.startsWith(BaiduConstants.LOGIN_SUCCESS_URL) && !url.startsWith(loginSuccessHttps)) { return; } const fragmentIndex = url.indexOf('#'); const fragment = fragmentIndex >= 0 ? url.substring(fragmentIndex + 1) : ''; if (!fragment) { return; } const params = this.parseAuthFragment(fragment); const responseState = params.get('state') ?? ''; if (this.baiduAuthStateToken && responseState && responseState !== this.baiduAuthStateToken) { this.baiduAuthStatus = '授权状态校验失败,请重试'; this.closeBaiduAuthDialog(); return; } const error = params.get('error'); if (error) { const description = params.get('error_description') ?? error; this.baiduAuthStatus = `授权失败: ${description}`; this.closeBaiduAuthDialog(); return; } const accessToken = params.get('access_token'); if (!accessToken) { return; } const expiresIn = Number(params.get('expires_in') ?? '0'); this.baiduAccessToken = accessToken; this.baiduRefreshToken = ''; this.baiduTokenExpiresAt = expiresIn > 0 ? Date.now() + expiresIn * 1000 : 0; this.baiduAuthStatus = '授权成功,可保存账户'; ToastUtil.showToast('百度授权成功'); this.closeBaiduAuthDialog(); } private parseAuthFragment(fragment: string): Map { const params = new Map(); const pairs = fragment.split('&'); for (let i = 0; i < pairs.length; i++) { const pair = pairs[i]; if (!pair || pair.length === 0) { continue; } const separator = pair.indexOf('='); const key = separator >= 0 ? pair.substring(0, separator) : pair; const value = separator >= 0 ? pair.substring(separator + 1) : ''; params.set(decodeURIComponent(key), decodeURIComponent(value)); } return params; } /** * 处理选择封面 */ private async handleSelectCover() { try { // 需要获取上下文,这里通过全局上下文获取 const context = getContext(this) as Context if (!context) { ToastUtil.showToast('获取应用上下文失败') return } const selectedPath = await ImagePickerUtil.selectSingleWebDavCover(context) if (selectedPath) { // 如果之前有封面,删除旧封面 if (this.coverPath && this.coverPath !== this.account.coverPath) { ImagePickerUtil.deleteImage(this.coverPath) } this.coverPath = selectedPath Logger.info('heanup RemoteDriveAccountDialog', `选择封面成功: ${selectedPath}`) } } catch (error) { Logger.error('heanup RemoteDriveAccountDialog', `选择封面失败: ${(error as Error).message}`) ToastUtil.showToast('选择封面失败') } } /** * 处理移除封面 */ private handleRemoveCover() { if (this.coverPath) { if(!this.account){ this.coverPath = '' return } // 只有当封面不是原来的封面时才删除文件 if (this.coverPath !== this.account.coverPath) { ImagePickerUtil.deleteImage(this.coverPath) } this.coverPath = '' Logger.info('heanup RemoteDriveAccountDialog', '移除封面成功') } } } export function getCloudDiskIcon(type: number): ResourceStr { switch ( type) { case RemoteDriveType.WebDav: return $r('app.media.webdav'); case RemoteDriveType.Smb: return $r('app.media.smb'); case RemoteDriveType.Ftp: return $r('app.media.ftp'); case RemoteDriveType.Navidrome: return $r('app.media.navidrome'); case RemoteDriveType.Jellyfin: return $r('app.media.jellyfin'); case RemoteDriveType.Emby: return $r('app.media.emby'); case RemoteDriveType.Baidu: return $r('app.media.baiduwp'); case RemoteDriveType.ALi: return $r('app.media.ali_disk'); case RemoteDriveType.DISK_123: return $r('app.media.ali_disk'); } return $r('app.media.cloudDisk'); }