From 9aceabd83cc1ed3568e4ac8d6e50f6bb011c698f Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Mon, 27 May 2024 23:07:01 +0800 Subject: [PATCH] :sparkles: Video player! --- ios/Podfile.lock | 19 ++ lib/providers/content/attachment.dart | 19 +- lib/router.dart | 2 +- lib/translations.dart | 2 + lib/widgets/attachments/attachment_item.dart | 163 +++++++++++++----- lib/widgets/attachments/attachment_list.dart | 8 +- macos/Flutter/GeneratedPluginRegistrant.swift | 6 + pubspec.lock | 120 +++++++++++++ pubspec.yaml | 3 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 11 files changed, 300 insertions(+), 46 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index adb0115..21911c8 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -40,6 +40,8 @@ PODS: - Flutter - image_picker_ios (0.0.1): - Flutter + - package_info_plus (0.4.5): + - Flutter - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS @@ -51,6 +53,11 @@ PODS: - SwiftyGif (5.4.5) - url_launcher_ios (0.0.1): - Flutter + - video_player_avfoundation (0.0.1): + - Flutter + - FlutterMacOS + - wakelock_plus (0.0.1): + - Flutter DEPENDENCIES: - file_picker (from `.symlinks/plugins/file_picker/ios`) @@ -58,9 +65,12 @@ DEPENDENCIES: - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) + - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) + - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) SPEC REPOS: trunk: @@ -80,12 +90,18 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_secure_storage/ios" image_picker_ios: :path: ".symlinks/plugins/image_picker_ios/ios" + package_info_plus: + :path: ".symlinks/plugins/package_info_plus/ios" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" + video_player_avfoundation: + :path: ".symlinks/plugins/video_player_avfoundation/darwin" + wakelock_plus: + :path: ".symlinks/plugins/wakelock_plus/ios" SPEC CHECKSUMS: DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c @@ -95,11 +111,14 @@ SPEC CHECKSUMS: flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086 flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 + package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 SDWebImage: dfe95b2466a9823cf9f0c6d01217c06550d7b29a SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe + video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3 + wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 diff --git a/lib/providers/content/attachment.dart b/lib/providers/content/attachment.dart index fcf66e4..2c86e5f 100644 --- a/lib/providers/content/attachment.dart +++ b/lib/providers/content/attachment.dart @@ -30,6 +30,8 @@ class AttachmentProvider extends GetConnect { httpClient.baseUrl = ServiceFinder.services['paperclip']; } + static Map mimetypeOverrides = {'mov': 'video/quicktime'}; + Future getMetadata(int id) => get('/api/attachments/$id/meta'); Future createAttachment(File file, String hash, String usage, @@ -46,7 +48,15 @@ class AttachmentProvider extends GetConnect { final fileAlt = basename(file.path).contains('.') ? basename(file.path).substring(0, basename(file.path).lastIndexOf('.')) : basename(file.path); + final fileExt = basename(file.path) + .substring(basename(file.path).lastIndexOf('.') + 1) + .toLowerCase(); + // Override for some files cannot be detected mimetype by server-side + String? mimetypeOverride; + if (mimetypeOverrides.keys.contains(fileExt)) { + mimetypeOverride = mimetypeOverrides[fileExt]; + } final resp = await client.post( '/api/attachments', FormData({ @@ -54,16 +64,17 @@ class AttachmentProvider extends GetConnect { 'file': filePayload, 'hash': hash, 'usage': usage, + if (mimetypeOverride != null) 'mimetype': mimetypeOverride, 'metadata': jsonEncode({ if (ratio != null) 'ratio': ratio, }), }), ); - if (resp.statusCode == 200) { - return resp; + if (resp.statusCode != 200) { + throw Exception(resp.bodyString); } - throw Exception(resp.bodyString); + return resp; } Future updateAttachment( @@ -93,7 +104,7 @@ class AttachmentProvider extends GetConnect { throw Exception(resp.bodyString); } - return resp.body; + return resp; } Future deleteAttachment(int id) async { diff --git a/lib/router.dart b/lib/router.dart index dba61f9..dcf72c3 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -42,7 +42,7 @@ abstract class AppRouter { BasicShell(state: state, child: child), routes: [ GoRoute( - path: '/posts/:alias', + path: '/posts/view/:alias', name: 'postDetail', builder: (context, state) => PostDetailScreen( alias: state.pathParameters['alias']!, diff --git a/lib/translations.dart b/lib/translations.dart index 78a3f27..1c01ccf 100644 --- a/lib/translations.dart +++ b/lib/translations.dart @@ -21,6 +21,7 @@ class SolianMessages extends Translations { 'search': 'Search', 'reply': 'Reply', 'repost': 'Repost', + 'openInBrowser': 'Open in browser', 'notification': 'Notification', 'errorHappened': 'An error occurred', 'email': 'Email', @@ -134,6 +135,7 @@ class SolianMessages extends Translations { 'search': '搜索', 'reply': '回复', 'repost': '转帖', + 'openInBrowser': '在浏览器中打开', 'notification': '通知', 'errorHappened': '发生错误了', 'email': '邮件地址', diff --git a/lib/widgets/attachments/attachment_item.dart b/lib/widgets/attachments/attachment_item.dart index f624fe8..80253dc 100644 --- a/lib/widgets/attachments/attachment_item.dart +++ b/lib/widgets/attachments/attachment_item.dart @@ -1,9 +1,12 @@ +import 'package:chewie/chewie.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:solian/models/attachment.dart'; import 'package:solian/services.dart'; +import 'package:url_launcher/url_launcher_string.dart'; +import 'package:video_player/video_player.dart'; -class AttachmentItem extends StatelessWidget { +class AttachmentItem extends StatefulWidget { final String parentId; final Attachment item; final bool showBadge; @@ -24,45 +27,127 @@ class AttachmentItem extends StatelessWidget { }); @override - Widget build(BuildContext context) { - return Hero( - tag: Key('a${item.uuid}p$parentId'), - child: Stack( - fit: StackFit.expand, - children: [ - Image.network( - '${ServiceFinder.services['paperclip']}/api/attachments/${item.id}', - fit: fit, - ), - if (showBadge && badge != null) - Positioned( - right: 12, - bottom: 8, - child: Material( - color: Colors.transparent, - child: Chip(label: Text(badge!)), - ), - ), - if (showHideButton && item.isMature) - Positioned( - top: 8, - left: 12, - child: Material( - color: Colors.transparent, - child: ActionChip( - visualDensity: - const VisualDensity(vertical: -4, horizontal: -4), - avatar: Icon(Icons.visibility_off, - color: Theme.of(context).colorScheme.onSurfaceVariant), - label: Text('hide'.tr), - onPressed: () { - if (onHide != null) onHide!(); - }, - ), - ), - ), - ], + State createState() => _AttachmentItemState(); +} + +class _AttachmentItemState extends State { + VideoPlayerController? _videoPlayerController; + ChewieController? _chewieController; + + void ensureInitVideo() { + if (_videoPlayerController != null) return; + + _videoPlayerController = VideoPlayerController.networkUrl(Uri.parse( + '${ServiceFinder.services['paperclip']}/api/attachments/${widget.item.id}', + )); + _videoPlayerController!.initialize(); + _chewieController = ChewieController( + aspectRatio: widget.item.metadata?['ratio'] ?? 16 / 9, + videoPlayerController: _videoPlayerController!, + customControls: const MaterialControls(showPlayButton: true), + materialProgressColors: ChewieProgressColors( + playedColor: Theme.of(context).colorScheme.primary, + handleColor: Theme.of(context).colorScheme.primary, ), ); } + + @override + Widget build(BuildContext context) { + switch (widget.item.mimetype.split('/').first) { + case 'image': + return Hero( + tag: Key('a${widget.item.uuid}p${widget.parentId}'), + child: Stack( + fit: StackFit.expand, + children: [ + Image.network( + '${ServiceFinder.services['paperclip']}/api/attachments/${widget.item.id}', + fit: widget.fit, + ), + if (widget.showBadge && widget.badge != null) + Positioned( + right: 12, + bottom: 8, + child: Material( + color: Colors.transparent, + child: Chip(label: Text(widget.badge!)), + ), + ), + if (widget.showHideButton && widget.item.isMature) + Positioned( + top: 8, + left: 12, + child: Material( + color: Colors.transparent, + child: ActionChip( + visualDensity: + const VisualDensity(vertical: -4, horizontal: -4), + avatar: Icon(Icons.visibility_off, + color: + Theme.of(context).colorScheme.onSurfaceVariant), + label: Text('hide'.tr), + onPressed: () { + if (widget.onHide != null) widget.onHide!(); + }, + ), + ), + ), + ], + ), + ); + case 'video': + ensureInitVideo(); + return Chewie(controller: _chewieController!); + default: + return Center( + child: Container( + constraints: const BoxConstraints( + maxWidth: 280, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.file_present, size: 32), + const SizedBox(height: 6), + Text( + widget.item.mimetype, + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 2), + Text( + widget.item.alt, + style: const TextStyle(fontSize: 13), + textAlign: TextAlign.center, + ), + const SizedBox(height: 12), + TextButton.icon( + icon: const Icon(Icons.launch), + label: Text('openInBrowser'.tr), + style: const ButtonStyle( + visualDensity: VisualDensity(vertical: -2, horizontal: -4), + ), + onPressed: () { + launchUrlString( + '${ServiceFinder.services['paperclip']}/api/attachments/${widget.item.id}', + ); + }, + ), + ], + ), + ), + ); + } + } + + @override + void dispose() { + _videoPlayerController?.dispose(); + _chewieController?.dispose(); + super.dispose(); + } } diff --git a/lib/widgets/attachments/attachment_list.dart b/lib/widgets/attachments/attachment_list.dart index 5cb25c9..3606877 100644 --- a/lib/widgets/attachments/attachment_list.dart +++ b/lib/widgets/attachments/attachment_list.dart @@ -57,7 +57,11 @@ class _AttachmentListState extends State { int portrait = 0, square = 0, landscape = 0; for (var entry in _attachmentsMeta) { if (entry!.metadata?['ratio'] != null) { - consistentValue ??= entry.metadata?['ratio']; + if (entry.metadata?['ratio'] is int) { + consistentValue ??= entry.metadata?['ratio'].toDouble(); + } else { + consistentValue ??= entry.metadata?['ratio']; + } if (isConsistent && entry.metadata?['ratio'] != consistentValue) { isConsistent = false; } @@ -180,7 +184,7 @@ class _AttachmentListState extends State { onTap: () { if (!_showMature && _attachmentsMeta.any((e) => e!.isMature)) { setState(() => _showMature = true); - } else { + } else if (['image'].contains(element.mimetype.split('/').first)) { Navigator.of(context, rootNavigator: true).push( MaterialPageRoute( builder: (context) => AttachmentListFullScreen( diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index ed5dfb7..5523396 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,13 +8,19 @@ import Foundation import file_selector_macos import flutter_local_notifications import flutter_secure_storage_macos +import package_info_plus import path_provider_foundation import url_launcher_macos +import video_player_avfoundation +import wakelock_plus func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) + WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index fbe7423..006cae7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -49,6 +49,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + chewie: + dependency: "direct main" + description: + name: chewie + sha256: e53da939709efb9aad0f3d72a69a8d05f889168b7a138af60ce78bab5c94b135 + url: "https://pub.dev" + source: hosted + version: "1.8.1" clock: dependency: transitive description: @@ -81,6 +89,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + csslib: + dependency: transitive + description: + name: csslib + sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" + url: "https://pub.dev" + source: hosted + version: "1.0.0" cupertino_icons: dependency: "direct main" description: @@ -328,6 +344,14 @@ packages: url: "https://pub.dev" source: hosted version: "14.1.3" + html: + dependency: transitive + description: + name: html + sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" + url: "https://pub.dev" + source: hosted + version: "0.15.4" http: dependency: transitive description: @@ -520,6 +544,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" oauth2: dependency: "direct main" description: @@ -528,6 +560,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + package_info_plus: + dependency: transitive + description: + name: package_info_plus + sha256: b93d8b4d624b4ea19b0a5a208b2d6eff06004bc3ce74c06040b120eeadd00ce0 + url: "https://pub.dev" + source: hosted + version: "8.0.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: f49918f3433a3146047372f9d4f1f847511f2acd5cd030e1f44fe5a50036b70e + url: "https://pub.dev" + source: hosted + version: "3.0.0" path: dependency: "direct main" description: @@ -656,6 +704,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + provider: + dependency: transitive + description: + name: provider + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + url: "https://pub.dev" + source: hosted + version: "6.1.2" sky_engine: dependency: transitive description: flutter @@ -829,6 +885,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + video_player: + dependency: "direct main" + description: + name: video_player + sha256: db6a72d8f4fd155d0189845678f55ad2fd54b02c10dcafd11c068dbb631286c0 + url: "https://pub.dev" + source: hosted + version: "2.8.6" + video_player_android: + dependency: transitive + description: + name: video_player_android + sha256: "134e1ad410d67e18a19486ed9512c72dfc6d8ffb284d0e8f2e99e903d1ba8fa3" + url: "https://pub.dev" + source: hosted + version: "2.4.14" + video_player_avfoundation: + dependency: transitive + description: + name: video_player_avfoundation + sha256: d1e9a824f2b324000dc8fb2dcb2a3285b6c1c7c487521c63306cc5b394f68a7c + url: "https://pub.dev" + source: hosted + version: "2.6.1" + video_player_platform_interface: + dependency: transitive + description: + name: video_player_platform_interface + sha256: "236454725fafcacf98f0f39af0d7c7ab2ce84762e3b63f2cbb3ef9a7e0550bc6" + url: "https://pub.dev" + source: hosted + version: "6.2.2" + video_player_web: + dependency: transitive + description: + name: video_player_web + sha256: "41245cef5ef29c4585dbabcbcbe9b209e34376642c7576cabf11b4ad9289d6e4" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + video_player_win: + dependency: "direct main" + description: + name: video_player_win + sha256: b99e8dfe7fa87a6732b5c91caef4955ae3453fd6255fb8860a7686beccdcfe43 + url: "https://pub.dev" + source: hosted + version: "2.3.6" vm_service: dependency: transitive description: @@ -837,6 +941,22 @@ packages: url: "https://pub.dev" source: hosted version: "14.2.1" + wakelock_plus: + dependency: transitive + description: + name: wakelock_plus + sha256: "14758533319a462ffb5aa3b7ddb198e59b29ac3b02da14173a1715d65d4e6e68" + url: "https://pub.dev" + source: hosted + version: "1.2.5" + wakelock_plus_platform_interface: + dependency: transitive + description: + name: wakelock_plus_platform_interface + sha256: "422d1cdbb448079a8a62a5a770b69baa489f8f7ca21aef47800c726d404f9d16" + url: "https://pub.dev" + source: hosted + version: "1.2.1" web: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index e73d5ae..85edd0b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -57,6 +57,9 @@ dependencies: permission_handler: ^11.3.1 uuid: ^4.4.0 dropdown_button2: ^2.3.9 + video_player: ^2.8.6 + video_player_win: ^2.3.6 + chewie: ^1.8.1 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 5206490..0953d8d 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -10,6 +10,7 @@ #include #include #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { FileSelectorWindowsRegisterWithRegistrar( @@ -20,4 +21,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); + VideoPlayerWinPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("VideoPlayerWinPluginCApi")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 53447da..65857b3 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_secure_storage_windows permission_handler_windows url_launcher_windows + video_player_win ) list(APPEND FLUTTER_FFI_PLUGIN_LIST