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 | 客户端密码 | 仅开发者账号提供 | -Alipan Login Page +Alipan Login Page + +#### 夸克网盘 + +通过网页登录夸克后,点击右上角确认按钮后完成登录 + +Quark Login Page #### Webdav 填写Webdav对应的IP端口,输出账号密码,提交后完成登录。注:目前仅支持Basic编码登录 -Webdav Login Page 1 +Webdav Login Page 1 ### 添加资源 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> _quarkCookie() async { + final cookieManager = WebviewCookieManager(); + final gotCookies = await cookieManager.getCookies('https://pan.quark.cn'); + final cookies = gotCookies.map((c) => '${c.name}=${c.value}').join('; '); + return {'token': cookies}; + } + + Widget buildLoginLoading(Stream stream) { + return AlertDialog( + title: Text(AppLocalizations.of(context)!.modalTitleNotification), + content: StreamBuilder( + stream: stream, + builder: (context, snapshot) => PopScope( + canPop: false, + onPopInvoked: (didPop) { + if (!didPop && (snapshot.connectionState == ConnectionState.done || snapshot.connectionState == ConnectionState.none || snapshot.hasData)) { + Navigator.of(context).pop(); + } + }, + child: Builder(builder: (context) { + switch (snapshot.connectionState) { + case ConnectionState.waiting: + case ConnectionState.active: + if (snapshot.hasData) { + if (snapshot.requireData['type'] == 'qrcode') { + return SizedBox( + width: kQrSize, + height: kQrSize, + child: QrImageView( + backgroundColor: Colors.white, + data: snapshot.requireData['qrcode_data'], + version: QrVersions.auto, + size: kQrSize, + ), + ); + } else { + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Padding( + padding: EdgeInsets.all(17), + child: CircularProgressIndicator(), + ), + Text(AppLocalizations.of(context)!.modalNotificationLoadingText), + ], + ); + } + } else { + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Padding( + padding: EdgeInsets.all(17), + child: CircularProgressIndicator(), + ), + Text(AppLocalizations.of(context)!.modalNotificationLoadingText), + ], + ); + } + case ConnectionState.none: + case ConnectionState.done: + if (snapshot.hasError) { + return ErrorMessage(snapshot: snapshot, leading: const Icon(Icons.error_outline, size: 60, color: Colors.red)); + } else { + Future.delayed(const Duration(seconds: 1)).then((value) { + if (context.mounted) { + Navigator.of(context).pop(true); + } + }); + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.check_circle_outline, size: 60, color: Colors.green), + Gap.vMD, + Text(AppLocalizations.of(context)!.modalNotificationSuccessText), + ], + ); + } + } + }))), + ); + } + onConnectData(String data) { final formGroup = switch (driverType) { DriverType.alipan => _alipan, diff --git a/lib/pages/detail/components/overview.dart b/lib/pages/detail/components/overview.dart index 9508190..8671a08 100644 --- a/lib/pages/detail/components/overview.dart +++ b/lib/pages/detail/components/overview.dart @@ -28,7 +28,7 @@ class OverviewSection extends StatelessWidget { } showFull(BuildContext context) { - navigateTo(context, Overview(item: item, description: description)); + navigateToSlideUp(context, Overview(item: item, description: description)); } } @@ -60,6 +60,7 @@ class Overview extends StatelessWidget { children: [ if (item.poster != null) BlurredBackground( + defaultColor: item.themeColor != null ? Color(item.themeColor!) : null, background: item.poster!, ), if (item.poster != null) diff --git a/lib/pages/player/live_player.dart b/lib/pages/player/live_player.dart index 1cb120d..ea71e0a 100644 --- a/lib/pages/player/live_player.dart +++ b/lib/pages/player/live_player.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:animations/animations.dart'; import 'package:api/api.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:player_view/player.dart'; @@ -33,6 +32,7 @@ class _LivePlayerPageState extends State { void initState() { _pipSubscription = PlatformApi.pipEvent.listen((flag) { _controller.pipMode.value = flag; + _isShowControls.value = false; }); SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); super.initState(); @@ -59,7 +59,7 @@ class _LivePlayerPageState extends State { child: Builder(builder: (context) { return Scaffold( key: _scaffoldKey, - backgroundColor: kIsWeb ? Colors.transparent : Colors.black, + backgroundColor: Colors.transparent, body: Stack( children: [ PlayerPlatformView( @@ -75,7 +75,6 @@ class _LivePlayerPageState extends State { if (PlatformApi.isAndroidTV() && _isShowControls.value) { _hideControls(); } else { - await _controller.hide(); if (context.mounted) Navigator.pop(context); } }, diff --git a/lib/pages/player/singleton_player.dart b/lib/pages/player/singleton_player.dart index afc04d3..6456512 100644 --- a/lib/pages/player/singleton_player.dart +++ b/lib/pages/player/singleton_player.dart @@ -19,7 +19,7 @@ class _SingletonPlayerState extends State { url: Uri.parse(widget.url), sourceType: PlaylistItemSourceType.local, ), - ], null, Api.log); + ], 0, Api.log); @override void dispose() { diff --git a/lib/pages/settings/settings_diagnotics.dart b/lib/pages/settings/settings_diagnotics.dart index f14e5b2..4014d95 100644 --- a/lib/pages/settings/settings_diagnotics.dart +++ b/lib/pages/settings/settings_diagnotics.dart @@ -14,7 +14,7 @@ class SettingsDiagnotics extends StatelessWidget { title: Text(AppLocalizations.of(context)!.settingsItemNetworkDiagnotics), ), body: StreamBuilderHandler( - stream: Api.networkDiagnotics(), + stream: Api.networkDiagnostics(), builder: (context, snapshot) { return ListView.builder( itemBuilder: (context, index) { diff --git a/lib/pages/settings/settings_log.dart b/lib/pages/settings/settings_log.dart index 5fa934e..85fbf1b 100644 --- a/lib/pages/settings/settings_log.dart +++ b/lib/pages/settings/settings_log.dart @@ -7,6 +7,7 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import '../../components/error_message.dart'; import '../../components/no_data.dart'; +import '../../platform_api.dart'; class SettingsLogPage extends StatefulWidget { const SettingsLogPage({super.key}); @@ -76,12 +77,14 @@ class _SettingsLogPageState extends State { scrollController: _scrollController, builderDelegate: PagedChildBuilderDelegate( itemBuilder: (context, item, index) => ListTile( + autofocus: PlatformApi.isAndroidTV() && index == 0, dense: true, visualDensity: VisualDensity.compact, title: Text(item.message), subtitle: Text(formatDate(item.time, [yyyy, '-', mm, '-', dd, ' ', HH, ':', nn, ':', ss, '.', SSS])), leading: Badge( label: SizedBox(width: 40, child: Text(item.level.name.toUpperCase(), textAlign: TextAlign.center)), + textColor: Theme.of(context).colorScheme.surface, backgroundColor: switch (item.level) { LogLevel.error => null, LogLevel.warn => const Color(0xffffab32), diff --git a/lib/pages/settings/settings_player_history.dart b/lib/pages/settings/settings_player_history.dart index ccaeaed..b02fdc4 100644 --- a/lib/pages/settings/settings_player_history.dart +++ b/lib/pages/settings/settings_player_history.dart @@ -1,6 +1,7 @@ import 'package:api/api.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:ghosten_player/platform_api.dart'; import '../../components/async_image.dart'; import '../../components/future_builder_handler.dart'; @@ -33,6 +34,7 @@ class _SystemSettingsPlayerHistoryState extends State DeviceType.androidTV, - 1 => DeviceType.androidPad, - 2 => DeviceType.androidPhone, + static DeviceType fromString(String? s) { + return switch (s) { + '0' => DeviceType.androidTV, + '1' => DeviceType.androidPad, + '2' => DeviceType.androidPhone, _ => DeviceType.androidPhone, }; } @@ -32,7 +32,6 @@ enum DeviceType { class PlatformApi { static const _channelNamespace = 'com.ghosten.player'; - static const _platform = MethodChannel(_channelNamespace); static Stream pipEvent = kIsWeb ? const Stream.empty() : const EventChannel('$_channelNamespace/pip').receiveBroadcastStream().asBroadcastStream().cast(); static Stream deeplinkEvent = kIsWeb @@ -45,12 +44,6 @@ class PlatformApi { .stream; static late DeviceType deviceType; - static Future getDeviceType() async { - deviceType = DeviceType.fromInt(await _platform.invokeMethod('androidDeviceType')); - } - - static Future get externalUrl => _platform.invokeMethod('externalUrl'); - static bool isAndroidTV() { return deviceType == DeviceType.androidTV; } diff --git a/lib/utils/player.dart b/lib/utils/player.dart index 2be3678..a06ac0b 100644 --- a/lib/utils/player.dart +++ b/lib/utils/player.dart @@ -29,6 +29,6 @@ Future toPlayerCast(BuildContext context, CastDevice device, List("data")!!, call.argument("params")!!, @@ -124,8 +132,25 @@ class ApiPlugin : FlutterPlugin, MethodCallHandler, ActivityAware, ServiceConnec } } }) - activity.runOnUiThread { - eventSinkMap.remove(id)?.endOfStream() + if (data == null) { + activity.runOnUiThread { + eventSinkMap.remove(id)?.endOfStream() + } + } else { + val code = data.substring(0, 3) + val resp = data.substring(3) + activity.runOnUiThread { + if (code == "200") { + eventSinkMap.remove(id)?.endOfStream() + } else { + if (eventSinkMap[id] != null) { + eventSinkMap[id]?.error(code, resolveErrorCode(code), resp) + } else { + errorResp = data + } + eventSinkMap.remove(id)?.endOfStream() + } + } } finished = true } else { @@ -242,10 +267,11 @@ class ApiPlugin : FlutterPlugin, MethodCallHandler, ActivityAware, ServiceConnec "404" -> R.string.api_response_not_found "405" -> R.string.api_response_method_not_allowed "408" -> R.string.api_response_timeout + "409" -> R.string.api_response_conflict "429" -> R.string.api_response_too_many_requests "500" -> R.string.api_response_internal_error "501" -> R.string.api_response_not_implemented - "504" -> R.string.api_response_not_implemented + "504" -> R.string.api_response_gateway_timeout else -> null } return if (message != null) activity.getString(message) else null diff --git a/packages/api/android/src/main/kotlin/com/ghosten/api/ApiService.kt b/packages/api/android/src/main/kotlin/com/ghosten/api/ApiService.kt index 7aa5f8b..1d8a619 100644 --- a/packages/api/android/src/main/kotlin/com/ghosten/api/ApiService.kt +++ b/packages/api/android/src/main/kotlin/com/ghosten/api/ApiService.kt @@ -20,7 +20,7 @@ class ApiService : Service() { private external fun apiStop() external fun apiInitialized(): Boolean external fun call(method: String, data: String, params: String): String - external fun callWithCallback(method: String, data: String, params: String, callback: ApiMethodHandler) + external fun callWithCallback(method: String, data: String, params: String, callback: ApiMethodHandler): String external fun log(level: Int, message: String) var apiStarted = false diff --git a/packages/api/android/src/main/res/values-zh-rCN/strings.xml b/packages/api/android/src/main/res/values-zh-rCN/strings.xml index e7d3be9..15017cc 100644 --- a/packages/api/android/src/main/res/values-zh-rCN/strings.xml +++ b/packages/api/android/src/main/res/values-zh-rCN/strings.xml @@ -4,6 +4,7 @@ 禁止访问 错误请求 请求超时 + Conflict 内部错误 Multiple Choices 请求过于频繁,请稍后再尝试 diff --git a/packages/api/android/src/main/res/values/strings.xml b/packages/api/android/src/main/res/values/strings.xml index d9addb1..4ab2f23 100644 --- a/packages/api/android/src/main/res/values/strings.xml +++ b/packages/api/android/src/main/res/values/strings.xml @@ -4,6 +4,7 @@ Forbidden Bad Request Timeout + Conflict Internal Error Multiple Choices Too Many Requests diff --git a/packages/api/lib/api.dart b/packages/api/lib/api.dart index 5185397..42cc4ca 100644 --- a/packages/api/lib/api.dart +++ b/packages/api/lib/api.dart @@ -85,7 +85,7 @@ class Api { static final updatePlayedStatus = ApiPlatform.instance.updatePlayedStatus; static final setSkipTime = ApiPlatform.instance.setSkipTime; static final checkUpdate = ApiPlatform.instance.checkUpdate; - static final networkDiagnotics = ApiPlatform.instance.networkDiagnotics; + static final networkDiagnostics = ApiPlatform.instance.networkDiagnostics; static final logQueryPage = ApiPlatform.instance.logQueryPage; static final dlnaDiscover = ApiPlatform.instance.dlnaDiscover; static final dlnaSetUri = ApiPlatform.instance.dlnaSetUri; diff --git a/packages/api/lib/src/api_method_channel.dart b/packages/api/lib/src/api_method_channel.dart index 3a81c6e..65854e5 100644 --- a/packages/api/lib/src/api_method_channel.dart +++ b/packages/api/lib/src/api_method_channel.dart @@ -64,6 +64,19 @@ class MethodChannelApi extends ApiPlatform { /// Session End + /// Driver Start + + @override + Stream driverInsert(Json data) async* { + final resp = await client.put('/driver/insert/cb', data: data); + final eventChannel = EventChannel('$_pluginNamespace/update/${resp['id']}'); + yield* eventChannel.receiveBroadcastStream().map((event) => jsonDecode(event)).handleError((error) { + throw ApiException.fromPlatformException(error); + }, test: (error) => error is PlatformException); + } + + /// Driver End + /// Library Start @override @@ -71,10 +84,9 @@ class MethodChannelApi extends ApiPlatform { final data = await client.post('/library/refresh/id/cb', data: {'id': id}); final eventChannel = EventChannel('$_pluginNamespace/update/${data['id']}'); - ApiPlatform.streamController.addStream(eventChannel - .receiveBroadcastStream() - .map((data) => jsonDecode(data)['progress'] as double?) - .concatWith([TimerStream(null, const Duration(seconds: 3))]).distinct()); + ApiPlatform.streamController.addStream(eventChannel.receiveBroadcastStream().map((data) => jsonDecode(data)['progress'] as double?).handleError((error) { + throw ApiException.fromPlatformException(error); + }, test: (error) => error is PlatformException).concatWith([TimerStream(null, const Duration(seconds: 3))]).distinct()); } /// Library End @@ -110,10 +122,15 @@ class MethodChannelApi extends ApiPlatform { } @override - Stream> networkDiagnotics() async* { - final data = await client.post('/network/diagnotics/cb'); + Stream> networkDiagnostics() async* { + final data = await client.post('/network/diagnostics/cb'); final eventChannel = EventChannel('$_pluginNamespace/update/${data['id']}'); - yield* eventChannel.receiveBroadcastStream().map((event) => (jsonDecode(event) as List).map((item) => NetworkDiagnotics.fromJson(item)).toList()); + yield* eventChannel + .receiveBroadcastStream() + .map((event) => (jsonDecode(event) as List).map((item) => NetworkDiagnotics.fromJson(item)).toList()) + .handleError((error) { + throw ApiException.fromPlatformException(error); + }, test: (error) => error is PlatformException); } /// Miscellaneous End @@ -123,7 +140,9 @@ class MethodChannelApi extends ApiPlatform { Stream> dlnaDiscover() async* { final data = await client.post('/dlna/discover/cb'); final eventChannel = EventChannel('$_pluginNamespace/update/${data['id']}'); - yield* eventChannel.receiveBroadcastStream().map((event) => jsonDecode(event)); + yield* eventChannel.receiveBroadcastStream().map((event) => jsonDecode(event) as List).handleError((error) { + throw ApiException.fromPlatformException(error); + }, test: (error) => error is PlatformException); } /// Cast End diff --git a/packages/api/lib/src/api_platform_interface.dart b/packages/api/lib/src/api_platform_interface.dart index 05d2904..c71c56e 100644 --- a/packages/api/lib/src/api_platform_interface.dart +++ b/packages/api/lib/src/api_platform_interface.dart @@ -223,8 +223,8 @@ abstract class ApiPlatform extends PlatformInterface { return data!.map((e) => DriverAccount.fromJson(e)).toList(); } - Future driverInsert(dynamic data) { - return client.put('/driver/insert', data: data); + Stream driverInsert(Json data) async* { + throw UnimplementedError('driverInsert() has not been implemented.'); } Future driverDeleteById(int id) { @@ -449,8 +449,8 @@ abstract class ApiPlatform extends PlatformInterface { }); } - Stream> networkDiagnotics() { - throw UnimplementedError('networkDiagnotics() has not been implemented.'); + Stream> networkDiagnostics() { + throw UnimplementedError('networkDiagnostics() has not been implemented.'); } Future> logQueryPage(int limit, int offset, [(int, int)? range]) async { diff --git a/packages/api/lib/src/api_web.dart b/packages/api/lib/src/api_web.dart index abcb6fc..0b87f93 100644 --- a/packages/api/lib/src/api_web.dart +++ b/packages/api/lib/src/api_web.dart @@ -28,6 +28,34 @@ class ApiWeb extends ApiPlatform { /// Session End + /// Driver Start + + @override + Stream driverInsert(Json data) async* { + final resp = await client.put('/driver/insert/cb', data: data); + if (resp != null) { + final sessionId = resp['id']; + loop: + while (true) { + await Future.delayed(const Duration(milliseconds: 100)); + final session = await sessionStatus(sessionId); + switch (session.status) { + case SessionStatus.progressing: + yield session.data; + case SessionStatus.finished: + break loop; + case SessionStatus.failed: + throw Exception(session.data); + default: + } + } + } else { + throw Exception(resp.error); + } + } + + /// Driver End + /// Library Start @override @@ -68,8 +96,8 @@ class ApiWeb extends ApiPlatform { /// Miscellaneous Start @override - Stream> networkDiagnotics() async* { - final data = await client.post('/network/diagnotics/cb'); + Stream> networkDiagnostics() async* { + final data = await client.post('/network/diagnostics/cb'); if (data != null) { final sessionId = data['id']; loop: diff --git a/packages/api/lib/src/models.dart b/packages/api/lib/src/models.dart index 10c024a..3e9262b 100644 --- a/packages/api/lib/src/models.dart +++ b/packages/api/lib/src/models.dart @@ -660,12 +660,14 @@ enum MediaType { enum DriverType { alipan, + quark, webdav, local; static DriverType fromString(String? name) { return switch (name) { 'alipan' => DriverType.alipan, + 'quark' => DriverType.quark, 'webdav' => DriverType.webdav, 'local' => DriverType.local, _ => throw Exception('Wrong Driver Type of "$name"'), diff --git a/packages/player_view/android/src/main/kotlin/com/ghosten/player_view/PlayerView.kt b/packages/player_view/android/src/main/kotlin/com/ghosten/player_view/PlayerView.kt index c1deeb1..c5a0705 100644 --- a/packages/player_view/android/src/main/kotlin/com/ghosten/player_view/PlayerView.kt +++ b/packages/player_view/android/src/main/kotlin/com/ghosten/player_view/PlayerView.kt @@ -1,7 +1,5 @@ package com.ghosten.player_view -import android.animation.Animator -import android.animation.AnimatorListenerAdapter import android.app.* import android.content.BroadcastReceiver import android.content.Context @@ -15,6 +13,7 @@ import android.os.Build import android.os.Handler import android.os.Looper import android.view.View +import android.widget.FrameLayout import android.widget.Toast import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat @@ -42,7 +41,6 @@ import androidx.media3.ui.DefaultTrackNameProvider import androidx.media3.ui.TrackNameProvider import com.google.common.util.concurrent.MoreExecutors import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.platform.PlatformView import java.io.File import java.io.FileNotFoundException import java.io.FileOutputStream @@ -57,8 +55,8 @@ class PlayerView( extensionRendererMode: Int?, enableDecoderFallback: Boolean?, language: String? -) : PlatformView, - Player.Listener { +) : Player.Listener { + private val mRootView: FrameLayout = activity.findViewById(android.R.id.content) private val mNativeView: View = View.inflate(context, R.layout.player_view, null) private var httpDataSourceFactory = DefaultHttpDataSource.Factory().setUserAgent(USER_AGENT) .setAllowCrossProtocolRedirects(true) @@ -113,6 +111,8 @@ class PlayerView( } })) mNativeView.findViewById(R.id.video_view).player = player + mRootView.addView(mNativeView, 0) + mChannel.invokeMethod("isInitialized", null) mChannel.invokeMethod("volumeChanged", mCurrentVolume.toFloat() / mMaxVolume.toFloat()) checkPlaybackPosition(1000) @@ -246,11 +246,8 @@ class PlayerView( ) } - override fun getView(): View { - return mNativeView - } - - override fun dispose() { + fun dispose() { + mRootView.removeView(mNativeView) player.release() mediaSession.release() cancelNotification() @@ -844,18 +841,6 @@ class PlayerView( ) } - fun hide(result: MethodChannel.Result) { - mNativeView.animate() - .alpha(0f) - .setDuration(200) - .setListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - mNativeView.visibility = View.GONE - result.success(null) - } - }) - } - internal class Video( val type: Int, val url: String, diff --git a/packages/player_view/android/src/main/kotlin/com/ghosten/player_view/PlayerViewPlugin.kt b/packages/player_view/android/src/main/kotlin/com/ghosten/player_view/PlayerViewPlugin.kt index 4b14016..5a3df22 100644 --- a/packages/player_view/android/src/main/kotlin/com/ghosten/player_view/PlayerViewPlugin.kt +++ b/packages/player_view/android/src/main/kotlin/com/ghosten/player_view/PlayerViewPlugin.kt @@ -2,7 +2,6 @@ package com.ghosten.player_view import android.app.Activity import android.app.PictureInPictureParams -import android.content.Context import android.content.pm.PackageManager import android.os.Build import androidx.media3.common.C @@ -11,38 +10,17 @@ import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.StandardMessageCodec -import io.flutter.plugin.platform.PlatformView -import io.flutter.plugin.platform.PlatformViewFactory import java.net.InetAddress import java.net.NetworkInterface import java.util.* -class PlayerViewFactory(private val channel: MethodChannel) : PlatformViewFactory(StandardMessageCodec.INSTANCE) { - lateinit var mPlayerView: PlayerView - lateinit var activity: Activity - override fun create(context: Context, viewId: Int, args: Any?): PlatformView { - mPlayerView = PlayerView( - context, - activity, - channel, - (args as HashMap<*, *>)["extensionRendererMode"] as Int?, - args["enableDecoderFallback"] as Boolean?, - args["language"] as String?, - ) - return mPlayerView - } -} - class PlayerViewPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware { private lateinit var mChannel: MethodChannel - private lateinit var mPlayerViewFactory: PlayerViewFactory private lateinit var activity: Activity + private var mPlayerView: PlayerView? = null override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { - mChannel = MethodChannel(binding.binaryMessenger, "com.ghosten.player_view") + mChannel = MethodChannel(binding.binaryMessenger, "com.ghosten.player/player") mChannel.setMethodCallHandler(this) - mPlayerViewFactory = PlayerViewFactory(mChannel) - binding.platformViewRegistry.registerViewFactory("", mPlayerViewFactory) } override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { @@ -55,18 +33,32 @@ class PlayerViewPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, Activit "requestPip" -> result.success(requestPip()) "getLocalIpAddress" -> result.success(getLocalIpAddress()) else -> { - val mPlayerView = mPlayerViewFactory.mPlayerView when (call.method) { - "play" -> mPlayerView.play() - "pause" -> mPlayerView.pause() - "next" -> mPlayerView.next(call.arguments as Int) - "previous" -> mPlayerView.previous() - "seekTo" -> mPlayerView.seekTo((call.arguments as Int).toLong()) - "setSources" -> mPlayerView.setSources(call.argument("playlist")!!, call.argument("index")!!) - "updateSource" -> mPlayerView.updateSource(call.argument("source")!!, call.argument("index")!!) - "hide" -> return mPlayerView.hide(result) - "setVolume" -> mPlayerView.setVolume((call.arguments as Double).toFloat()) - "setTrack" -> mPlayerView.setTrack( + "init" -> { + if (mPlayerView == null) mPlayerView = PlayerView( + activity.applicationContext, + activity, + mChannel, + call.argument("extensionRendererMode"), + call.argument("enableDecoderFallback"), + call.argument("language"), + ) + } + + "play" -> mPlayerView?.play() + "pause" -> mPlayerView?.pause() + "next" -> mPlayerView?.next(call.arguments as Int) + "previous" -> mPlayerView?.previous() + "seekTo" -> mPlayerView?.seekTo((call.arguments as Int).toLong()) + "updateSource" -> mPlayerView?.updateSource(call.argument("source")!!, call.argument("index")!!) + "setSources" -> mPlayerView?.setSources(call.argument("playlist")!!, call.argument("index")!!) + "dispose" -> { + mPlayerView?.dispose() + mPlayerView = null + } + + "setVolume" -> mPlayerView?.setVolume((call.arguments as Double).toFloat()) + "setTrack" -> mPlayerView?.setTrack( when (call.argument("type")) { "video" -> C.TRACK_TYPE_VIDEO "audio" -> C.TRACK_TYPE_AUDIO @@ -76,17 +68,17 @@ class PlayerViewPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, Activit ) "setSkipPosition" -> - mPlayerView.setSkipPosition( + mPlayerView?.setSkipPosition( call.argument("type")!!, call.argument("list")!! ) - "getVideoThumbnail" -> return mPlayerView.getVideoThumbnail( + "getVideoThumbnail" -> return mPlayerView!!.getVideoThumbnail( result, call.argument("position")!! ) - "setPlaybackSpeed" -> mPlayerView.setPlaybackSpeed((call.arguments as Double).toFloat()) + "setPlaybackSpeed" -> mPlayerView?.setPlaybackSpeed((call.arguments as Double).toFloat()) else -> return result.notImplemented() } result.success(null) @@ -96,7 +88,6 @@ class PlayerViewPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, Activit override fun onAttachedToActivity(binding: ActivityPluginBinding) { activity = binding.activity - mPlayerViewFactory.activity = binding.activity } override fun onDetachedFromActivityForConfigChanges() { @@ -104,7 +95,6 @@ class PlayerViewPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, Activit override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { activity = binding.activity - mPlayerViewFactory.activity = binding.activity } override fun onDetachedFromActivity() { @@ -118,14 +108,14 @@ class PlayerViewPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, Activit } fun requestPip(): Boolean { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (!activity.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) { return false } - if (!mPlayerViewFactory.mPlayerView.canEnterPictureInPicture()) { + if (mPlayerView?.canEnterPictureInPicture() != true) { return false } - val params = mPlayerViewFactory.mPlayerView.getPictureInPictureParams() + val params = mPlayerView?.getPictureInPictureParams() if (params != null) { return activity.enterPictureInPictureMode(params) } else { diff --git a/packages/player_view/android/src/main/res/layout-v26/player_view.xml b/packages/player_view/android/src/main/res/layout-v26/player_view.xml deleted file mode 100644 index 580b379..0000000 --- a/packages/player_view/android/src/main/res/layout-v26/player_view.xml +++ /dev/null @@ -1,8 +0,0 @@ - diff --git a/packages/player_view/android/src/main/res/layout/player_view.xml b/packages/player_view/android/src/main/res/layout/player_view.xml index 09bd44f..72a2755 100644 --- a/packages/player_view/android/src/main/res/layout/player_view.xml +++ b/packages/player_view/android/src/main/res/layout/player_view.xml @@ -5,5 +5,6 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:keepScreenOn="true" + android:background="@android:color/background_dark" app:use_controller="false" - app:surface_type="texture_view"/> + app:surface_type="surface_view"/> diff --git a/packages/player_view/lib/src/player.dart b/packages/player_view/lib/src/player.dart index c257a5c..5c85990 100644 --- a/packages/player_view/lib/src/player.dart +++ b/packages/player_view/lib/src/player.dart @@ -29,7 +29,6 @@ class PlayerController { final ValueNotifier pipMode = ValueNotifier(false); final ValueNotifier isCasting = ValueNotifier(false); final ValueNotifier<(MediaChange, Duration)?> mediaChange = ValueNotifier(null); - bool isInitialized = false; T get currentItem => playlist[index.value]; @@ -92,13 +91,13 @@ class PlayerController { } case 'isInitialized': PlayerPlatform.instance.setSources(playlist.map((item) => item.toSource()).toList(), this.index.value); - isInitialized = true; } }); } void dispose() { PlayerPlatform.instance.setMethodCallHandler(null); + PlayerPlatform.instance.dispose(); index.dispose(); isFirst.dispose(); isLast.dispose(); @@ -166,8 +165,4 @@ class PlayerController { Future updateSource(T source, int index) { return PlayerPlatform.instance.updateSource(source.toSource(), index); } - - Future hide() { - return PlayerPlatform.instance.hide(); - } } diff --git a/packages/player_view/lib/src/player_cast.dart b/packages/player_view/lib/src/player_cast.dart index 7e64457..0260256 100644 --- a/packages/player_view/lib/src/player_cast.dart +++ b/packages/player_view/lib/src/player_cast.dart @@ -1,9 +1,12 @@ import 'dart:async'; +import 'dart:math'; +import 'dart:ui' as ui; import 'dart:ui'; import 'package:animations/animations.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'cast.dart'; @@ -573,8 +576,9 @@ class PlayerCastSearcher extends StatelessWidget { 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(); @@ -583,9 +587,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), @@ -600,30 +606,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: CachedNetworkImage( - imageUrl: widget.background, - fit: BoxFit.cover, - )), + 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/packages/player_view/lib/src/player_controls.dart b/packages/player_view/lib/src/player_controls.dart index b0c2986..c901b23 100644 --- a/packages/player_view/lib/src/player_controls.dart +++ b/packages/player_view/lib/src/player_controls.dart @@ -5,9 +5,7 @@ import 'package:animations/animations.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:rxdart/rxdart.dart'; @@ -302,7 +300,7 @@ class _PlayerControlsState extends State { child: Builder(builder: (context) { return Scaffold( key: _scaffoldKey, - backgroundColor: kIsWeb ? Colors.transparent : Colors.black, + backgroundColor: Colors.transparent, endDrawerEnableOpenDragGesture: false, endDrawer: _buildDraw(context), resizeToAvoidBottomInset: false, @@ -333,7 +331,7 @@ class _PlayerControlsState extends State { floatingActionButtonLocation: FloatingActionButtonLocation.startTop, body: PopScope( canPop: false, - onPopInvoked: (didPop) async { + onPopInvoked: (didPop) { if (didPop) { return; } else if (_scaffoldKey.currentState!.isEndDrawerOpen) { @@ -352,7 +350,6 @@ class _PlayerControlsState extends State { if (widget.onMediaChange != null && _controller.duration.value > Duration.zero) { widget.onMediaChange!(_controller.index.value, _controller.position.value, _controller.duration.value); } - await _controller.hide(); if (context.mounted) Navigator.pop(context); } }, @@ -960,69 +957,30 @@ class _PlayerPlaylistViewState extends State { } } -class PlayerPlatformView extends StatelessWidget { +class PlayerPlatformView extends StatefulWidget { final int? extensionRendererMode; final bool? enableDecoderFallback; const PlayerPlatformView({super.key, this.extensionRendererMode, this.enableDecoderFallback}); @override - Widget build(BuildContext context) { - return kIsWeb - ? const PlatformWebView() - : PlatformViewLink( - viewType: '', - surfaceFactory: (context, controller) { - return AndroidViewSurface( - controller: controller as AndroidViewController, - gestureRecognizers: const >{}, - hitTestBehavior: PlatformViewHitTestBehavior.opaque, - ); - }, - onCreatePlatformView: (params) { - return PlatformViewsService.initSurfaceAndroidView( - id: params.id, - viewType: '', - layoutDirection: TextDirection.ltr, - creationParams: { - 'extensionRendererMode': extensionRendererMode, - 'enableDecoderFallback': enableDecoderFallback, - 'language': Localizations.localeOf(context).languageCode - }, - creationParamsCodec: const StandardMessageCodec(), - onFocus: () { - params.onFocusChanged(true); - }, - ) - ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) - ..create(); - }, - ); - } -} - -class PlatformWebView extends StatefulWidget { - const PlatformWebView({super.key}); - - @override - State createState() => _PlatformWebViewState(); + State createState() => _PlayerPlatformViewState(); } -class _PlatformWebViewState extends State { - @override - void initState() { - PlayerPlatform.instance.initWeb(); - super.initState(); - } - +class _PlayerPlatformViewState extends State { @override void dispose() { - PlayerPlatform.instance.destroyWeb(); + PlayerPlatform.instance.dispose(); super.dispose(); } @override Widget build(BuildContext context) { + PlayerPlatform.instance.init({ + 'extensionRendererMode': widget.extensionRendererMode, + 'enableDecoderFallback': widget.enableDecoderFallback, + 'language': Localizations.localeOf(context).languageCode + }); return const SizedBox(); } } diff --git a/packages/player_view/lib/src/player_method_channel.dart b/packages/player_view/lib/src/player_method_channel.dart index 1e4d388..0e8ff22 100644 --- a/packages/player_view/lib/src/player_method_channel.dart +++ b/packages/player_view/lib/src/player_method_channel.dart @@ -3,7 +3,7 @@ import 'package:flutter/services.dart'; import 'player_platform_interface.dart'; class MethodChannelPlayer extends PlayerPlatform { - final MethodChannel _channel = const MethodChannel('com.ghosten.player_view'); + final MethodChannel _channel = const MethodChannel('com.ghosten.player/player'); MethodChannelPlayer(); @@ -62,11 +62,6 @@ class MethodChannelPlayer extends PlayerPlatform { return _channel.invokeMethod('updateSource', {'source': source, 'index': index}); } - @override - Future hide() { - return _channel.invokeMethod('hide'); - } - @override Future getVideoThumbnail(int position) { return _channel.invokeMethod('getVideoThumbnail', {'position': position}); @@ -86,4 +81,14 @@ class MethodChannelPlayer extends PlayerPlatform { void setMethodCallHandler(Future Function(MethodCall call)? handler) { _channel.setMethodCallHandler(handler); } + + @override + Future init(Map args) { + return _channel.invokeMethod('init', args); + } + + @override + Future dispose() { + return _channel.invokeMethod('dispose'); + } } diff --git a/packages/player_view/lib/src/player_platform_interface.dart b/packages/player_view/lib/src/player_platform_interface.dart index aafc94a..0736eec 100644 --- a/packages/player_view/lib/src/player_platform_interface.dart +++ b/packages/player_view/lib/src/player_platform_interface.dart @@ -65,10 +65,6 @@ abstract class PlayerPlatform extends PlatformInterface { throw UnimplementedError('updateSource() has not been implemented.'); } - Future hide() { - throw UnimplementedError('hide() has not been implemented.'); - } - Future getVideoThumbnail(int position) { throw UnimplementedError('getVideoThumbnail() has not been implemented.'); } @@ -85,11 +81,11 @@ abstract class PlayerPlatform extends PlatformInterface { throw UnimplementedError('setMethodCallHandler() has not been implemented.'); } - void initWeb() { - throw UnimplementedError('setMethodCallHandler() has not been implemented.'); + void init(Map args) { + throw UnimplementedError('init() has not been implemented.'); } - void destroyWeb() { - throw UnimplementedError('setMethodCallHandler() has not been implemented.'); + void dispose() { + throw UnimplementedError('dispose() has not been implemented.'); } } diff --git a/packages/player_view/lib/src/player_web.dart b/packages/player_view/lib/src/player_web.dart index c0b1789..788c23e 100644 --- a/packages/player_view/lib/src/player_web.dart +++ b/packages/player_view/lib/src/player_web.dart @@ -170,9 +170,6 @@ class PlayerWeb extends PlayerPlatform { invoke('set_sources', {'playlist': playlist, 'index': index}); } - @override - Future hide() async {} - @override Future getVideoThumbnail(int position) async { return null; @@ -189,12 +186,12 @@ class PlayerWeb extends PlayerPlatform { } @override - void initWeb() { - invoke('init'); + void init(Map args) { + invoke('init', args); } @override - void destroyWeb() { - invoke('destroy'); + void dispose() { + invoke('dispose'); } } diff --git a/pubspec.lock b/pubspec.lock index 0f83d26..056c5ba 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -726,6 +726,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + webview_cookie_manager: + dependency: "direct main" + description: + name: webview_cookie_manager + sha256: "425a9feac5cd2cb62a71da3dda5ac2eaf9ece5481ee8d79f3868dc5ba8223ad3" + url: "https://pub.dev" + source: hosted + version: "2.0.6" + webview_flutter: + dependency: "direct main" + description: + name: webview_flutter + sha256: "6869c8786d179f929144b4a1f86e09ac0eddfe475984951ea6c634774c16b522" + url: "https://pub.dev" + source: hosted + version: "4.8.0" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + sha256: ed021f27ae621bc97a6019fb601ab16331a3db4bf8afa305e9f6689bdb3edced + url: "https://pub.dev" + source: hosted + version: "3.16.8" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + sha256: d937581d6e558908d7ae3dc1989c4f87b786891ab47bb9df7de548a151779d8d + url: "https://pub.dev" + source: hosted + version: "2.10.0" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + sha256: "9c62cc46fa4f2d41e10ab81014c1de470a6c6f26051a2de32111b2ee55287feb" + url: "https://pub.dev" + source: hosted + version: "3.14.0" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d789915..c316aaa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: ghosten_player description: "Ghosten Player: A Video Player" publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 1.2.0 +version: 1.3.0 environment: sdk: '>=3.4.3 <4.0.0' dependencies: @@ -32,6 +32,8 @@ dependencies: rxdart: ^0.28.0 shared_preferences: ^2.3.3 url_launcher: ^6.3.1 + webview_cookie_manager: ^2.0.6 + webview_flutter: ^4.8.0 dev_dependencies: flutter_test: