diff --git a/README.md b/README.md
index eb5e20b..e1d7723 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
[下载](https://github.com/GhostenEditor/Ghosten-Player/releases/latest)
-一款同时适配Android TV和Android Phone的视频播放器,同时支持云播放(阿里云盘和Webdav)和本地播放,支持刮削影视的元信息,界面简洁纯净,操作简单
+一款同时适配Android TV和Android Phone的视频播放器,同时支持云播放(阿里云盘、夸克网盘和Webdav)和本地播放,支持刮削影视的元信息,界面简洁纯净,操作简单
[^1]: 开发中
@@ -63,7 +63,7 @@
## Features
1. 支持 **Android TV** 和 **Android Phone** (桌面端开发中)
-2. [支持阿里云盘、Webdav和本地文件播放](#添加账号)
+2. [支持阿里云盘、夸克网盘、Webdav和本地文件播放](#添加账号)
3. 纯本地运行,无需后端服务支持 [^3]
4. [支持跳过片头/片尾](#跳过片头片尾)
5. 支持视频轨道选择
@@ -135,13 +135,19 @@ _**[Media3文档](https://developer.android.google.cn/media/media3/exoplayer/sup
| 3 | 客户端ID | 仅开发者账号提供 |
| 4 | 客户端密码 | 仅开发者账号提供 |
-
+
+
+#### 夸克网盘
+
+通过网页登录夸克后,点击右上角确认按钮后完成登录
+
+
#### Webdav
填写Webdav对应的IP端口,输出账号密码,提交后完成登录。注:目前仅支持Basic编码登录
-
+
### 添加资源
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 5e5dd0d..c8bae98 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -1,4 +1,11 @@
+
+
+
@@ -31,6 +38,7 @@
+
diff --git a/android/app/src/main/kotlin/com/ghosten/player/MainActivity.kt b/android/app/src/main/kotlin/com/ghosten/player/MainActivity.kt
index cd017c8..09c780a 100644
--- a/android/app/src/main/kotlin/com/ghosten/player/MainActivity.kt
+++ b/android/app/src/main/kotlin/com/ghosten/player/MainActivity.kt
@@ -1,138 +1,196 @@
package com.ghosten.player
+import android.annotation.TargetApi
import android.app.UiModeManager
-import android.content.BroadcastReceiver
-import android.content.Context
import android.content.Intent
-import android.content.IntentFilter
import android.content.res.Configuration
-import io.flutter.embedding.android.FlutterActivity
-import io.flutter.embedding.engine.FlutterEngine
-import io.flutter.plugin.common.EventChannel
-import io.flutter.plugin.common.MethodCall
-import io.flutter.plugin.common.MethodChannel
-
-class MainActivity : FlutterActivity(), MethodChannel.MethodCallHandler {
- private var externalUrl: String? = null
- private var deeplink: String? = null
- private var channel: MethodChannel? = null
- private var pipChannel: EventChannel? = null
- private var pipSink: EventChannel.EventSink? = null
- private var screenChannel: EventChannel? = null
- private var screenSink: EventChannel.EventSink? = null
- private var deeplinkChannel: EventChannel? = null
- private var deeplinkSink: EventChannel.EventSink? = null
- private val screenStateReceiver = ScreenStateReceiver()
-
- override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
- super.configureFlutterEngine(flutterEngine)
- channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, PLUGIN_NAMESPACE)
- channel!!.setMethodCallHandler(this)
- pipChannel = EventChannel(flutterEngine.dartExecutor.binaryMessenger, "$PLUGIN_NAMESPACE/pip")
- pipChannel!!.setStreamHandler(object : EventChannel.StreamHandler {
- override fun onListen(args: Any?, sink: EventChannel.EventSink?) {
- pipSink = sink
- }
+import android.os.Build
+import android.os.Bundle
+import android.window.BackEvent
+import android.window.OnBackAnimationCallback
+import android.window.OnBackInvokedCallback
+import android.window.OnBackInvokedDispatcher
+import androidx.annotation.RequiresApi
+import androidx.fragment.app.FragmentActivity
+import androidx.fragment.app.FragmentManager
+import io.flutter.Build.API_LEVELS
+import io.flutter.embedding.android.FlutterFragment
- override fun onCancel(args: Any?) {
- pipSink?.endOfStream()
- pipSink = null
- }
- })
- screenChannel = EventChannel(flutterEngine.dartExecutor.binaryMessenger, "$PLUGIN_NAMESPACE/screen")
- screenChannel!!.setStreamHandler(object : EventChannel.StreamHandler {
- override fun onListen(args: Any?, sink: EventChannel.EventSink?) {
- screenSink = sink
- screenSink?.success(SCREEN_MODE_PRESENT)
- }
+class MainActivity : FragmentActivity() {
+ private var mainFragment: MainFragment? = null
+ private var hasRegisteredBackCallback = false
+ override fun onCreate(savedInstanceState: Bundle?) {
+ setTheme(R.style.NormalTheme)
+ setContentView(R.layout.main_layout)
- override fun onCancel(args: Any?) {
- screenSink?.endOfStream()
- screenSink = null
- }
- })
- deeplinkChannel = EventChannel(flutterEngine.dartExecutor.binaryMessenger, "$PLUGIN_NAMESPACE/deeplink")
- deeplinkChannel!!.setStreamHandler(object : EventChannel.StreamHandler {
- override fun onListen(args: Any?, sink: EventChannel.EventSink?) {
- deeplinkSink = sink
- deeplinkSink?.success(deeplink)
- }
+ super.onCreate(savedInstanceState)
+
+ if (intent.scheme == "content") {
+ mainFragment = ensureFlutterFragmentCreated(
+ PLAYER_FRAGMENT, "player", listOf(androidDeviceType().toString(), intent.data?.toString())
+ )
+ } else {
+ mainFragment = ensureFlutterFragmentCreated(MAIN_FRAGMENT, "main", listOf(androidDeviceType().toString()))
+ }
+ registerOnBackInvokedCallback()
+ }
+
+ private fun registerOnBackInvokedCallback() {
+ if (!hasRegisteredBackCallback && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ onBackInvokedDispatcher.registerOnBackInvokedCallback(
+ OnBackInvokedDispatcher.PRIORITY_DEFAULT, createOnBackInvokedCallback()
+ )
+ hasRegisteredBackCallback = true
+ }
+ }
- override fun onCancel(args: Any?) {
- deeplinkSink?.endOfStream()
- deeplinkSink = null
+ private fun createOnBackInvokedCallback(): OnBackInvokedCallback {
+ if (Build.VERSION.SDK_INT >= API_LEVELS.API_34) {
+ return object : OnBackAnimationCallback {
+ override fun onBackInvoked() {
+ commitBackGesture()
+ }
+
+ override fun onBackCancelled() {
+ cancelBackGesture()
+ }
+
+ override fun onBackProgressed(backEvent: BackEvent) {
+ updateBackGestureProgress(backEvent)
+ }
+
+ override fun onBackStarted(backEvent: BackEvent) {
+ startBackGesture(backEvent)
+ }
}
- })
+ }
+
+ return OnBackInvokedCallback { mainFragment?.onBackPressed() }
}
- override fun onResume() {
- super.onResume()
- if (intent.scheme == "content") {
- externalUrl = intent.data?.toString()
+ @TargetApi(API_LEVELS.API_34)
+ @RequiresApi(API_LEVELS.API_34)
+ fun startBackGesture(backEvent: BackEvent) {
+ if (stillAttachedForEvent("startBackGesture")) {
+ mainFragment?.startBackGesture(backEvent)
}
- val mScreenStatusFilter = IntentFilter()
- mScreenStatusFilter.addAction(Intent.ACTION_SCREEN_ON)
- mScreenStatusFilter.addAction(Intent.ACTION_SCREEN_OFF)
- mScreenStatusFilter.addAction(Intent.ACTION_USER_PRESENT)
- context.registerReceiver(screenStateReceiver, mScreenStatusFilter)
}
- override fun onNewIntent(intent: Intent) {
- if (intent.scheme == "ghosten") {
- deeplink = intent.data?.toString()
- deeplinkSink?.success(deeplink)
+ @TargetApi(API_LEVELS.API_34)
+ @RequiresApi(API_LEVELS.API_34)
+ fun updateBackGestureProgress(backEvent: BackEvent) {
+ if (stillAttachedForEvent("updateBackGestureProgress")) {
+ mainFragment?.updateBackGestureProgress(backEvent)
+ }
+ }
+
+ @TargetApi(API_LEVELS.API_34)
+ @RequiresApi(API_LEVELS.API_34)
+ fun commitBackGesture() {
+ if (stillAttachedForEvent("commitBackGesture")) {
+ mainFragment?.commitBackGesture()
}
+ }
+
+ @TargetApi(API_LEVELS.API_34)
+ @RequiresApi(API_LEVELS.API_34)
+ fun cancelBackGesture() {
+ if (stillAttachedForEvent("cancelBackGesture")) {
+ mainFragment?.cancelBackGesture()
+ }
+ }
+
+ private fun stillAttachedForEvent(event: String): Boolean {
+ if (mainFragment == null) {
+ return false
+ }
+ return true
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
+ super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
+ mainFragment?.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
+ }
+
+ override fun onPostResume() {
+ super.onPostResume()
+ mainFragment?.onPostResume()
+ }
+
+ override fun onNewIntent(intent: Intent) {
+ mainFragment?.onNewIntent(intent)
super.onNewIntent(intent)
}
- override fun onPause() {
- super.onPause()
- context.unregisterReceiver(screenStateReceiver)
+ @Deprecated("Deprecated in Java")
+ override fun onBackPressed() {
+ mainFragment?.onBackPressed()
+ }
+
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray
+ ) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ mainFragment?.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ }
+
+ @Deprecated("Deprecated in Java")
+ override fun onActivityResult(
+ requestCode: Int, resultCode: Int, data: Intent?
+ ) {
+ super.onActivityResult(requestCode, resultCode, data)
+ mainFragment?.onActivityResult(requestCode, resultCode, data)
}
- override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration?) {
- pipSink?.success(isInPictureInPictureMode)
+ override fun onUserLeaveHint() {
+ mainFragment?.onUserLeaveHint()
}
- override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
- when (call.method) {
- "androidDeviceType" -> result.success(androidDeviceType())
- "externalUrl" -> result.success(externalUrl)
- else -> result.notImplemented()
+ override fun onTrimMemory(level: Int) {
+ super.onTrimMemory(level)
+ mainFragment?.onTrimMemory(level)
+ }
+
+
+ private fun ensureFlutterFragmentCreated(tag: String, entryPoint: String, args: List): MainFragment {
+ val fragmentManager: FragmentManager = supportFragmentManager
+ var fragment = fragmentManager.findFragmentByTag(tag) as MainFragment?
+
+ val newFragment = FlutterFragment
+ .NewEngineFragmentBuilder(MainFragment::class.java)
+ .shouldDelayFirstAndroidViewDraw(true)
+ .shouldAutomaticallyHandleOnBackPressed(true)
+ .dartEntrypoint(entryPoint)
+ .dartEntrypointArgs(args)
+ .build()
+ if (fragment == null) {
+ fragmentManager.beginTransaction().add(R.id.fragment_container, newFragment, tag).commit()
+ } else {
+ fragmentManager.beginTransaction().replace(R.id.fragment_container, newFragment, tag).commit()
}
+ fragment = newFragment
+ return fragment;
}
private fun androidDeviceType(): Int {
val uiModeManager = getSystemService(UI_MODE_SERVICE) as UiModeManager
return if (uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION) {
DEVICE_TYPE_TV
- } else if (context.resources.configuration.screenLayout and Configuration.SCREENLAYOUT_SIZE_MASK >= Configuration.SCREENLAYOUT_SIZE_LARGE) {
+ } else if (resources.configuration.screenLayout and Configuration.SCREENLAYOUT_SIZE_MASK >= Configuration.SCREENLAYOUT_SIZE_LARGE) {
DEVICE_TYPE_PAD
} else {
DEVICE_TYPE_PHONE
}
}
- inner class ScreenStateReceiver : BroadcastReceiver() {
- override fun onReceive(context: Context?, intent: Intent?) {
- val action = intent?.action
- if (Intent.ACTION_SCREEN_ON == action) {
- screenSink?.success(SCREEN_MODE_ON)
- } else if (Intent.ACTION_SCREEN_OFF == action) {
- screenSink?.success(SCREEN_MODE_OFF)
- } else if (Intent.ACTION_USER_PRESENT == action) {
- screenSink?.success(SCREEN_MODE_PRESENT)
- }
- }
- }
-
companion object {
- const val PLUGIN_NAMESPACE = "com.ghosten.player"
- const val SCREEN_MODE_ON = "on"
- const val SCREEN_MODE_OFF = "off"
- const val SCREEN_MODE_PRESENT = "present"
- const val DEVICE_TYPE_TV = 0
- const val DEVICE_TYPE_PAD = 1
- const val DEVICE_TYPE_PHONE = 2
+ private const val DEVICE_TYPE_TV = 0
+ private const val DEVICE_TYPE_PAD = 1
+ private const val DEVICE_TYPE_PHONE = 2
+ private const val MAIN_FRAGMENT = "main_fragment"
+ private const val PLAYER_FRAGMENT = "player_fragment"
}
}
diff --git a/android/app/src/main/kotlin/com/ghosten/player/MainFragment.kt b/android/app/src/main/kotlin/com/ghosten/player/MainFragment.kt
new file mode 100644
index 0000000..b37dfa9
--- /dev/null
+++ b/android/app/src/main/kotlin/com/ghosten/player/MainFragment.kt
@@ -0,0 +1,152 @@
+package com.ghosten.player
+
+import android.annotation.TargetApi
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.res.Configuration
+import android.window.BackEvent
+import androidx.annotation.RequiresApi
+import io.flutter.Build.API_LEVELS
+import io.flutter.embedding.android.FlutterFragment
+import io.flutter.embedding.engine.FlutterEngine
+import io.flutter.plugin.common.EventChannel
+import io.flutter.plugins.GeneratedPluginRegistrant
+
+class MainFragment : FlutterFragment() {
+ private var deeplink: String? = null
+ private var pipChannel: EventChannel? = null
+ private var pipSink: EventChannel.EventSink? = null
+ private var screenChannel: EventChannel? = null
+ private var screenSink: EventChannel.EventSink? = null
+ private var deeplinkChannel: EventChannel? = null
+ private var deeplinkSink: EventChannel.EventSink? = null
+ private val screenStateReceiver = ScreenStateReceiver()
+
+ override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
+ super.configureFlutterEngine(flutterEngine)
+ GeneratedPluginRegistrant.registerWith(flutterEngine)
+ pipChannel = EventChannel(flutterEngine.dartExecutor.binaryMessenger, "$PLUGIN_NAMESPACE/pip")
+ pipChannel!!.setStreamHandler(object : EventChannel.StreamHandler {
+ override fun onListen(args: Any?, sink: EventChannel.EventSink?) {
+ pipSink = sink
+ }
+
+ override fun onCancel(args: Any?) {
+ pipSink?.endOfStream()
+ pipSink = null
+ }
+ })
+ screenChannel = EventChannel(flutterEngine.dartExecutor.binaryMessenger, "$PLUGIN_NAMESPACE/screen")
+ screenChannel!!.setStreamHandler(object : EventChannel.StreamHandler {
+ override fun onListen(args: Any?, sink: EventChannel.EventSink?) {
+ screenSink = sink
+ screenSink?.success(SCREEN_MODE_PRESENT)
+ }
+
+ override fun onCancel(args: Any?) {
+ screenSink?.endOfStream()
+ screenSink = null
+ }
+ })
+ deeplinkChannel = EventChannel(flutterEngine.dartExecutor.binaryMessenger, "$PLUGIN_NAMESPACE/deeplink")
+ deeplinkChannel!!.setStreamHandler(object : EventChannel.StreamHandler {
+ override fun onListen(args: Any?, sink: EventChannel.EventSink?) {
+ deeplinkSink = sink
+ deeplinkSink?.success(deeplink)
+ }
+
+ override fun onCancel(args: Any?) {
+ deeplinkSink?.endOfStream()
+ deeplinkSink = null
+ }
+ })
+ }
+
+ override fun onResume() {
+ super.onResume()
+ val mScreenStatusFilter = IntentFilter()
+ mScreenStatusFilter.addAction(Intent.ACTION_SCREEN_ON)
+ mScreenStatusFilter.addAction(Intent.ACTION_SCREEN_OFF)
+ mScreenStatusFilter.addAction(Intent.ACTION_USER_PRESENT)
+ context.registerReceiver(screenStateReceiver, mScreenStatusFilter)
+ }
+
+ override fun onNewIntent(intent: Intent) {
+ if (intent.scheme == "ghosten") {
+ deeplink = intent.data?.toString()
+ deeplinkSink?.success(deeplink)
+ }
+ super.onNewIntent(intent)
+ }
+
+ override fun onPause() {
+ super.onPause()
+ context.unregisterReceiver(screenStateReceiver)
+ }
+
+ @TargetApi(API_LEVELS.API_34)
+ @RequiresApi(API_LEVELS.API_34)
+ fun startBackGesture(backEvent: BackEvent) {
+ ensureAlive()
+ if (flutterEngine != null) {
+ flutterEngine!!.backGestureChannel.startBackGesture(backEvent)
+ }
+ }
+
+ @TargetApi(API_LEVELS.API_34)
+ @RequiresApi(API_LEVELS.API_34)
+ fun updateBackGestureProgress(backEvent: BackEvent) {
+ ensureAlive()
+ if (flutterEngine != null) {
+ flutterEngine!!.backGestureChannel.updateBackGestureProgress(backEvent)
+ }
+ }
+
+ @TargetApi(API_LEVELS.API_34)
+ @RequiresApi(API_LEVELS.API_34)
+ fun commitBackGesture() {
+ ensureAlive()
+ if (flutterEngine != null) {
+ flutterEngine!!.backGestureChannel.commitBackGesture()
+ }
+ }
+
+ @TargetApi(API_LEVELS.API_34)
+ @RequiresApi(API_LEVELS.API_34)
+ fun cancelBackGesture() {
+ ensureAlive()
+ if (flutterEngine != null) {
+ flutterEngine!!.backGestureChannel.cancelBackGesture()
+ }
+ }
+
+ private fun ensureAlive() {
+ checkNotNull(host) { "Cannot execute method on a destroyed FlutterActivityAndFragmentDelegate." }
+ }
+
+ fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
+ pipSink?.success(isInPictureInPictureMode)
+ }
+
+ inner class ScreenStateReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ val action = intent?.action
+ if (Intent.ACTION_SCREEN_ON == action) {
+ screenSink?.success(SCREEN_MODE_ON)
+ } else if (Intent.ACTION_SCREEN_OFF == action) {
+ screenSink?.success(SCREEN_MODE_OFF)
+ } else if (Intent.ACTION_USER_PRESENT == action) {
+ screenSink?.success(SCREEN_MODE_PRESENT)
+ }
+ }
+ }
+
+ companion object {
+ const val PLUGIN_NAMESPACE = "com.ghosten.player"
+ const val SCREEN_MODE_ON = "on"
+ const val SCREEN_MODE_OFF = "off"
+ const val SCREEN_MODE_PRESENT = "present"
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/res/drawable-xhdpi/ic_banner.xml b/android/app/src/main/res/drawable-xhdpi/ic_banner.xml
new file mode 100644
index 0000000..15a4444
--- /dev/null
+++ b/android/app/src/main/res/drawable-xhdpi/ic_banner.xml
@@ -0,0 +1,20 @@
+
+ -
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/drawable/ic_banner.xml b/android/app/src/main/res/drawable/ic_banner.xml
deleted file mode 100644
index fb5c9c2..0000000
--- a/android/app/src/main/res/drawable/ic_banner.xml
+++ /dev/null
@@ -1,101 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/android/app/src/main/res/layout/main_layout.xml b/android/app/src/main/res/layout/main_layout.xml
new file mode 100644
index 0000000..8aaab77
--- /dev/null
+++ b/android/app/src/main/res/layout/main_layout.xml
@@ -0,0 +1,8 @@
+
+
+
+
diff --git a/android/app/src/main/res/values-night-v21/styles.xml b/android/app/src/main/res/values-night-v21/styles.xml
index 6476f97..345c32b 100644
--- a/android/app/src/main/res/values-night-v21/styles.xml
+++ b/android/app/src/main/res/values-night-v21/styles.xml
@@ -6,7 +6,7 @@
- true
diff --git a/android/app/src/main/res/values-v21/styles.xml b/android/app/src/main/res/values-v21/styles.xml
index c2065a4..14a8594 100644
--- a/android/app/src/main/res/values-v21/styles.xml
+++ b/android/app/src/main/res/values-v21/styles.xml
@@ -6,7 +6,7 @@
- true
diff --git a/lib/components/blurred_background.dart b/lib/components/blurred_background.dart
index c9feeae..a4e9848 100644
--- a/lib/components/blurred_background.dart
+++ b/lib/components/blurred_background.dart
@@ -1,13 +1,16 @@
-import 'dart:ui';
+import 'dart:math';
+import 'dart:ui' as ui;
+import 'package:cached_network_image/cached_network_image.dart';
+import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
-
-import 'async_image.dart';
+import 'package:flutter/rendering.dart';
class BlurredBackground extends StatefulWidget {
final String background;
+ final Color? defaultColor;
- const BlurredBackground({super.key, required this.background});
+ const BlurredBackground({super.key, required this.background, this.defaultColor});
@override
State createState() => _BlurredBackgroundState();
@@ -16,9 +19,11 @@ class BlurredBackground extends StatefulWidget {
class _BlurredBackgroundState extends State with SingleTickerProviderStateMixin {
late final size = MediaQuery.of(context).size;
final blurSize = 50.0;
- final scaleSize = 3;
+ final scaleSize = 4;
Offset offset = Offset.zero;
- Offset vector = const Offset(2, 2);
+ Offset vector = const Offset(1, 1);
+ Size imageSize = Size.zero;
+ Size imageSizeFixed = Size.zero;
late final AnimationController _controller = AnimationController(
duration: const Duration(seconds: 10),
@@ -33,25 +38,150 @@ class _BlurredBackgroundState extends State with SingleTicker
@override
Widget build(BuildContext context) {
- return AnimatedBuilder(
- animation: _controller,
- builder: (context, child) => Transform(
- transform: transform(),
- child: child,
- ),
- child: ImageFiltered(imageFilter: ImageFilter.blur(sigmaX: blurSize, sigmaY: blurSize), child: AsyncImage(widget.background)),
+ return Container(
+ clipBehavior: Clip.hardEdge,
+ decoration: const BoxDecoration(),
+ child: FutureBuilder(
+ future: background(),
+ builder: (context, snapshot) {
+ return AnimatedOpacity(
+ opacity: snapshot.hasData ? 1 : 0,
+ duration: const Duration(milliseconds: 500),
+ curve: Curves.easeIn,
+ child: AnimatedBuilder(
+ animation: _controller,
+ builder: (context, child) => Transform(
+ transform: transform(),
+ alignment: Alignment.center,
+ filterQuality: FilterQuality.high,
+ child: child,
+ ),
+ child: snapshot.hasData ? snapshot.requireData : Container(color: widget.defaultColor ?? Theme.of(context).colorScheme.surface),
+ ));
+ }),
+ );
+ }
+
+ Future background() async {
+ final data = await widgetToUiImage(
+ ImageFiltered(imageFilter: ui.ImageFilter.blur(sigmaX: blurSize, sigmaY: blurSize), child: CachedNetworkImage(imageUrl: widget.background)));
+ imageSize = Size(data.width.toDouble(), data.height.toDouble());
+ imageSizeFixed = Size(
+ imageSize.aspectRatio > size.aspectRatio ? size.width : size.height * imageSize.aspectRatio,
+ imageSize.aspectRatio < size.aspectRatio ? size.height : size.width / imageSize.aspectRatio,
);
+ return Image.memory((await data.toByteData(format: ui.ImageByteFormat.png))!.buffer.asUint8List(), fit: BoxFit.contain);
}
Matrix4 transform() {
- assert(scaleSize >= 2);
- if (offset.dx < 0 || offset.dx > size.width * (scaleSize - 1)) {
+ final offsetLimitation = Size(
+ max(imageSizeFixed.width * scaleSize - size.width, 0),
+ max(imageSizeFixed.height * scaleSize - size.height, 0),
+ ) /
+ 2;
+ offset += vector;
+ if (offset.dx < -offsetLimitation.width || offset.dx > offsetLimitation.width) {
vector = Offset(-vector.dx, vector.dy);
}
- if (offset.dy < 0 || offset.dy > size.height * (scaleSize - 1)) {
+ if (offset.dy < -offsetLimitation.height || offset.dy > offsetLimitation.height) {
vector = Offset(vector.dx, -vector.dy);
}
- offset += vector;
- return Matrix4.translationValues(-offset.dx, -offset.dy, 0).scaled(scaleSize.toDouble(), scaleSize.toDouble(), 1.0);
+ offset = Offset(
+ clampDouble(offset.dx, -offsetLimitation.width, offsetLimitation.width),
+ clampDouble(offset.dy, -offsetLimitation.height, offsetLimitation.height),
+ );
+ final matrix = Matrix4.translationValues(-size.width / 2, -size.height / 2, 0).scaled(scaleSize.toDouble(), scaleSize.toDouble(), 1.0);
+ matrix.translate((offset.dx + size.width / 2) / scaleSize, (offset.dy + size.height / 2) / scaleSize, 0);
+ return matrix;
+ }
+
+ static Future widgetToUiImage(
+ Widget widget, {
+ Duration delay = Duration.zero,
+ double? pixelRatio,
+ BuildContext? context,
+ Size? targetSize,
+ }) async {
+ int retryCounter = 3;
+ bool isDirty = false;
+
+ Widget child = widget;
+
+ if (context != null) {
+ child = InheritedTheme.captureAll(
+ context,
+ MediaQuery(
+ data: MediaQuery.of(context),
+ child: Material(
+ color: Colors.transparent,
+ child: child,
+ )),
+ );
+ }
+
+ final RenderRepaintBoundary repaintBoundary = RenderRepaintBoundary();
+ final platformDispatcher = WidgetsBinding.instance.platformDispatcher;
+ final fallBackView = platformDispatcher.views.first;
+ final view = context == null ? fallBackView : View.maybeOf(context) ?? fallBackView;
+ Size logicalSize = targetSize ?? view.physicalSize / view.devicePixelRatio;
+ Size imageSize = targetSize ?? view.physicalSize;
+
+ assert(logicalSize.aspectRatio.toStringAsPrecision(5) == imageSize.aspectRatio.toStringAsPrecision(5));
+
+ final RenderView renderView = RenderView(
+ view: view,
+ child: RenderPositionedBox(alignment: Alignment.center, child: repaintBoundary),
+ configuration: ViewConfiguration(
+ logicalConstraints: BoxConstraints(
+ maxWidth: logicalSize.width,
+ maxHeight: logicalSize.height,
+ ),
+ devicePixelRatio: pixelRatio ?? 1.0,
+ ),
+ );
+
+ final PipelineOwner pipelineOwner = PipelineOwner();
+ final BuildOwner buildOwner = BuildOwner(focusManager: FocusManager(), onBuildScheduled: () => isDirty = true);
+
+ pipelineOwner.rootNode = renderView;
+ renderView.prepareInitialFrame();
+
+ final RenderObjectToWidgetElement rootElement =
+ RenderObjectToWidgetAdapter(container: repaintBoundary, child: Directionality(textDirection: TextDirection.ltr, child: child))
+ .attachToRenderTree(buildOwner);
+
+ buildOwner.buildScope(
+ rootElement,
+ );
+ buildOwner.finalizeTree();
+
+ pipelineOwner.flushLayout();
+ pipelineOwner.flushCompositingBits();
+ pipelineOwner.flushPaint();
+
+ ui.Image? image;
+
+ do {
+ isDirty = false;
+ image = await repaintBoundary.toImage(pixelRatio: pixelRatio ?? (imageSize.width / logicalSize.width));
+
+ await Future.delayed(delay);
+
+ if (isDirty) {
+ buildOwner.buildScope(
+ rootElement,
+ );
+ buildOwner.finalizeTree();
+ pipelineOwner.flushLayout();
+ pipelineOwner.flushCompositingBits();
+ pipelineOwner.flushPaint();
+ }
+ retryCounter--;
+ } while (isDirty && retryCounter >= 0);
+ try {
+ buildOwner.finalizeTree();
+ } catch (e) {}
+
+ return image;
}
}
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index ab81e9f..318a6e4 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -68,7 +68,7 @@
"downloaderDeleteFileConfirmText": "Whether to delete files at the same time?",
"downloaderLabelDownloaded": "Downloaded",
"downloaderLabelDownloading": "Downloading",
- "driverType": "{driverType, select, alipan{Alipan} webdav{Webdav} local{Local} other{Unknown}}",
+ "driverType": "{driverType, select, alipan{Alipan} quark{Quark} quarktv{Quark TV} webdav{Webdav} local{Local} other{Unknown}}",
"episodeCount": "Episode {episodes}",
"errorLoadData": "Load data failed",
"errorTextConnectTimeout": "Connect Timeout",
diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb
index 7bab7c3..a4cb33f 100644
--- a/lib/l10n/app_zh.arb
+++ b/lib/l10n/app_zh.arb
@@ -68,7 +68,7 @@
"downloaderDeleteFileConfirmText": "是否同时删除文件?",
"downloaderLabelDownloaded": "已完成",
"downloaderLabelDownloading": "下载中",
- "driverType": "{driverType, select, alipan{阿里云盘} webdav{Webdav} local{本地} other{未知}}",
+ "driverType": "{driverType, select, alipan{阿里云盘} quark{夸克网盘} quarktv{夸克网盘 TV} webdav{Webdav} local{本地} other{未知}}",
"episodeCount": "{episodes}集",
"errorLoadData": "数据获取失败",
"errorTextConnectTimeout": "连接超时",
diff --git a/lib/main.dart b/lib/main.dart
index 9866da9..796d734 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -20,7 +20,7 @@ import 'theme.dart';
import 'utils/notification.dart';
import 'utils/utils.dart';
-void main() async {
+void main(List args) async {
WidgetsFlutterBinding.ensureInitialized();
await Api.initialized();
if (kIsWeb) {
@@ -30,29 +30,29 @@ void main() async {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: [SystemUiOverlay.top]);
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
HttpOverrides.global = MyHttpOverrides();
- await PlatformApi.getDeviceType();
+ PlatformApi.deviceType = DeviceType.fromString(args[0]);
kIsAndroidTV = PlatformApi.isAndroidTV();
}
- final externalUrl = kIsWeb ? null : await PlatformApi.externalUrl;
- if (externalUrl == null) {
- setPreferredOrientations(false);
- final userConfig = await UserConfig.init();
- Provider.debugCheckInvalidValueType = null;
- if (!kIsWeb && userConfig.shouldCheckUpdate()) {
- Api.checkUpdate(
- updateUrl,
- Version.fromString(appVersion),
- needUpdate: (data, url) => showModalBottomSheet(
- context: navigatorKey.currentContext!,
- constraints: const BoxConstraints(minWidth: double.infinity, maxHeight: 320),
- builder: (context) => UpdateBottomSheet(data, url: url)),
- );
- }
- runApp(ChangeNotifierProvider(create: (_) => userConfig, child: const MainApp()));
- PlatformApi.deeplinkEvent.listen(scanToLogin);
- } else {
- runApp(PlayerApp(url: externalUrl));
+ setPreferredOrientations(false);
+ final userConfig = await UserConfig.init();
+ Provider.debugCheckInvalidValueType = null;
+ if (!kIsWeb && userConfig.shouldCheckUpdate()) {
+ Api.checkUpdate(
+ updateUrl,
+ Version.fromString(appVersion),
+ needUpdate: (data, url) => showModalBottomSheet(
+ context: navigatorKey.currentContext!,
+ constraints: const BoxConstraints(minWidth: double.infinity, maxHeight: 320),
+ builder: (context) => UpdateBottomSheet(data, url: url)),
+ );
}
+ runApp(ChangeNotifierProvider(create: (_) => userConfig, child: const MainApp()));
+ PlatformApi.deeplinkEvent.listen(scanToLogin);
+}
+
+void player(List args) async {
+ PlatformApi.deviceType = DeviceType.fromString(args[0]);
+ runApp(PlayerApp(url: args[1]));
}
class MainApp extends StatelessWidget {
@@ -170,7 +170,7 @@ Future scanToLogin(String link) async {
try {
final url = Uri.parse(link);
final data = utf8.decode(base64.decode(url.path.split('/').last));
- if (context.mounted) await showNotification(context, Api.driverInsert(jsonDecode(data)));
+ if (context.mounted) await showNotification(context, Api.driverInsert(jsonDecode(data)).last);
if (context.mounted) navigateTo(context, const AccountManage());
} catch (_) {}
}
diff --git a/lib/pages/account/account.dart b/lib/pages/account/account.dart
index 889a287..6811c73 100644
--- a/lib/pages/account/account.dart
+++ b/lib/pages/account/account.dart
@@ -1,4 +1,5 @@
import 'package:api/api.dart';
+import 'package:cached_network_image/cached_network_image.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart' hide PopupMenuItem;
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@@ -96,7 +97,9 @@ class _AccountManageState extends State {
children: [
AspectRatio(
aspectRatio: 1,
- child: item.avatar == null ? const Icon(Icons.account_circle, size: 160) : Image.network(item.avatar!),
+ child: item.avatar == null
+ ? const Icon(Icons.account_circle, size: 160)
+ : CachedNetworkImage(imageUrl: item.avatar!, fit: BoxFit.cover),
),
Expanded(
child: Padding(
diff --git a/lib/pages/account/account_login.dart b/lib/pages/account/account_login.dart
index a715a52..e2cec20 100644
--- a/lib/pages/account/account_login.dart
+++ b/lib/pages/account/account_login.dart
@@ -1,11 +1,16 @@
import 'package:api/api.dart';
+import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:qr_flutter/qr_flutter.dart';
+import 'package:webview_cookie_manager/webview_cookie_manager.dart';
+import 'package:webview_flutter/webview_flutter.dart';
import '../../components/connect_button.dart';
+import '../../components/error_message.dart';
import '../../components/form_group.dart';
import '../../components/gap.dart';
-import '../../utils/notification.dart';
+import '../../const.dart';
import '../../validators/validators.dart';
class AccountLoginPage extends StatefulWidget {
@@ -18,6 +23,7 @@ class AccountLoginPage extends StatefulWidget {
class _AccountLoginPageState extends State {
late final FormGroupController _alipan;
late final FormGroupController _webdav;
+
DriverType driverType = DriverType.alipan;
@override
@@ -89,26 +95,36 @@ class _AccountLoginPageState extends State {
],
),
body: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Padding(
- padding: const EdgeInsets.all(8.0),
- child: Row(
- children: [
- Radio(value: DriverType.alipan, groupValue: driverType, onChanged: (t) => setState(() => driverType = t!)),
- GestureDetector(
- onTap: () => setState(() => driverType = DriverType.alipan),
- child: Text(AppLocalizations.of(context)!.driverType(DriverType.alipan.name)),
- ),
- Gap.hSM,
- Radio(value: DriverType.webdav, groupValue: driverType, onChanged: (t) => setState(() => driverType = t!)),
- GestureDetector(
- onTap: () => setState(() => driverType = DriverType.webdav),
- child: Text(AppLocalizations.of(context)!.driverType(DriverType.webdav.name)),
- ),
- ],
+ SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.start,
+ children: [DriverType.alipan, DriverType.quark, DriverType.webdav]
+ .map((ty) => [
+ Radio(value: ty, groupValue: driverType, onChanged: (t) => setState(() => driverType = t!)),
+ GestureDetector(
+ onTap: () => setState(() => driverType = ty),
+ child: Text(AppLocalizations.of(context)!.driverType(ty.name)),
+ ),
+ Gap.hSM,
+ ])
+ .flattened
+ .toList(),
+ ),
),
),
if (driverType == DriverType.alipan) Expanded(child: FormGroup(controller: _alipan)),
+ if (driverType == DriverType.quark)
+ Expanded(
+ child: WebViewWidget(
+ controller: WebViewController()
+ ..setJavaScriptMode(JavaScriptMode.unrestricted)
+ ..setUserAgent(ua)
+ ..loadRequest(Uri.parse('https://pan.quark.cn')))),
if (driverType == DriverType.webdav) Expanded(child: FormGroup(controller: _webdav)),
],
),
@@ -119,19 +135,110 @@ class _AccountLoginPageState extends State {
if (switch (driverType) {
DriverType.alipan => _alipan.validate(),
DriverType.webdav => _webdav.validate(),
+ DriverType.quark => true,
_ => throw UnimplementedError(),
}) {
final data = switch (driverType) {
DriverType.alipan => _alipan.data,
DriverType.webdav => _webdav.data,
+ DriverType.quark => await _quarkCookie(),
_ => throw UnimplementedError(),
};
+ if (!mounted) return;
data['type'] = driverType.name;
- final resp = await showNotification(context, Api.driverInsert(data));
- if (resp!.error == null && mounted) Navigator.of(context).pop(true);
+ final flag = await showDialog(context: context, builder: (context) => buildLoginLoading(Api.driverInsert(data)));
+ if (flag == true && mounted) {
+ Navigator.of(context).pop(true);
+ }
}
}
+ Future