Files for version 1.0

This commit is contained in:
anthony@rabine.fr 2024-05-17 21:12:39 +02:00
parent c954563472
commit d0988a7e6b
31 changed files with 266 additions and 168 deletions

View file

@ -5,18 +5,18 @@
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "flutter_launcher", "name": "story_player",
"request": "launch", "request": "launch",
"type": "dart" "type": "dart"
}, },
{ {
"name": "flutter_launcher (profile mode)", "name": "story_player (profile mode)",
"request": "launch", "request": "launch",
"type": "dart", "type": "dart",
"flutterMode": "profile" "flutterMode": "profile"
}, },
{ {
"name": "flutter_launcher (release mode)", "name": "story_player (release mode)",
"request": "launch", "request": "launch",
"type": "dart", "type": "dart",
"flutterMode": "release" "flutterMode": "release"

View file

@ -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.

View file

@ -26,7 +26,7 @@ apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android { android {
namespace "com.example.flutter_launcher" namespace "org.openstoryteller.story_player"
compileSdkVersion flutter.compileSdkVersion compileSdkVersion flutter.compileSdkVersion
ndkVersion flutter.ndkVersion ndkVersion flutter.ndkVersion
@ -45,7 +45,7 @@ android {
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // 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. // 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. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion flutter.minSdkVersion minSdkVersion flutter.minSdkVersion

View file

@ -1,12 +1,17 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="ANDROID.PERMISSION.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
<!-- allows startup after boot --> <!-- allows startup after boot -->
<application <application
android:label="flutter_launcher" android:label="Story Player"
android:name="${applicationName}" android:name="${applicationName}"
android:requestLegacyExternalStorage="true"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"

View file

@ -1,4 +1,4 @@
package com.example.flutter_launcher package org.openstoryteller.story_player
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -0,0 +1,102 @@
import 'dart:io';
import 'dart:convert';
import 'dart:async';
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: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<void> _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<int> 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}');
}
}
}

View file

@ -4,6 +4,7 @@ import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
import 'package:permission_handler/permission_handler.dart';
var logger = Logger(printer: PrettyPrinter(methodCount: 0)); var logger = Logger(printer: PrettyPrinter(methodCount: 0));
@ -117,14 +118,33 @@ class IndexFile {
Future<bool> loadIndexFile(String libraryRoot) async { Future<bool> loadIndexFile(String libraryRoot) async {
libraryPath = libraryRoot; libraryPath = libraryRoot;
indexFileIsValid = false; 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 // Ouvrir le fichier en mode lecture binaire
indexFileBuffer = file.readAsBytesSync();
readPtr = 0; readPtr = 0;
indexFileStream = indexFileStream =
ByteData.sublistView(indexFileBuffer, readPtr); // start at zero ByteData.sublistView(indexFileBuffer, readPtr); // start at zero

View file

@ -3,113 +3,35 @@ import 'package:flutter/material.dart' hide Router;
import 'dart:io'; import 'dart:io';
import 'dart:convert'; import 'dart:convert';
import 'dart:async'; 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:saf/saf.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:audioplayers/audioplayers.dart'; import 'package:audioplayers/audioplayers.dart';
import 'package:file_picker/file_picker.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/storyvm.dart';
import 'libstory/indexfile.dart'; import 'libstory/indexfile.dart';
import 'httpserver.dart';
import 'package:logger/logger.dart'; class ProductionFilter extends LogFilter {
@override
var logger = Logger(printer: PrettyPrinter(methodCount: 0)); bool shouldLog(LogEvent event) {
return true;
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<void> _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<int> 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}');
}
} }
} }
var logger = Logger(
printer: PrettyPrinter(methodCount: 0),
filter: ProductionFilter(), // Use the ProductionFilter to enable logging in release mode
);
void main() { void main() {
StoryVm.loadLibrary(); StoryVm.loadLibrary();
StoryVm.initialize(); StoryVm.initialize();
@ -182,6 +104,13 @@ class _MyHomePageState extends State<MyHomePage> {
PlayerState state = PlayerState.disabled; PlayerState state = PlayerState.disabled;
StreamSubscription? audioPlayerSub; 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 { void initPaths() async {
Directory? dir; Directory? dir;
@ -206,6 +135,8 @@ class _MyHomePageState extends State<MyHomePage> {
} }
}); });
img = Image.file(File(currentImage));
if (event.sound.isNotEmpty) { if (event.sound.isNotEmpty) {
player.play(DeviceFileSource('${indexFile.getCurrentStoryPath()}/assets/${event.sound}')); player.play(DeviceFileSource('${indexFile.getCurrentStoryPath()}/assets/${event.sound}'));
} }
@ -221,17 +152,53 @@ class _MyHomePageState extends State<MyHomePage> {
state = PlayerState.indexFile; state = PlayerState.indexFile;
} }
void showCurrentStory() async { Uint8List soundBuffer = Uint8List(0);
void showCurrentStoryIndex() async {
setState(() { setState(() {
currentImage = indexFile.getCurrentTitleImage(); currentImage = indexFile.getCurrentTitleImage();
logger.d('Current image: $currentImage'); logger.d('Current image: $currentImage');
}); });
var asset = DeviceFileSource(indexFile.getCurrentSoundImage()); img = Image.file(File(currentImage));
logger.d('Asset: ${asset.toString()}');
await player.play(asset); // 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance // This method is rerun every time setState is called, for instance
@ -240,6 +207,23 @@ class _MyHomePageState extends State<MyHomePage> {
// fast, so that you can just rebuild anything that needs updating rather // fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets. // than having to individually change instances of widgets.
return Scaffold( return Scaffold(
appBar: AppBar(
// title: Text('Homepage'),
backgroundColor: const Color(0xFF9ab4a4),
actions: <Widget>[
PopupMenuButton<String>(
onSelected: handleClick,
itemBuilder: (BuildContext context) {
return {'Library'}.map((String choice) {
return PopupMenuItem<String>(
value: choice,
child: Text(choice),
);
}).toList();
},
),
],
),
body: Center( body: Center(
// Center is a layout widget. It takes a single child and positions it // Center is a layout widget. It takes a single child and positions it
// in the middle of the parent. // in the middle of the parent.
@ -262,9 +246,9 @@ class _MyHomePageState extends State<MyHomePage> {
children: <Widget>[ children: <Widget>[
Text( Text(
myPath, 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<MyHomePage> {
color: const Color(0xFF9ab4a4), color: const Color(0xFF9ab4a4),
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
IconButton( /* IconButton(
tooltip: 'Select library directory', tooltip: 'Select library directory',
icon: const Icon( icon: const Icon(
Icons.folder, Icons.folder,
size: 40, size: 40,
), ),
onPressed: () async { onPressed: () async {
String? selectedDirectory = // FilePickerResult? result = await FilePicker.platform.pickFiles();
await FilePicker.platform.getDirectoryPath(); String ?path = await FilePicker.platform.getDirectoryPath();
if (selectedDirectory == null) { if (path != null) {
// User canceled the picker logger.d("Selected directory: $path");
} else {
bool? isGranted = true;
logger.d(selectedDirectory);
if (Platform.isAndroid) { setState(() {
Saf saf = Saf(selectedDirectory); myPath = path;
});
isGranted = bool success = await indexFile.loadIndexFile(path);
await saf.getDirectoryPermission(isDynamic: false); if (success) {
showCurrentStoryIndex();
if (isGranted != null && isGranted) { state = PlayerState.indexFile;
// 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();
}
} }
} }
}, },
color: const Color(0xFFb05728), color: const Color(0xFFb05728),
), ),*/
IconButton( IconButton(
tooltip: 'Previous', tooltip: 'Previous',
icon: const Icon(Icons.arrow_circle_left, size: 40), icon: const Icon(Icons.arrow_circle_left, size: 40),
@ -323,7 +289,7 @@ class _MyHomePageState extends State<MyHomePage> {
StoryVm.previousButton(); StoryVm.previousButton();
} else if (state == PlayerState.indexFile) { } else if (state == PlayerState.indexFile) {
indexFile.previous(); indexFile.previous();
showCurrentStory(); showCurrentStoryIndex();
} }
}, },
color: const Color(0xFFb05728), color: const Color(0xFFb05728),
@ -337,7 +303,22 @@ class _MyHomePageState extends State<MyHomePage> {
StoryVm.nextButton(); StoryVm.nextButton();
} else if (state == PlayerState.indexFile) { } else if (state == PlayerState.indexFile) {
indexFile.next(); 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), color: const Color(0xFFb05728),

View file

@ -4,10 +4,10 @@ project(runner LANGUAGES CXX)
# The name of the executable created for the application. Change this to change # The name of the executable created for the application. Change this to change
# the on-disk name of your application. # 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: # The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID # 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 # Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake. # versions of CMake.

View file

@ -1,5 +1,5 @@
name: flutter_launcher name: story_player
description: A new Flutter project. description: OpenStoryTeller universal player.
# The following line prevents the package from being accidentally published to # The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages. # 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 publish_to: 'none' # Remove this line if you wish to publish to pub.dev
@ -43,10 +43,12 @@ dependencies:
saf: ^1.0.3+4 saf: ^1.0.3+4
ffi: ^2.1.0 ffi: ^2.1.0
logger: ^2.2.0 logger: ^2.2.0
file_picker: ^7.0.2 file_picker: ^8.0.3
dqoi: ^1.3.0 dqoi: ^1.3.0
audioplayers: ^5.2.1 audioplayers: ^5.2.1
event_bus: ^2.0.0 event_bus: ^2.0.0
external_path: ^1.0.3
permission_handler: ^10.4.5
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View file

@ -8,7 +8,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_launcher/main.dart'; import 'package:story_player/main.dart';
void main() { void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async { testWidgets('Counter increments smoke test', (WidgetTester tester) async {