|
|
@@ -6,7 +6,7 @@ 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 { BaiduTokenError, BaiduTokenResponse, pollAccessToken, requestDeviceCode } from '../common/network/BaiduPanClient';
|
|
|
+import { webview } from '@kit.ArkWeb';
|
|
|
|
|
|
interface ParsedConnectionParts {
|
|
|
protocol: string;
|
|
|
@@ -52,13 +52,13 @@ export struct RemoteDriveAccountDialog {
|
|
|
@State coverPath: string = '';
|
|
|
@State isDarkMode: boolean = false;
|
|
|
@State ftpEncoding: string = 'utf-8';
|
|
|
- @State baiduUserCode: string = '';
|
|
|
- @State baiduQrUrl: string = '';
|
|
|
- @State baiduVerificationUrl: string = '';
|
|
|
@State baiduAuthStatus: string = '';
|
|
|
@State baiduAccessToken: string = '';
|
|
|
@State baiduRefreshToken: string = '';
|
|
|
@State baiduTokenExpiresAt: number = 0;
|
|
|
+ @State showBaiduAuthDialog: boolean = false;
|
|
|
+ @State baiduAuthorizeUrl: string = '';
|
|
|
+ @State baiduAuthProgress: number = 0;
|
|
|
@StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
|
|
|
@StorageProp('currentColorMode') @Watch('onColorModeChange') currentMode: number =
|
|
|
ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT;
|
|
|
@@ -67,9 +67,8 @@ export struct RemoteDriveAccountDialog {
|
|
|
private nameCustomized: boolean = false;
|
|
|
private portCustomized: boolean = false;
|
|
|
private navBasePathCustomized: boolean = false;
|
|
|
- private baiduDeviceCode?: string;
|
|
|
- private baiduDeviceCodeExpireAt: number = 0;
|
|
|
- private baiduPollingTimer?: number;
|
|
|
+ private baiduAuthStateToken: string = '';
|
|
|
+ private baiduWebController: webview.WebviewController = new webview.WebviewController();
|
|
|
|
|
|
onColorModeChange() {
|
|
|
this.isDarkMode = this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK
|
|
|
@@ -113,17 +112,24 @@ export struct RemoteDriveAccountDialog {
|
|
|
}
|
|
|
|
|
|
aboutToDisappear(): void {
|
|
|
- this.stopBaiduPolling();
|
|
|
+ this.closeBaiduAuthDialog();
|
|
|
}
|
|
|
|
|
|
build() {
|
|
|
- Scroll(){
|
|
|
- this.contentBuilder()
|
|
|
+ Stack() {
|
|
|
+ Scroll() {
|
|
|
+ this.contentBuilder();
|
|
|
+ }
|
|
|
+ .height('100%')
|
|
|
+ .width('100%')
|
|
|
+ .scrollBar(BarState.Off);
|
|
|
+
|
|
|
+ if (this.showBaiduAuthDialog) {
|
|
|
+ this.buildBaiduAuthDialogOverlay();
|
|
|
+ }
|
|
|
}
|
|
|
.height('100%')
|
|
|
- .width('100%')
|
|
|
- .scrollBar(BarState.Off)
|
|
|
-
|
|
|
+ .width('100%');
|
|
|
}
|
|
|
|
|
|
@Builder
|
|
|
@@ -355,53 +361,54 @@ export struct RemoteDriveAccountDialog {
|
|
|
.visibility(this.driveType === RemoteDriveType.Navidrome? Visibility.None:Visibility.Visible)
|
|
|
// this.buildHelperText('从共享根开始的路径,例如 /music 或 /音乐/歌单1')
|
|
|
|
|
|
- if (this.driveType !== RemoteDriveType.Baidu) {
|
|
|
- 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.username })
|
|
|
+ .layoutWeight(1)
|
|
|
+ .maxLines(1)
|
|
|
+ .onChange((value: string) => {
|
|
|
+ this.username = value;
|
|
|
+ });
|
|
|
+ }
|
|
|
+ .alignItems(VerticalAlign.Center);
|
|
|
|
|
|
- Row({ space: 8 }) {
|
|
|
- Text('密码')
|
|
|
+ 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) {
|
|
|
+ Row({ space: 12 }) {
|
|
|
+ Text('启用HTTPS')
|
|
|
.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;
|
|
|
+ Toggle({ type: ToggleType.Switch, isOn: this.enableHttps })
|
|
|
+ .selectedColor(this.themeColor)
|
|
|
+ .onChange((isOn: boolean) => {
|
|
|
+ this.enableHttps = isOn;
|
|
|
+ if (!this.portCustomized && this.driveType === RemoteDriveType.WebDav) {
|
|
|
+ this.updatePortState(this.getDefaultPort(), false);
|
|
|
+ }
|
|
|
});
|
|
|
}
|
|
|
+ .width('100%')
|
|
|
+ .justifyContent(FlexAlign.SpaceBetween)
|
|
|
.alignItems(VerticalAlign.Center);
|
|
|
-
|
|
|
- if (this.driveType === RemoteDriveType.WebDav) {
|
|
|
- Row() {
|
|
|
- Text('启用HTTPS')
|
|
|
- .fontSize(14)
|
|
|
- .fontColor($r('app.color.index_tab_font_color'));
|
|
|
- Blank();
|
|
|
- Toggle({ type: ToggleType.Switch, isOn: this.enableHttps })
|
|
|
- .selectedColor(this.themeColor)
|
|
|
- .onChange((isOn: boolean) => {
|
|
|
- this.enableHttps = isOn;
|
|
|
- if (!this.portCustomized && this.driveType === RemoteDriveType.WebDav) {
|
|
|
- this.updatePortState(this.getDefaultPort(), false);
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
- .width('100%');
|
|
|
- }
|
|
|
}
|
|
|
|
|
|
+ } // end non-baidu section
|
|
|
+
|
|
|
// 按钮
|
|
|
Row({ space: 12 }) {
|
|
|
Button('取消', { type: ButtonType.Capsule })
|
|
|
@@ -461,7 +468,7 @@ export struct RemoteDriveAccountDialog {
|
|
|
}
|
|
|
.padding(24)
|
|
|
.width('90%')
|
|
|
- .borderRadius(16)
|
|
|
+ .borderRadius(16);
|
|
|
}
|
|
|
|
|
|
@Builder
|
|
|
@@ -501,7 +508,7 @@ export struct RemoteDriveAccountDialog {
|
|
|
return;
|
|
|
}
|
|
|
if (this.driveType === RemoteDriveType.Baidu && type !== RemoteDriveType.Baidu) {
|
|
|
- this.stopBaiduPolling();
|
|
|
+ this.closeBaiduAuthDialog();
|
|
|
}
|
|
|
this.driveType = type;
|
|
|
if (type === RemoteDriveType.Smb || type === RemoteDriveType.Ftp) {
|
|
|
@@ -800,12 +807,12 @@ export struct RemoteDriveAccountDialog {
|
|
|
.alignSelf(ItemAlign.Start);
|
|
|
|
|
|
Row({ space: 8 }) {
|
|
|
- Button(this.baiduDeviceCode ? '刷新二维码' : '生成授权二维码')
|
|
|
+ Button('打开百度授权页')
|
|
|
.type(ButtonType.Capsule)
|
|
|
.backgroundColor(this.themeColor)
|
|
|
.fontColor(Color.White)
|
|
|
.onClick(() => {
|
|
|
- this.handleBaiduDeviceCodeRequest();
|
|
|
+ this.handleBaiduImplicitAuthorize();
|
|
|
});
|
|
|
if (this.baiduAccessToken) {
|
|
|
Button('清除授权')
|
|
|
@@ -817,33 +824,15 @@ export struct RemoteDriveAccountDialog {
|
|
|
this.baiduRefreshToken = '';
|
|
|
this.baiduTokenExpiresAt = 0;
|
|
|
this.baiduAuthStatus = '已清除授权信息';
|
|
|
- this.stopBaiduPolling();
|
|
|
+ this.closeBaiduAuthDialog();
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- if (this.baiduQrUrl) {
|
|
|
- Image(this.baiduQrUrl)
|
|
|
- .width(180)
|
|
|
- .height(180)
|
|
|
- .objectFit(ImageFit.Contain)
|
|
|
- .backgroundColor(this.isDarkMode ? '#1C1C1E' : '#F2F2F7')
|
|
|
- .borderRadius(12);
|
|
|
- }
|
|
|
-
|
|
|
- if (this.baiduUserCode) {
|
|
|
- Text(`用户码: ${this.baiduUserCode}`)
|
|
|
- .fontSize(18)
|
|
|
- .fontWeight(FontWeight.Bold)
|
|
|
- .fontColor(this.themeColor);
|
|
|
- }
|
|
|
-
|
|
|
- if (this.baiduVerificationUrl) {
|
|
|
- Text(`也可访问 ${this.baiduVerificationUrl} 输入用户码完成授权`)
|
|
|
- .fontSize(13)
|
|
|
- .fontColor($r('app.color.index_tab_font_color'))
|
|
|
- .maxLines(2);
|
|
|
- }
|
|
|
+ Text('系统将打开百度网盘授权网页,请登录并允许访问网盘文件。')
|
|
|
+ .fontSize(13)
|
|
|
+ .fontColor(this.isDarkMode ? '#C7C7CC' : '#666666')
|
|
|
+ .maxLines(2);
|
|
|
|
|
|
if (this.baiduAuthStatus) {
|
|
|
Text(this.baiduAuthStatus)
|
|
|
@@ -861,6 +850,73 @@ export struct RemoteDriveAccountDialog {
|
|
|
.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 });
|
|
|
+
|
|
|
+ Divider()
|
|
|
+ .color(this.isDarkMode ? '#3A3A3C' : '#E5E5EA');
|
|
|
+
|
|
|
+ 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%');
|
|
|
+ }
|
|
|
+
|
|
|
private getBaiduExpireText(): string {
|
|
|
if (!this.baiduTokenExpiresAt) {
|
|
|
return '';
|
|
|
@@ -871,72 +927,87 @@ export struct RemoteDriveAccountDialog {
|
|
|
return hours > 0 ? `${hours}小时${minutes}分后` : `${minutes}分钟后`;
|
|
|
}
|
|
|
|
|
|
- private async handleBaiduDeviceCodeRequest(): Promise<void> {
|
|
|
- try {
|
|
|
- this.baiduAuthStatus = '正在获取设备码...';
|
|
|
- const response = await requestDeviceCode();
|
|
|
- this.baiduDeviceCode = response.device_code;
|
|
|
- this.baiduUserCode = response.user_code;
|
|
|
- this.baiduVerificationUrl = response.verification_url;
|
|
|
- this.baiduQrUrl = response.qrcode_url;
|
|
|
- this.baiduDeviceCodeExpireAt = Date.now() + response.expires_in * 1000;
|
|
|
- this.baiduAuthStatus = '请使用百度网盘或百度APP扫码授权';
|
|
|
- this.startBaiduPolling(response.device_code, response.interval);
|
|
|
- } catch (error) {
|
|
|
- this.baiduAuthStatus = `获取设备码失败: ${(error as Error).message}`;
|
|
|
- }
|
|
|
+ private handleBaiduImplicitAuthorize(): void {
|
|
|
+ this.baiduAuthStatus = '正在打开百度授权页面...';
|
|
|
+ this.baiduAuthStateToken = `${Date.now()}`;
|
|
|
+ this.baiduAuthProgress = 0;
|
|
|
+ this.baiduAuthorizeUrl = this.buildBaiduAuthorizeUrl(this.baiduAuthStateToken);
|
|
|
+ this.showBaiduAuthDialog = true;
|
|
|
}
|
|
|
|
|
|
- private startBaiduPolling(deviceCode: string, intervalSeconds: number): void {
|
|
|
- this.stopBaiduPolling();
|
|
|
- const interval = Math.max(intervalSeconds || BaiduConstants.DEFAULT_POLL_INTERVAL, BaiduConstants.DEFAULT_POLL_INTERVAL) * 1000;
|
|
|
- const timerId = setInterval(() => {
|
|
|
- this.pollBaiduAuthorization(deviceCode).catch((err: Error) => {
|
|
|
- Logger.error('RemoteDriveAccountDialog', `poll baidu auth failed: ${err.message}`);
|
|
|
- });
|
|
|
- }, interval);
|
|
|
- this.baiduPollingTimer = timerId as number;
|
|
|
+ private closeBaiduAuthDialog(): void {
|
|
|
+ this.showBaiduAuthDialog = false;
|
|
|
+ this.baiduAuthorizeUrl = '';
|
|
|
+ this.baiduAuthProgress = 0;
|
|
|
+ this.baiduAuthStateToken = '';
|
|
|
}
|
|
|
|
|
|
- private stopBaiduPolling(): void {
|
|
|
- if (this.baiduPollingTimer !== undefined) {
|
|
|
- clearInterval(this.baiduPollingTimer);
|
|
|
- this.baiduPollingTimer = undefined;
|
|
|
- }
|
|
|
+ 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 async pollBaiduAuthorization(deviceCode: string): Promise<void> {
|
|
|
- if (!this.baiduDeviceCode || Date.now() > this.baiduDeviceCodeExpireAt) {
|
|
|
- this.stopBaiduPolling();
|
|
|
- this.baiduAuthStatus = '二维码已过期,请重新获取';
|
|
|
+ private handleBaiduAuthNavigation(url: string): void {
|
|
|
+ if (!url) {
|
|
|
return;
|
|
|
}
|
|
|
- const result = await pollAccessToken(deviceCode);
|
|
|
- if ((result as BaiduTokenError).error) {
|
|
|
- const errorResult = result as BaiduTokenError;
|
|
|
- switch (errorResult.error) {
|
|
|
- case 'authorization_pending':
|
|
|
- this.baiduAuthStatus = '等待用户确认授权...';
|
|
|
- return;
|
|
|
- case 'slow_down':
|
|
|
- this.baiduAuthStatus = '授权中,请稍候...';
|
|
|
- return;
|
|
|
- case 'expired_token':
|
|
|
- this.stopBaiduPolling();
|
|
|
- this.baiduAuthStatus = '设备码已过期,请重新获取';
|
|
|
- return;
|
|
|
- default:
|
|
|
- this.stopBaiduPolling();
|
|
|
- this.baiduAuthStatus = `授权失败: ${errorResult.error}`;
|
|
|
- return;
|
|
|
- }
|
|
|
+ const loginSuccessHttps = BaiduConstants.LOGIN_SUCCESS_URL.replace('http://', 'https://');
|
|
|
+ if (!url.startsWith(BaiduConstants.LOGIN_SUCCESS_URL) && !url.startsWith(loginSuccessHttps)) {
|
|
|
+ return;
|
|
|
}
|
|
|
- const token = result as BaiduTokenResponse;
|
|
|
- this.baiduAccessToken = token.access_token;
|
|
|
- this.baiduRefreshToken = token.refresh_token ?? this.baiduRefreshToken;
|
|
|
- this.baiduTokenExpiresAt = token.expires_in ? Date.now() + token.expires_in * 1000 : 0;
|
|
|
+ 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 = '授权成功,可保存账户';
|
|
|
- this.stopBaiduPolling();
|
|
|
+ ToastUtil.showToast('百度授权成功');
|
|
|
+ this.closeBaiduAuthDialog();
|
|
|
+ }
|
|
|
+
|
|
|
+ private parseAuthFragment(fragment: string): Map<string, string> {
|
|
|
+ const params = new Map<string, string>();
|
|
|
+ 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;
|
|
|
}
|
|
|
|
|
|
/**
|