diff --git a/story-player/.vscode/launch.json b/story-player/.vscode/launch.json index 01ad9f2..e5581aa 100644 --- a/story-player/.vscode/launch.json +++ b/story-player/.vscode/launch.json @@ -5,18 +5,18 @@ "version": "0.2.0", "configurations": [ { - "name": "flutter_launcher", + "name": "story_player", "request": "launch", "type": "dart" }, { - "name": "flutter_launcher (profile mode)", + "name": "story_player (profile mode)", "request": "launch", "type": "dart", "flutterMode": "profile" }, { - "name": "flutter_launcher (release mode)", + "name": "story_player (release mode)", "request": "launch", "type": "dart", "flutterMode": "release" diff --git a/story-player/README.md b/story-player/README.md index c1949b3..97560a0 100644 --- a/story-player/README.md +++ b/story-player/README.md @@ -1,16 +1,4 @@ -# flutter_launcher +# Story Player -A new Flutter project. +See /docs or website to learn more. -## Getting Started - -This project is a starting point for a Flutter application. - -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) - -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. diff --git a/story-player/android/app/build.gradle b/story-player/android/app/build.gradle index 19ca63c..e1da9c7 100644 --- a/story-player/android/app/build.gradle +++ b/story-player/android/app/build.gradle @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - namespace "com.example.flutter_launcher" + namespace "org.openstoryteller.story_player" compileSdkVersion flutter.compileSdkVersion ndkVersion flutter.ndkVersion @@ -45,7 +45,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "com.example.flutter_launcher" + applicationId "org.openstoryteller.story_player" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. minSdkVersion flutter.minSdkVersion diff --git a/story-player/android/app/src/main/AndroidManifest.xml b/story-player/android/app/src/main/AndroidManifest.xml index ea9b44e..e6c7ac5 100644 --- a/story-player/android/app/src/main/AndroidManifest.xml +++ b/story-player/android/app/src/main/AndroidManifest.xml @@ -1,12 +1,17 @@ - + + + + + p.basename(file.path)).toList(); + final json = {'files': files}; + return Response.ok(jsonEncode(json), + headers: {'Content-Type': 'application/json'}); + }); + + // Démarrer le serveur + io.serve(router, 'localhost', 8080).then((server) { + logger.d('Serving at http://${server.address.host}:${server.port}'); + }); +} + +String? _extractFilename(String contentDisposition) { + return RegExp('filename="([^"]*)"').firstMatch(contentDisposition)?.group(1); +} + +Future _saveFile(MimeMultipart part, String filename) async { + var file = File(filename); + await file.create(recursive: true); + await part.pipe(file.openWrite()); +} + +void udpServer() async { + // Créer un socket UDP + var port = 8080; // Vous pouvez spécifier le port de votre choix + var address = + InternetAddress.anyIPv4; // Écouter sur toutes les interfaces IPv4 + RawDatagramSocket socket = await RawDatagramSocket.bind(address, port); + logger.d('Serveur UDP écoutant sur ${address.address}:$port'); + + // Gérer les événements du socket + socket.listen((RawSocketEvent event) { + if (event == RawSocketEvent.read) { + Datagram? datagram = socket.receive(); + if (datagram != null) { + var message = utf8.decode(datagram.data); + logger.d( + 'Message reçu de ${datagram.address.address}:${datagram.port}: $message'); + + // Envoyer une réponse + String response = 'Reçu: $message'; + List data = utf8.encode(response); + socket.send(data, datagram.address, datagram.port); + logger.d( + 'Réponse envoyée à ${datagram.address.address}:${datagram.port}'); + } + } + }); +} + +Future printIps() async { + for (var interface in await NetworkInterface.list()) { + logger.d('== Interface: ${interface.name} =='); + for (var addr in interface.addresses) { + logger.d( + '${addr.address} ${addr.host} ${addr.isLoopback} ${addr.rawAddress} ${addr.type.name}'); + } + } +} diff --git a/story-player/lib/libstory/indexfile.dart b/story-player/lib/libstory/indexfile.dart index 55d7574..0ab5cb5 100644 --- a/story-player/lib/libstory/indexfile.dart +++ b/story-player/lib/libstory/indexfile.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:logger/logger.dart'; +import 'package:permission_handler/permission_handler.dart'; var logger = Logger(printer: PrettyPrinter(methodCount: 0)); @@ -117,14 +118,33 @@ class IndexFile { Future loadIndexFile(String libraryRoot) async { libraryPath = libraryRoot; indexFileIsValid = false; - final file = File('$libraryRoot/index.ost'); - if (!await file.exists()) { - logger.d('Le fichier n\'existe pas.'); - return false; - } + String indexFileName = '$libraryRoot/index.ost'; + + bool isGranted = true; + + // if (Platform.isAndroid) { + + // if (await Permission.manageExternalStorage.request().isGranted) { + // isGranted = true; + // } + // } else { + // isGranted = true; + // } + + if (isGranted) { + final file = File(indexFileName); + if (!await file.exists()) { + logger.d('Le fichier n\'existe pas.'); + return false; + } + indexFileBuffer = file.readAsBytesSync(); + } else { + logger.e("Cannot access to file: $indexFileName"); + } + // Ouvrir le fichier en mode lecture binaire - indexFileBuffer = file.readAsBytesSync(); + readPtr = 0; indexFileStream = ByteData.sublistView(indexFileBuffer, readPtr); // start at zero diff --git a/story-player/lib/main.dart b/story-player/lib/main.dart index 2847c9b..939f82a 100644 --- a/story-player/lib/main.dart +++ b/story-player/lib/main.dart @@ -3,113 +3,35 @@ import 'package:flutter/material.dart' hide Router; import 'dart:io'; import 'dart:convert'; import 'dart:async'; +import 'dart:typed_data'; -import 'package:shelf/shelf.dart'; -import 'package:shelf/shelf_io.dart' as io; -import 'package:shelf_router/shelf_router.dart'; -import 'package:mime/mime.dart'; -import 'package:path/path.dart' as p; -import 'package:http_parser/http_parser.dart'; // y'a un warning mais il faut laisser cet import import 'package:saf/saf.dart'; import 'package:path_provider/path_provider.dart'; import 'package:audioplayers/audioplayers.dart'; import 'package:file_picker/file_picker.dart'; +import 'package:external_path/external_path.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:logger/logger.dart'; +import 'package:file_picker/file_picker.dart'; import 'libstory/storyvm.dart'; import 'libstory/indexfile.dart'; +import 'httpserver.dart'; -import 'package:logger/logger.dart'; - -var logger = Logger(printer: PrettyPrinter(methodCount: 0)); - -void httpServer() async { - final router = Router(); - - // Route pour uploader des fichiers - router.post('/upload', (Request request) async { - var contentType = MediaType.parse(request.headers['content-type']!); - var boundary = contentType.parameters['boundary']!; - var transformer = MimeMultipartTransformer(boundary); - var bodyStream = request.read(); - var parts = await transformer.bind(bodyStream).toList(); - - for (var part in parts) { - var contentDisp = part.headers['content-disposition']!; - var filename = _extractFilename(contentDisp); - if (filename != null) { - var filePath = p.join('uploads', filename); - await _saveFile(part, filePath); - logger.d('File uploaded: $filePath'); - } - } - - return Response.ok('File uploaded successfully'); - }); - - // Route pour lister les fichiers - router.get('/files', (Request request) async { - final directory = Directory('uploads'); - final files = - directory.listSync().map((file) => p.basename(file.path)).toList(); - final json = {'files': files}; - return Response.ok(jsonEncode(json), - headers: {'Content-Type': 'application/json'}); - }); - - // Démarrer le serveur - io.serve(router, 'localhost', 8080).then((server) { - logger.d('Serving at http://${server.address.host}:${server.port}'); - }); -} - -String? _extractFilename(String contentDisposition) { - return RegExp('filename="([^"]*)"').firstMatch(contentDisposition)?.group(1); -} - -Future _saveFile(MimeMultipart part, String filename) async { - var file = File(filename); - await file.create(recursive: true); - await part.pipe(file.openWrite()); -} - -void udpServer() async { - // Créer un socket UDP - var port = 8080; // Vous pouvez spécifier le port de votre choix - var address = - InternetAddress.anyIPv4; // Écouter sur toutes les interfaces IPv4 - RawDatagramSocket socket = await RawDatagramSocket.bind(address, port); - logger.d('Serveur UDP écoutant sur ${address.address}:$port'); - - // Gérer les événements du socket - socket.listen((RawSocketEvent event) { - if (event == RawSocketEvent.read) { - Datagram? datagram = socket.receive(); - if (datagram != null) { - var message = utf8.decode(datagram.data); - logger.d( - 'Message reçu de ${datagram.address.address}:${datagram.port}: $message'); - - // Envoyer une réponse - String response = 'Reçu: $message'; - List data = utf8.encode(response); - socket.send(data, datagram.address, datagram.port); - logger.d( - 'Réponse envoyée à ${datagram.address.address}:${datagram.port}'); - } - } - }); -} - -Future printIps() async { - for (var interface in await NetworkInterface.list()) { - logger.d('== Interface: ${interface.name} =='); - for (var addr in interface.addresses) { - logger.d( - '${addr.address} ${addr.host} ${addr.isLoopback} ${addr.rawAddress} ${addr.type.name}'); - } +class ProductionFilter extends LogFilter { + @override + bool shouldLog(LogEvent event) { + return true; } } +var logger = Logger( + printer: PrettyPrinter(methodCount: 0), + filter: ProductionFilter(), // Use the ProductionFilter to enable logging in release mode + +); + + void main() { StoryVm.loadLibrary(); StoryVm.initialize(); @@ -182,6 +104,13 @@ class _MyHomePageState extends State { PlayerState state = PlayerState.disabled; StreamSubscription? audioPlayerSub; + Image img = const Image(image: AssetImage('assets/320x240.png')); + + // final Permission _permission = Permission.storage; + PermissionStatus _permissionStatus = PermissionStatus.denied; + + var _openResult = 'Unknown'; + void initPaths() async { Directory? dir; @@ -206,6 +135,8 @@ class _MyHomePageState extends State { } }); + img = Image.file(File(currentImage)); + if (event.sound.isNotEmpty) { player.play(DeviceFileSource('${indexFile.getCurrentStoryPath()}/assets/${event.sound}')); } @@ -221,17 +152,53 @@ class _MyHomePageState extends State { state = PlayerState.indexFile; } - void showCurrentStory() async { + Uint8List soundBuffer = Uint8List(0); + + void showCurrentStoryIndex() async { setState(() { currentImage = indexFile.getCurrentTitleImage(); logger.d('Current image: $currentImage'); }); - var asset = DeviceFileSource(indexFile.getCurrentSoundImage()); - logger.d('Asset: ${asset.toString()}'); - await player.play(asset); + img = Image.file(File(currentImage)); + + // File sound = File(indexFile.getCurrentSoundImage()); + // soundBuffer = sound.readAsBytesSync(); + // // logger.d('Asset: ${asset.toString()}'); + // await player.play(BytesSource(soundBuffer)); + + player.play(DeviceFileSource(indexFile.getCurrentSoundImage())); } + + void chooseLibraryDirectory() async { + // FilePickerResult? result = await FilePicker.platform.pickFiles(); + String ?path = await FilePicker.platform.getDirectoryPath(); + + if (path != null) { + logger.d("Selected directory: $path"); + + setState(() { + myPath = path; + }); + bool success = await indexFile.loadIndexFile(path); + if (success) { + showCurrentStoryIndex(); + state = PlayerState.indexFile; + } + } + } + + void handleClick(String value) async { + switch (value) { + case 'Library': + chooseLibraryDirectory(); + break; + case 'Settings': + break; + } +} + @override Widget build(BuildContext context) { // This method is rerun every time setState is called, for instance @@ -240,6 +207,23 @@ class _MyHomePageState extends State { // fast, so that you can just rebuild anything that needs updating rather // than having to individually change instances of widgets. return Scaffold( + appBar: AppBar( + // title: Text('Homepage'), + backgroundColor: const Color(0xFF9ab4a4), + actions: [ + PopupMenuButton( + onSelected: handleClick, + itemBuilder: (BuildContext context) { + return {'Library'}.map((String choice) { + return PopupMenuItem( + value: choice, + child: Text(choice), + ); + }).toList(); + }, + ), + ], + ), body: Center( // Center is a layout widget. It takes a single child and positions it // in the middle of the parent. @@ -262,9 +246,9 @@ class _MyHomePageState extends State { children: [ Text( myPath, - style: Theme.of(context).textTheme.headlineMedium, + style: Theme.of(context).textTheme.bodySmall, ), - Image(image: AssetImage(currentImage)), + img, ], ), ), @@ -272,49 +256,31 @@ class _MyHomePageState extends State { color: const Color(0xFF9ab4a4), child: Row( children: [ - IconButton( + /* IconButton( tooltip: 'Select library directory', icon: const Icon( Icons.folder, size: 40, ), onPressed: () async { - String? selectedDirectory = - await FilePicker.platform.getDirectoryPath(); + // FilePickerResult? result = await FilePicker.platform.pickFiles(); + String ?path = await FilePicker.platform.getDirectoryPath(); - if (selectedDirectory == null) { - // User canceled the picker - } else { - bool? isGranted = true; - logger.d(selectedDirectory); + if (path != null) { + logger.d("Selected directory: $path"); - if (Platform.isAndroid) { - Saf saf = Saf(selectedDirectory); - - isGranted = - await saf.getDirectoryPermission(isDynamic: false); - - if (isGranted != null && isGranted) { - // Perform some file operations - logger.d('Granted!'); - } else { - // failed to get the permission - } - } - - if (isGranted == true) { - setState(() { - myPath = selectedDirectory; - }); - bool success = await indexFile.loadIndexFile(selectedDirectory); - if (success) { - showCurrentStory(); - } + setState(() { + myPath = path; + }); + bool success = await indexFile.loadIndexFile(path); + if (success) { + showCurrentStoryIndex(); + state = PlayerState.indexFile; } } }, color: const Color(0xFFb05728), - ), + ),*/ IconButton( tooltip: 'Previous', icon: const Icon(Icons.arrow_circle_left, size: 40), @@ -323,7 +289,7 @@ class _MyHomePageState extends State { StoryVm.previousButton(); } else if (state == PlayerState.indexFile) { indexFile.previous(); - showCurrentStory(); + showCurrentStoryIndex(); } }, color: const Color(0xFFb05728), @@ -337,7 +303,22 @@ class _MyHomePageState extends State { StoryVm.nextButton(); } else if (state == PlayerState.indexFile) { indexFile.next(); - showCurrentStory(); + showCurrentStoryIndex(); + } + }, + color: const Color(0xFFb05728), + ), + IconButton( + tooltip: 'Home', + icon: const Icon(Icons.home_filled, size: 40), + onPressed: () { + + if (state == PlayerState.inStory) { + player.stop(); + showCurrentStoryIndex(); + state = PlayerState.indexFile; + } else if (state == PlayerState.indexFile) { + player.stop(); } }, color: const Color(0xFFb05728), diff --git a/story-player/linux/CMakeLists.txt b/story-player/linux/CMakeLists.txt index 0b792dc..b444059 100644 --- a/story-player/linux/CMakeLists.txt +++ b/story-player/linux/CMakeLists.txt @@ -4,10 +4,10 @@ project(runner LANGUAGES CXX) # The name of the executable created for the application. Change this to change # the on-disk name of your application. -set(BINARY_NAME "flutter_launcher") +set(BINARY_NAME "story_player") # The unique GTK application identifier for this application. See: # https://wiki.gnome.org/HowDoI/ChooseApplicationID -set(APPLICATION_ID "com.example.flutter_launcher") +set(APPLICATION_ID "org.openstoryteller.story_player") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. diff --git a/story-player/pubspec.yaml b/story-player/pubspec.yaml index 3a67741..3f67bca 100644 --- a/story-player/pubspec.yaml +++ b/story-player/pubspec.yaml @@ -1,5 +1,5 @@ -name: flutter_launcher -description: A new Flutter project. +name: story_player +description: OpenStoryTeller universal player. # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev @@ -43,10 +43,12 @@ dependencies: saf: ^1.0.3+4 ffi: ^2.1.0 logger: ^2.2.0 - file_picker: ^7.0.2 + file_picker: ^8.0.3 dqoi: ^1.3.0 audioplayers: ^5.2.1 event_bus: ^2.0.0 + external_path: ^1.0.3 + permission_handler: ^10.4.5 dev_dependencies: flutter_test: diff --git a/story-player/test/widget_test.dart b/story-player/test/widget_test.dart index bf51d11..ea5ad27 100644 --- a/story-player/test/widget_test.dart +++ b/story-player/test/widget_test.dart @@ -8,7 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_launcher/main.dart'; +import 'package:story_player/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async {