diff --git a/b0esche_cloud/lib/injection.dart b/b0esche_cloud/lib/injection.dart index e550e5b..1a99e9c 100644 --- a/b0esche_cloud/lib/injection.dart +++ b/b0esche_cloud/lib/injection.dart @@ -1,4 +1,4 @@ -import 'package:b0esche_cloud/services/api_client.dart'; +import 'package:b0esche.cloud/services/api_client.dart'; import 'package:get_it/get_it.dart'; import 'blocs/session/session_bloc.dart'; import 'repositories/auth_repository.dart'; diff --git a/b0esche_cloud/lib/pages/file_explorer.dart b/b0esche_cloud/lib/pages/file_explorer.dart index b389fd9..64eb3b9 100644 --- a/b0esche_cloud/lib/pages/file_explorer.dart +++ b/b0esche_cloud/lib/pages/file_explorer.dart @@ -22,6 +22,7 @@ import '../theme/app_theme.dart'; import '../theme/modern_glass_button.dart'; import 'document_viewer.dart'; import 'editor_page.dart'; +import 'video_viewer.dart'; import '../injection.dart'; import '../services/file_service.dart'; @@ -1376,6 +1377,39 @@ class _FileExplorerState extends State { final isSelected = _selectedFilePath == file.path; final isHovered = _hovered[file.path] ?? false; + // Video file detection + final videoExtensions = [ + '.mp4', + '.webm', + '.mov', + '.avi', + '.mkv', + '.flv', + '.wmv', + '.m4v', + ]; + final isVideo = videoExtensions.any( + (ext) => file.name.toLowerCase().endsWith(ext), + ); + if (isVideo) { + return GestureDetector( + onTap: () async { + final videoUrl = await getIt() + .getFileUrl( + orgId: widget.orgId, + fileId: file.id ?? '', + ); + _showVideoViewer(file.name, videoUrl); + }, + child: _buildFileItem( + file, + isSelected, + isHovered, + false, + ), + ); + } + if (file.type == FileType.folder) { return DragTarget( builder: (context, candidate, rejected) { @@ -1581,4 +1615,18 @@ class _FileExplorerState extends State { }, ); } + + void _showVideoViewer(String fileName, String videoUrl) { + showDialog( + context: context, + barrierDismissible: false, + barrierColor: Colors.transparent, + builder: (BuildContext context) { + return BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: VideoViewer(videoUrl: videoUrl, fileName: fileName), + ); + }, + ); + } } diff --git a/b0esche_cloud/lib/pages/home_page.dart b/b0esche_cloud/lib/pages/home_page.dart index 3f0c14e..3527fcc 100644 --- a/b0esche_cloud/lib/pages/home_page.dart +++ b/b0esche_cloud/lib/pages/home_page.dart @@ -1,5 +1,5 @@ import 'dart:ui' as ui; -import 'package:b0esche_cloud/blocs/organization/organization_state.dart'; +import '../blocs/organization/organization_state.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../blocs/auth/auth_bloc.dart'; diff --git a/b0esche_cloud/lib/pages/video_viewer.dart b/b0esche_cloud/lib/pages/video_viewer.dart new file mode 100644 index 0000000..a7cf41a --- /dev/null +++ b/b0esche_cloud/lib/pages/video_viewer.dart @@ -0,0 +1,140 @@ +import 'package:flutter/material.dart'; +import 'package:video_player/video_player.dart'; +import '../theme/app_theme.dart'; + +class VideoViewer extends StatefulWidget { + final String videoUrl; + final String fileName; + + const VideoViewer({ + super.key, + required this.videoUrl, + required this.fileName, + }); + + @override + State createState() => _VideoViewerState(); +} + +class _VideoViewerState extends State { + late VideoPlayerController _controller; + bool _isInitialized = false; + bool _isError = false; + + @override + void initState() { + super.initState(); + _controller = VideoPlayerController.network(widget.videoUrl) + ..initialize() + .then((_) { + if (mounted) setState(() => _isInitialized = true); + }) + .catchError((_) { + if (mounted) setState(() => _isError = true); + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: Colors.transparent, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 700, maxHeight: 500), + child: Container( + decoration: AppTheme.glassDecoration, + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Expanded( + child: Text( + widget.fileName, + style: const TextStyle( + color: AppTheme.primaryText, + fontWeight: FontWeight.bold, + fontSize: 18, + ), + overflow: TextOverflow.ellipsis, + ), + ), + IconButton( + icon: const Icon(Icons.close, color: AppTheme.primaryText), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ), + const SizedBox(height: 16), + if (_isError) + const Text( + 'Failed to load video', + style: TextStyle(color: Colors.red), + ) + else if (!_isInitialized) + const Center( + child: CircularProgressIndicator(color: AppTheme.accentColor), + ) + else + AspectRatio( + aspectRatio: _controller.value.aspectRatio, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + VideoPlayer(_controller), + _ControlsOverlay(controller: _controller), + VideoProgressIndicator( + _controller, + allowScrubbing: true, + colors: VideoProgressColors( + playedColor: AppTheme.accentColor, + backgroundColor: AppTheme.secondaryText.withOpacity( + 0.2, + ), + bufferedColor: AppTheme.secondaryText.withOpacity( + 0.5, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} + +class _ControlsOverlay extends StatelessWidget { + final VideoPlayerController controller; + const _ControlsOverlay({required this.controller}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + controller.value.isPlaying ? controller.pause() : controller.play(); + }, + child: Stack( + children: [ + if (!controller.value.isPlaying) + Center( + child: Icon( + Icons.play_arrow, + size: 64, + color: AppTheme.accentColor.withOpacity(0.8), + ), + ), + ], + ), + ); + } +} diff --git a/b0esche_cloud/lib/services/file_service.dart b/b0esche_cloud/lib/services/file_service.dart index b81aae8..64b434c 100644 --- a/b0esche_cloud/lib/services/file_service.dart +++ b/b0esche_cloud/lib/services/file_service.dart @@ -116,6 +116,18 @@ class FileService { return '/orgs/$orgId/files/download?path=${Uri.encodeComponent(path)}'; } + Future getFileUrl({ + required String orgId, + required String fileId, + String? fileName, + }) async { + // If you have a direct fileId-based endpoint, use it. Otherwise, fallback to path-based if needed. + if (orgId.isEmpty) { + return '/user/files/download?id=${Uri.encodeComponent(fileId)}'; + } + return '/orgs/$orgId/files/download?id=${Uri.encodeComponent(fileId)}'; + } + Future createFolder( String orgId, String parentPath, diff --git a/b0esche_cloud/macos/Flutter/GeneratedPluginRegistrant.swift b/b0esche_cloud/macos/Flutter/GeneratedPluginRegistrant.swift index a80bc25..f1735bf 100644 --- a/b0esche_cloud/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/b0esche_cloud/macos/Flutter/GeneratedPluginRegistrant.swift @@ -17,6 +17,7 @@ import sqflite_darwin import super_native_extensions import syncfusion_pdfviewer_macos import url_launcher_macos +import video_player_avfoundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) @@ -31,4 +32,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { SuperNativeExtensionsPlugin.register(with: registry.registrar(forPlugin: "SuperNativeExtensionsPlugin")) SyncfusionFlutterPdfViewerPlugin.register(with: registry.registrar(forPlugin: "SyncfusionFlutterPdfViewerPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) } diff --git a/b0esche_cloud/pubspec.lock b/b0esche_cloud/pubspec.lock index 9692117..57d93c6 100644 --- a/b0esche_cloud/pubspec.lock +++ b/b0esche_cloud/pubspec.lock @@ -249,6 +249,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.7" + csslib: + dependency: transitive + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" dart_style: dependency: transitive description: @@ -560,6 +568,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + html: + dependency: transitive + description: + name: html + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" + url: "https://pub.dev" + source: hosted + version: "0.15.6" http: dependency: "direct main" description: @@ -1421,6 +1437,46 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + video_player: + dependency: "direct main" + description: + name: video_player + sha256: "096bc28ce10d131be80dfb00c223024eb0fba301315a406728ab43dd99c45bdf" + url: "https://pub.dev" + source: hosted + version: "2.10.1" + video_player_android: + dependency: transitive + description: + name: video_player_android + sha256: ee4fd520b0cafa02e4a867a0f882092e727cdaa1a2d24762171e787f8a502b0a + url: "https://pub.dev" + source: hosted + version: "2.9.1" + video_player_avfoundation: + dependency: transitive + description: + name: video_player_avfoundation + sha256: d1eb970495a76abb35e5fa93ee3c58bd76fb6839e2ddf2fbb636674f2b971dd4 + url: "https://pub.dev" + source: hosted + version: "2.8.9" + video_player_platform_interface: + dependency: transitive + description: + name: video_player_platform_interface + sha256: "57c5d73173f76d801129d0531c2774052c5a7c11ccb962f1830630decd9f24ec" + url: "https://pub.dev" + source: hosted + version: "6.6.0" + video_player_web: + dependency: transitive + description: + name: video_player_web + sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb" + url: "https://pub.dev" + source: hosted + version: "2.4.0" vm_service: dependency: transitive description: diff --git a/b0esche_cloud/pubspec.yaml b/b0esche_cloud/pubspec.yaml index 4239cf9..912e0f7 100644 --- a/b0esche_cloud/pubspec.yaml +++ b/b0esche_cloud/pubspec.yaml @@ -1,5 +1,5 @@ -name: b0esche_cloud -description: "A new Flutter project." +name: b0esche.cloud +description: "become cloud." publish_to: "none" version: 0.1.0 @@ -59,6 +59,9 @@ dependencies: http: ^1.2.0 archive: ^4.0.4 + # Video Playback + video_player: ^2.8.2 + dev_dependencies: flutter_test: sdk: flutter