ソースを参照

支持百度网盘的不同登录方式

chendeben 8 ヶ月 前
コミット
e892dfada7

+ 13 - 9
entry/src/main/ets/ListView/IjkPlayer.ets

@@ -57,21 +57,25 @@ export class IjkPlayer extends BasePlayer {
       }
       ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "enable-accurate-seek", "1");
 
-      ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max-buffer-size", "102400");
-      ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "min-frames", "100");
+      // 缓冲策略优化(针对网络流媒体)
+      ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max-buffer-size", "512000");
+      ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "min-frames", "25");
       ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", "1");
-      ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", "0");
+      ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", "1");
       ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", "5");
-      ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max_cached_duration", "3000");
+      ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max_cached_duration", "5000");
       ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", "1");
+      // 网络流优化
+      ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", "1000000");
+      ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", "524288");
       //屏幕常亮
       ijkMediaPlayer.setScreenOnWhilePlaying(true);
-      //设置超时
-      ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", "10000000");
-      ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "connect_timeout", "10000000");
-      ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "listen_timeout", "10000000");
+      //设置超时(单位:微秒,优化移动网络下的播放响应速度)
+      ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", "30000000");
+      ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "connect_timeout", "15000000");
+      ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "listen_timeout", "15000000");
       ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "addrinfo_timeout", "10000000");
-      ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_timeout", "10000000");
+      ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_timeout", "60000000");
 
       let listener = this.playListener;
 

+ 1 - 0
entry/src/main/ets/common/constants/BaiduConstants.ets

@@ -5,6 +5,7 @@ export class BaiduConstants {
   static readonly SIGN_KEY: string = '1~ml7QjPapdU$jSLR0Bop2rC3V9TyV5v';
   static readonly USER_AGENT: string = 'pan.baidu.com';
   static readonly AUTHORIZE_URL: string = 'https://openapi.baidu.com/oauth/2.0/authorize';
+  static readonly DEVICE_CODE_URL: string = 'https://openapi.baidu.com/oauth/2.0/device/code';
   static readonly TOKEN_URL: string = 'https://openapi.baidu.com/oauth/2.0/token';
   static readonly OPENAPI_BASE: string = 'https://openapi.baidu.com';
   static readonly PAN_BASE: string = 'https://pan.baidu.com';

+ 39 - 0
entry/src/main/ets/common/network/BaiduPanClient.ets

@@ -68,6 +68,17 @@ export interface BaiduTokenError {
   error_description?: string;
 }
 
+export interface BaiduDeviceCodeResponse {
+  device_code: string;
+  user_code: string;
+  verification_url: string;
+  qrcode_url?: string;
+  expires_in: number;
+  interval?: number;
+}
+
+export type BaiduDeviceTokenResult = BaiduTokenResponse | BaiduTokenError;
+
 export interface BaiduListEntry {
   fs_id: number;
   path: string;
@@ -122,6 +133,34 @@ export async function refreshAccessToken(refreshToken: string): Promise<BaiduTok
   return result as BaiduTokenResponse;
 }
 
+export async function requestDeviceCode(): Promise<BaiduDeviceCodeResponse> {
+  const payload = await httpGet(BaiduConstants.DEVICE_CODE_URL, [
+    { key: 'response_type', value: 'device_code' },
+    { key: 'client_id', value: BaiduConstants.APP_KEY },
+    { key: 'scope', value: BaiduConstants.AUTH_SCOPE }
+  ]);
+  const result = await parseJson<BaiduDeviceCodeResponse | BaiduTokenError>(payload);
+  const errorResult = result as BaiduTokenError;
+  if ((result as BaiduDeviceCodeResponse).device_code) {
+    return result as BaiduDeviceCodeResponse;
+  }
+  if (errorResult.error) {
+    throw new Error(errorResult.error_description || errorResult.error);
+  }
+  throw new Error('未获取到百度设备码');
+}
+
+export async function pollDeviceToken(deviceCode: string): Promise<BaiduDeviceTokenResult> {
+  const payload = await httpGet(BaiduConstants.TOKEN_URL, [
+    { key: 'grant_type', value: 'device_token' },
+    { key: 'code', value: deviceCode },
+    { key: 'client_id', value: BaiduConstants.APP_KEY },
+    { key: 'client_secret', value: BaiduConstants.SECRET_KEY }
+  ]);
+  const result = await parseJson<BaiduDeviceTokenResult>(payload);
+  return result;
+}
+
 export async function listDirectory(
   accessToken: string,
   dir: string,

+ 278 - 23
entry/src/main/ets/dialog/RemoteDriveAccountDialog.ets

@@ -6,6 +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 { BaiduDeviceTokenResult, pollDeviceToken as pollBaiduDeviceToken, requestDeviceCode as requestBaiduDeviceCode } from '../common/network/BaiduPanClient';
 import { webview } from '@kit.ArkWeb';
 
 interface ParsedConnectionParts {
@@ -17,6 +18,14 @@ interface ParsedConnectionParts {
   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) {
@@ -59,6 +68,14 @@ export struct RemoteDriveAccountDialog {
   @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;
@@ -69,6 +86,7 @@ export struct RemoteDriveAccountDialog {
   private navBasePathCustomized: 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
@@ -807,6 +825,13 @@ export struct RemoteDriveAccountDialog {
         .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)
@@ -829,7 +854,7 @@ export struct RemoteDriveAccountDialog {
         }
       }
 
-      Text('系统将打开百度网盘授权网页,请登录并允许访问网盘文件。')
+      Text('可扫码授权或打开百度网盘授权网页,请登录并允许访问网盘文件。')
         .fontSize(13)
         .fontColor(this.isDarkMode ? '#C7C7CC' : '#666666')
         .maxLines(2);
@@ -879,33 +904,39 @@ export struct RemoteDriveAccountDialog {
         .alignItems(VerticalAlign.Center)
         .padding({ bottom: 4 });
 
+        this.buildBaiduAuthModeSwitch();
+
         Divider()
           .color(this.isDarkMode ? '#3A3A3C' : '#E5E5EA');
 
-        if (this.baiduAuthProgress > 0 && this.baiduAuthProgress < 100) {
-          Text(`页面加载中 ${this.baiduAuthProgress}%`)
-            .fontSize(12)
-            .fontColor(this.isDarkMode ? '#C7C7CC' : '#666666');
-        }
+        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);
-            }
+          Web({
+            src: this.baiduAuthorizeUrl,
+            controller: this.baiduWebController
           })
-          .onProgressChange((event) => {
-            if (event) {
-              this.baiduAuthProgress = event.newProgress;
-            }
-          });
+            .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%')
@@ -917,6 +948,98 @@ export struct RemoteDriveAccountDialog {
     .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 '';
@@ -927,7 +1050,137 @@ export struct RemoteDriveAccountDialog {
     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<void> {
+    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<void> {
+    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;
@@ -940,6 +1193,8 @@ export struct RemoteDriveAccountDialog {
     this.baiduAuthorizeUrl = '';
     this.baiduAuthProgress = 0;
     this.baiduAuthStateToken = '';
+    this.stopBaiduDevicePolling();
+    this.resetBaiduDeviceState();
   }
 
   private buildBaiduAuthorizeUrl(state: string): string {