mirror of
https://github.com/arabine/open-story-teller.git
synced 2025-12-06 17:09:06 +01:00
first working app
This commit is contained in:
parent
bb0c18e433
commit
c954563472
10 changed files with 509 additions and 264 deletions
|
|
@ -20,7 +20,7 @@ You'll need:
|
||||||
Here is a list of packages for Ubuntu-like systems:
|
Here is a list of packages for Ubuntu-like systems:
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo apt install cmake mesa-utils mesa-common-dev ninja-build libxext-dev
|
sudo apt install cmake mesa-utils mesa-common-dev ninja-build libxext-dev libpipewire-0.3-dev libasound2-dev libpulse-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## How to build
|
## How to build
|
||||||
|
|
|
||||||
21
docs/player-dev.md
Normal file
21
docs/player-dev.md
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
# Story player project
|
||||||
|
|
||||||
|
The Story Player is a Flutter application.
|
||||||
|
|
||||||
|
# Packages
|
||||||
|
|
||||||
|
sudo apt-get install clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Build for Linux
|
||||||
|
|
||||||
|
flutter build linux
|
||||||
|
|
||||||
|
# Build for Android
|
||||||
|
|
||||||
|
|
||||||
|
flutter build apk
|
||||||
|
|
||||||
1
story-player/.gitignore
vendored
1
story-player/.gitignore
vendored
|
|
@ -31,6 +31,7 @@ migrate_working_dir/
|
||||||
.pub-cache/
|
.pub-cache/
|
||||||
.pub/
|
.pub/
|
||||||
/build/
|
/build/
|
||||||
|
/linux/flutter/generated_plugin*
|
||||||
|
|
||||||
# Symbolication related
|
# Symbolication related
|
||||||
app.*.symbols
|
app.*.symbols
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,12 @@ android {
|
||||||
signingConfig signingConfigs.debug
|
signingConfig signingConfigs.debug
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
externalNativeBuild {
|
||||||
|
cmake {
|
||||||
|
path "../../storyvm/CMakeLists.txt"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flutter {
|
flutter {
|
||||||
|
|
|
||||||
175
story-player/lib/libstory/indexfile.dart
Normal file
175
story-player/lib/libstory/indexfile.dart
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
library libstory;
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:logger/logger.dart';
|
||||||
|
|
||||||
|
var logger = Logger(printer: PrettyPrinter(methodCount: 0));
|
||||||
|
|
||||||
|
class IndexFile {
|
||||||
|
static const int tlvArrayType = 0xAB;
|
||||||
|
static const int tlvObjectType = 0xE7;
|
||||||
|
static const int tlvIntegerType = 0x77;
|
||||||
|
static const int tlvStringType = 0x3D;
|
||||||
|
static const int tlvRealType = 0xB8;
|
||||||
|
|
||||||
|
// Index File stuff
|
||||||
|
Uint8List indexFileBuffer = Uint8List(0);
|
||||||
|
ByteData indexFileStream = ByteData(0);
|
||||||
|
int readPtr = 0;
|
||||||
|
int indexFileVersion = 0;
|
||||||
|
bool indexFileIsValid = false;
|
||||||
|
|
||||||
|
// Variables de gestion de l'index des histoires
|
||||||
|
int storiesCount = 0;
|
||||||
|
String libraryPath = '';
|
||||||
|
int currentStoryIndex = 0;
|
||||||
|
|
||||||
|
List<
|
||||||
|
({
|
||||||
|
String uuid,
|
||||||
|
String titleImage,
|
||||||
|
String titleSound,
|
||||||
|
String title,
|
||||||
|
String description,
|
||||||
|
int version
|
||||||
|
})> stories = [];
|
||||||
|
|
||||||
|
String getCurrentTitleImage() {
|
||||||
|
String fileName = '';
|
||||||
|
if (currentStoryIndex < storiesCount) {
|
||||||
|
fileName =
|
||||||
|
'$libraryPath/${stories[currentStoryIndex].uuid}/assets/${stories[currentStoryIndex].titleImage}';
|
||||||
|
}
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getCurrentSoundImage() {
|
||||||
|
String fileName = '';
|
||||||
|
if (currentStoryIndex < storiesCount) {
|
||||||
|
fileName =
|
||||||
|
'$libraryPath/${stories[currentStoryIndex].uuid}/assets/${stories[currentStoryIndex].titleSound}';
|
||||||
|
}
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getCurrentStoryPath() {
|
||||||
|
String path = '';
|
||||||
|
if (currentStoryIndex < storiesCount) {
|
||||||
|
path =
|
||||||
|
'$libraryPath/${stories[currentStoryIndex].uuid}';
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
void next() {
|
||||||
|
currentStoryIndex++;
|
||||||
|
if (currentStoryIndex >= storiesCount) {
|
||||||
|
currentStoryIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void previous() {
|
||||||
|
currentStoryIndex--;
|
||||||
|
if (currentStoryIndex < 0) {
|
||||||
|
currentStoryIndex = storiesCount - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the size, 0 if error
|
||||||
|
int getTl(int expectedType) {
|
||||||
|
int size = 0;
|
||||||
|
if (indexFileStream.getUint8(readPtr) == expectedType) {
|
||||||
|
readPtr++;
|
||||||
|
size = indexFileStream.getUint16(readPtr, Endian.little);
|
||||||
|
readPtr += 2;
|
||||||
|
} else {
|
||||||
|
throw Exception("Expected type: $expectedType");
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
int getIntegerValue() {
|
||||||
|
int size = getTl(tlvIntegerType);
|
||||||
|
|
||||||
|
if (size == 4) {
|
||||||
|
int value = indexFileStream.getUint32(readPtr, Endian.little);
|
||||||
|
readPtr += 4;
|
||||||
|
return value;
|
||||||
|
} else {
|
||||||
|
throw Exception("Expected an integer of size 4 bytes");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String getStringValue() {
|
||||||
|
int size = getTl(tlvStringType);
|
||||||
|
if (size > 0) {
|
||||||
|
String value = String.fromCharCodes(
|
||||||
|
indexFileStream.buffer.asUint8List(), readPtr, readPtr + size);
|
||||||
|
readPtr += size;
|
||||||
|
return value;
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ouvrir le fichier en mode lecture binaire
|
||||||
|
indexFileBuffer = file.readAsBytesSync();
|
||||||
|
readPtr = 0;
|
||||||
|
indexFileStream =
|
||||||
|
ByteData.sublistView(indexFileBuffer, readPtr); // start at zero
|
||||||
|
stories.clear();
|
||||||
|
|
||||||
|
if (indexFileBuffer.lengthInBytes > 3) {
|
||||||
|
// Root must be an object containing 2 elements
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (getTl(tlvObjectType) == 2) {
|
||||||
|
indexFileVersion = getIntegerValue();
|
||||||
|
storiesCount = getTl(tlvArrayType);
|
||||||
|
|
||||||
|
for (int i = 0; i < storiesCount; i++) {
|
||||||
|
if (getTl(tlvObjectType) == 6) {
|
||||||
|
var record = (
|
||||||
|
uuid: getStringValue(),
|
||||||
|
titleImage: getStringValue(),
|
||||||
|
titleSound: getStringValue(),
|
||||||
|
title: getStringValue(),
|
||||||
|
description: getStringValue(),
|
||||||
|
version: getIntegerValue()
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.d('Found story: ${record.title.toString()}');
|
||||||
|
|
||||||
|
stories.add(record);
|
||||||
|
} else {
|
||||||
|
throw Exception("Expected object of 6 elements at root");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If get through here, no exception raised so the file shoould be ok
|
||||||
|
indexFileIsValid = true;
|
||||||
|
currentStoryIndex = 0;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw Exception("Expected object of 2 elements at root");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.e('Exception reading index file: $e');
|
||||||
|
} finally {}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
164
story-player/lib/libstory/storyvm.dart
Normal file
164
story-player/lib/libstory/storyvm.dart
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
library libstory;
|
||||||
|
|
||||||
|
import 'dart:ffi';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:ffi/ffi.dart';
|
||||||
|
import 'package:logger/logger.dart';
|
||||||
|
import 'package:event_bus/event_bus.dart';
|
||||||
|
|
||||||
|
var logger = Logger(printer: PrettyPrinter(methodCount: 0));
|
||||||
|
|
||||||
|
/// The global [EventBus] object.
|
||||||
|
EventBus eventBus = EventBus();
|
||||||
|
|
||||||
|
class MediaEvent {
|
||||||
|
String image;
|
||||||
|
String sound;
|
||||||
|
|
||||||
|
MediaEvent(this.image, this.sound);
|
||||||
|
}
|
||||||
|
|
||||||
|
Completer<bool> periodic(Duration interval, Function(int cycle) callback) {
|
||||||
|
final done = Completer<bool>();
|
||||||
|
() async {
|
||||||
|
var cycle = 0;
|
||||||
|
while (!done.isCompleted) {
|
||||||
|
try {
|
||||||
|
await callback(cycle);
|
||||||
|
} catch (e, s) {
|
||||||
|
logger.e("$e", stackTrace: s);
|
||||||
|
}
|
||||||
|
cycle++;
|
||||||
|
await done.future.timeout(interval).onError((error, stackTrace) => false);
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
return done;
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef MediaCallbackType = Void Function(Int32 i, Pointer<Utf8> str);
|
||||||
|
typedef MediaCallback = void Function(Int32 i, Pointer<Utf8> str);
|
||||||
|
|
||||||
|
typedef VmInitializeType = Void Function(
|
||||||
|
Pointer<NativeFunction<MediaCallbackType>>);
|
||||||
|
typedef VmInitialize = void Function(
|
||||||
|
Pointer<NativeFunction<MediaCallbackType>>);
|
||||||
|
|
||||||
|
typedef VmStartType = Void Function(Pointer<Uint8> data, Uint32 size);
|
||||||
|
typedef VmStart = void Function(Pointer<Uint8> data, int size);
|
||||||
|
|
||||||
|
typedef VmRunType = Void Function();
|
||||||
|
typedef VmRun = void Function();
|
||||||
|
|
||||||
|
typedef VmSendEventType = Void Function(Int);
|
||||||
|
typedef VmSendEvent = void Function(int event);
|
||||||
|
|
||||||
|
enum VmEvent { evNoEvent, evStep, evOkButton, evPreviousButton, evNextButton, evAudioFinished, evStop }
|
||||||
|
|
||||||
|
class StoryVm {
|
||||||
|
static late DynamicLibrary nativeApiLib;
|
||||||
|
static late VmInitialize vmInitialize;
|
||||||
|
static late VmStart vmStart;
|
||||||
|
static late VmRun vmRun;
|
||||||
|
static late VmSendEvent vmSendEvent;
|
||||||
|
static String currentStoryPath = '';
|
||||||
|
|
||||||
|
static bool running = false;
|
||||||
|
|
||||||
|
static bool loadLibrary() {
|
||||||
|
String dllName = 'libstoryvm.so';
|
||||||
|
|
||||||
|
if (Platform.isMacOS) {
|
||||||
|
dllName = 'libstoryvm.dylib';
|
||||||
|
} else if (Platform.isWindows) {
|
||||||
|
dllName = 'storyvm.dll';
|
||||||
|
}
|
||||||
|
|
||||||
|
final dylib = DynamicLibrary.open(dllName);
|
||||||
|
|
||||||
|
vmInitialize = dylib
|
||||||
|
.lookup<NativeFunction<VmInitializeType>>('storyvm_initialize')
|
||||||
|
.asFunction();
|
||||||
|
|
||||||
|
vmStart = dylib.lookup<NativeFunction<VmStartType>>('storyvm_start').asFunction();
|
||||||
|
|
||||||
|
vmRun = dylib.lookup<NativeFunction<VmRunType>>('storyvm_run').asFunction();
|
||||||
|
|
||||||
|
vmSendEvent = dylib.lookup<NativeFunction<VmSendEventType>>('storyvm_send_event').asFunction();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void mediaCallback(int i, Pointer<Utf8> str) {
|
||||||
|
String file = str.toDartString();
|
||||||
|
logger.d('Mediatype: $i, Media file: $file');
|
||||||
|
|
||||||
|
if (i == 0) {
|
||||||
|
eventBus.fire(MediaEvent(file, ""));
|
||||||
|
} else {
|
||||||
|
eventBus.fire(MediaEvent("", file));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static void initialize() {
|
||||||
|
vmInitialize(Pointer.fromFunction<MediaCallbackType>(mediaCallback));
|
||||||
|
}
|
||||||
|
|
||||||
|
static Completer<bool> task = Completer<bool>();
|
||||||
|
|
||||||
|
static Future<bool> start(String storyBasePath) async{
|
||||||
|
currentStoryPath = storyBasePath;
|
||||||
|
final file = File('$storyBasePath/story.c32');
|
||||||
|
if (!file.existsSync()) {
|
||||||
|
logger.d('Le fichier n\'existe pas.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ouvrir le fichier en mode lecture binaire
|
||||||
|
Uint8List fileBuffer = file.readAsBytesSync();
|
||||||
|
|
||||||
|
Pointer<Uint8> dataPointer = malloc.allocate<Uint8>(fileBuffer.length);
|
||||||
|
for (int i = 0; i < fileBuffer.length; i++) {
|
||||||
|
dataPointer[i] = fileBuffer[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
vmStart(dataPointer, fileBuffer.length);
|
||||||
|
running = true;
|
||||||
|
task = periodic(const Duration(milliseconds: 10), (cycle) async {
|
||||||
|
if (running) {
|
||||||
|
vmRun();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void endOfSound() {
|
||||||
|
|
||||||
|
vmSendEvent(VmEvent.evAudioFinished.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void okButton() {
|
||||||
|
|
||||||
|
vmSendEvent(VmEvent.evOkButton.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void previousButton() {
|
||||||
|
|
||||||
|
vmSendEvent(VmEvent.evPreviousButton.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void nextButton() {
|
||||||
|
|
||||||
|
vmSendEvent(VmEvent.evNextButton.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void stop() {
|
||||||
|
logger.d('VM stop');
|
||||||
|
running = false;
|
||||||
|
task.complete(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,13 +9,14 @@ import 'package:shelf/shelf_io.dart' as io;
|
||||||
import 'package:shelf_router/shelf_router.dart';
|
import 'package:shelf_router/shelf_router.dart';
|
||||||
import 'package:mime/mime.dart';
|
import 'package:mime/mime.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:http_parser/http_parser.dart';
|
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:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
|
||||||
import 'package:flutter_launcher/storyvm.dart';
|
import 'libstory/storyvm.dart';
|
||||||
|
import 'libstory/indexfile.dart';
|
||||||
|
|
||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.dart';
|
||||||
|
|
||||||
|
|
@ -112,7 +113,7 @@ Future printIps() async {
|
||||||
void main() {
|
void main() {
|
||||||
StoryVm.loadLibrary();
|
StoryVm.loadLibrary();
|
||||||
StoryVm.initialize();
|
StoryVm.initialize();
|
||||||
StoryVm.start();
|
|
||||||
udpServer();
|
udpServer();
|
||||||
httpServer();
|
httpServer();
|
||||||
printIps();
|
printIps();
|
||||||
|
|
@ -170,9 +171,16 @@ class MyHomePage extends StatefulWidget {
|
||||||
State<MyHomePage> createState() => _MyHomePageState();
|
State<MyHomePage> createState() => _MyHomePageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum PlayerState { disabled, indexFile, inStory }
|
||||||
|
|
||||||
class _MyHomePageState extends State<MyHomePage> {
|
class _MyHomePageState extends State<MyHomePage> {
|
||||||
int _counter = 0;
|
|
||||||
String myPath = 'fffff';
|
String myPath = 'fffff';
|
||||||
|
IndexFile indexFile = IndexFile();
|
||||||
|
String currentImage = 'assets/320x240.png';
|
||||||
|
final player = AudioPlayer();
|
||||||
|
StreamSubscription? mediaPub;
|
||||||
|
PlayerState state = PlayerState.disabled;
|
||||||
|
StreamSubscription? audioPlayerSub;
|
||||||
|
|
||||||
void initPaths() async {
|
void initPaths() async {
|
||||||
Directory? dir;
|
Directory? dir;
|
||||||
|
|
@ -191,23 +199,42 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||||
|
|
||||||
_MyHomePageState() {
|
_MyHomePageState() {
|
||||||
initPaths();
|
initPaths();
|
||||||
|
mediaPub = eventBus.on<MediaEvent>().listen((event) {
|
||||||
|
setState(() {
|
||||||
|
if (event.image.isNotEmpty) {
|
||||||
|
currentImage = '${indexFile.getCurrentStoryPath()}/assets/${event.image}';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (event.sound.isNotEmpty) {
|
||||||
|
player.play(DeviceFileSource('${indexFile.getCurrentStoryPath()}/assets/${event.sound}'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
audioPlayerSub = player.onPlayerComplete.listen((event) {
|
||||||
|
if (state == PlayerState.inStory) {
|
||||||
|
// Send end of music event
|
||||||
|
StoryVm.endOfSound();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
state = PlayerState.indexFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _incrementCounter() {
|
void showCurrentStory() async {
|
||||||
setState(() {
|
setState(() {
|
||||||
// This call to setState tells the Flutter framework that something has
|
currentImage = indexFile.getCurrentTitleImage();
|
||||||
// changed in this State, which causes it to rerun the build method below
|
logger.d('Current image: $currentImage');
|
||||||
// so that the display can reflect the updated values. If we changed
|
|
||||||
// _counter without calling setState(), then the build method would not be
|
|
||||||
// called again, and so nothing would appear to happen.
|
|
||||||
_counter++;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var asset = DeviceFileSource(indexFile.getCurrentSoundImage());
|
||||||
|
logger.d('Asset: ${asset.toString()}');
|
||||||
|
await player.play(asset);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// This method is rerun every time setState is called, for instance as done
|
// This method is rerun every time setState is called, for instance
|
||||||
// by the _incrementCounter method above.
|
|
||||||
//
|
//
|
||||||
// The Flutter framework has been optimized to make rerunning build methods
|
// The Flutter framework has been optimized to make rerunning build methods
|
||||||
// fast, so that you can just rebuild anything that needs updating rather
|
// fast, so that you can just rebuild anything that needs updating rather
|
||||||
|
|
@ -233,18 +260,11 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||||
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
const Text(
|
|
||||||
'You have pushed the button this many times:',
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'$_counter',
|
|
||||||
style: Theme.of(context).textTheme.headlineMedium,
|
|
||||||
),
|
|
||||||
Text(
|
Text(
|
||||||
myPath,
|
myPath,
|
||||||
style: Theme.of(context).textTheme.headlineMedium,
|
style: Theme.of(context).textTheme.headlineMedium,
|
||||||
),
|
),
|
||||||
const Image(image: AssetImage('assets/320x240.png')),
|
Image(image: AssetImage(currentImage)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -253,7 +273,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||||
child: Row(
|
child: Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: 'Open navigation menu',
|
tooltip: 'Select library directory',
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.folder,
|
Icons.folder,
|
||||||
size: 40,
|
size: 40,
|
||||||
|
|
@ -283,29 +303,63 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isGranted == true) {
|
if (isGranted == true) {
|
||||||
StoryVm.loadIndexFile('$selectedDirectory/index.ost');
|
setState(() {
|
||||||
|
myPath = selectedDirectory;
|
||||||
|
});
|
||||||
|
bool success = await indexFile.loadIndexFile(selectedDirectory);
|
||||||
|
if (success) {
|
||||||
|
showCurrentStory();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
color: const Color(0xFFb05728),
|
color: const Color(0xFFb05728),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: 'Search',
|
tooltip: 'Previous',
|
||||||
icon: const Icon(Icons.arrow_circle_left, size: 40),
|
icon: const Icon(Icons.arrow_circle_left, size: 40),
|
||||||
onPressed: () {},
|
onPressed: () {
|
||||||
|
if (state == PlayerState.inStory) {
|
||||||
|
StoryVm.previousButton();
|
||||||
|
} else if (state == PlayerState.indexFile) {
|
||||||
|
indexFile.previous();
|
||||||
|
showCurrentStory();
|
||||||
|
}
|
||||||
|
},
|
||||||
color: const Color(0xFFb05728),
|
color: const Color(0xFFb05728),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: 'Favorite',
|
tooltip: 'Next',
|
||||||
icon: const Icon(Icons.arrow_circle_right, size: 40),
|
icon: const Icon(Icons.arrow_circle_right, size: 40),
|
||||||
onPressed: () {},
|
onPressed: () {
|
||||||
|
|
||||||
|
if (state == PlayerState.inStory) {
|
||||||
|
StoryVm.nextButton();
|
||||||
|
} else if (state == PlayerState.indexFile) {
|
||||||
|
indexFile.next();
|
||||||
|
showCurrentStory();
|
||||||
|
}
|
||||||
|
},
|
||||||
color: const Color(0xFFb05728),
|
color: const Color(0xFFb05728),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
onPressed: _incrementCounter,
|
onPressed: () {
|
||||||
|
|
||||||
|
if (state == PlayerState.inStory) {
|
||||||
|
StoryVm.okButton();
|
||||||
|
} else if (state == PlayerState.indexFile) {
|
||||||
|
String path = indexFile.getCurrentStoryPath();
|
||||||
|
|
||||||
|
if (path.isNotEmpty) {
|
||||||
|
StoryVm.start(path);
|
||||||
|
state = PlayerState.inStory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
tooltip: 'Ok',
|
tooltip: 'Ok',
|
||||||
backgroundColor: const Color(0xFF0092c8),
|
backgroundColor: const Color(0xFF0092c8),
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
|
|
|
||||||
|
|
@ -1,204 +0,0 @@
|
||||||
import 'dart:ffi';
|
|
||||||
import 'dart:io';
|
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:ffi/ffi.dart';
|
|
||||||
import 'package:logger/logger.dart';
|
|
||||||
|
|
||||||
var logger = Logger(printer: PrettyPrinter(methodCount: 0));
|
|
||||||
|
|
||||||
Completer<bool> periodic(Duration interval, Function(int cycle) callback) {
|
|
||||||
final done = Completer<bool>();
|
|
||||||
() async {
|
|
||||||
var cycle = 0;
|
|
||||||
while (!done.isCompleted) {
|
|
||||||
try {
|
|
||||||
await callback(cycle);
|
|
||||||
} catch (e, s) {
|
|
||||||
logger.e("$e", stackTrace: s);
|
|
||||||
}
|
|
||||||
cycle++;
|
|
||||||
await done.future.timeout(interval).onError((error, stackTrace) => false);
|
|
||||||
}
|
|
||||||
}();
|
|
||||||
return done;
|
|
||||||
}
|
|
||||||
|
|
||||||
typedef MediaCallbackType = Void Function(Int32 i, Pointer<Utf8> str);
|
|
||||||
typedef MediaCallback = void Function(Int32 i, Pointer<Utf8> str);
|
|
||||||
|
|
||||||
typedef VmInitializeType = Void Function(
|
|
||||||
Pointer<NativeFunction<MediaCallbackType>>);
|
|
||||||
typedef VmInitialize = void Function(
|
|
||||||
Pointer<NativeFunction<MediaCallbackType>>);
|
|
||||||
|
|
||||||
typedef VmStartType = Void Function(Pointer<Uint8> data, Uint32 size);
|
|
||||||
typedef VmStart = void Function(Pointer<Uint8> data, int size);
|
|
||||||
|
|
||||||
typedef VmRunType = Void Function();
|
|
||||||
typedef VmRun = void Function();
|
|
||||||
|
|
||||||
class StoryVm {
|
|
||||||
static late DynamicLibrary nativeApiLib;
|
|
||||||
static late VmInitialize vmInitialize;
|
|
||||||
static late VmStart vmStart;
|
|
||||||
static late VmRun vmRun;
|
|
||||||
|
|
||||||
static bool running = false;
|
|
||||||
|
|
||||||
static bool loadLibrary() {
|
|
||||||
String dllName = 'libstoryvm.so';
|
|
||||||
|
|
||||||
if (Platform.isMacOS) {
|
|
||||||
dllName = 'libstoryvm.dylib';
|
|
||||||
} else if (Platform.isWindows) {
|
|
||||||
dllName = 'storyvm.dll';
|
|
||||||
}
|
|
||||||
|
|
||||||
final dylib = DynamicLibrary.open(dllName);
|
|
||||||
|
|
||||||
vmInitialize = dylib
|
|
||||||
.lookup<NativeFunction<VmInitializeType>>('storyvm_initialize')
|
|
||||||
.asFunction();
|
|
||||||
|
|
||||||
vmStart =
|
|
||||||
dylib.lookup<NativeFunction<VmStartType>>('storyvm_start').asFunction();
|
|
||||||
|
|
||||||
vmRun =
|
|
||||||
dylib.lookup<NativeFunction<VmRunType>>('storyvm_start').asFunction();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void mediaCallback(int i, Pointer<Utf8> str) {
|
|
||||||
logger.d('Mediatype: $i, Media file: $str');
|
|
||||||
}
|
|
||||||
|
|
||||||
static void initialize() {
|
|
||||||
vmInitialize(Pointer.fromFunction<MediaCallbackType>(mediaCallback));
|
|
||||||
}
|
|
||||||
|
|
||||||
static Completer<bool> task = Completer<bool>();
|
|
||||||
|
|
||||||
static void start() {
|
|
||||||
task = periodic(const Duration(milliseconds: 10), (cycle) async {
|
|
||||||
if (running) {
|
|
||||||
vmRun();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static void stop() {
|
|
||||||
logger.d('VM stop');
|
|
||||||
task.complete(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Uint8List indexFileBuffer = Uint8List(0);
|
|
||||||
static ByteData streamIndex = ByteData(0);
|
|
||||||
static int indexFileVersion = 0;
|
|
||||||
static int storiesCount = 0;
|
|
||||||
static int readPtr = 0;
|
|
||||||
static bool indexValid = false;
|
|
||||||
|
|
||||||
static const int tlvArrayType = 0xAB;
|
|
||||||
static const int tlvObjectType = 0xE7;
|
|
||||||
static const int tlvIntegerType = 0x77;
|
|
||||||
static const int tlvStringType = 0x3D;
|
|
||||||
static const int tlvRealType = 0xB8;
|
|
||||||
|
|
||||||
static List<
|
|
||||||
({
|
|
||||||
String uuid,
|
|
||||||
String titleImage,
|
|
||||||
String titleSound,
|
|
||||||
String title,
|
|
||||||
String description,
|
|
||||||
int version
|
|
||||||
})> stories = [];
|
|
||||||
|
|
||||||
// Returns the size, 0 if error
|
|
||||||
static int getTl(int expectedType) {
|
|
||||||
int size = 0;
|
|
||||||
if (streamIndex.getUint8(readPtr) == expectedType) {
|
|
||||||
readPtr++;
|
|
||||||
size = streamIndex.getUint16(readPtr, Endian.little);
|
|
||||||
readPtr += 2;
|
|
||||||
} else {
|
|
||||||
throw Exception("Expected type: $expectedType");
|
|
||||||
}
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int getIntegerValue() {
|
|
||||||
int size = getTl(tlvIntegerType);
|
|
||||||
|
|
||||||
if (size == 4) {
|
|
||||||
int value = streamIndex.getUint32(readPtr, Endian.little);
|
|
||||||
readPtr += 4;
|
|
||||||
return value;
|
|
||||||
} else {
|
|
||||||
throw Exception("Expected an integer of size 4 bytes");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static String getStringValue() {
|
|
||||||
int size = getTl(tlvStringType);
|
|
||||||
if (size > 0) {
|
|
||||||
String value =
|
|
||||||
ByteData.sublistView(streamIndex, readPtr, readPtr + size).toString();
|
|
||||||
readPtr += size;
|
|
||||||
return value;
|
|
||||||
} else {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void loadIndexFile(String filePath) async {
|
|
||||||
final file = File(filePath);
|
|
||||||
if (!await file.exists()) {
|
|
||||||
logger.d('Le fichier n\'existe pas.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ouvrir le fichier en mode lecture binaire
|
|
||||||
indexFileBuffer = file.readAsBytesSync();
|
|
||||||
readPtr = 0;
|
|
||||||
streamIndex =
|
|
||||||
ByteData.sublistView(indexFileBuffer, readPtr); // start at zero
|
|
||||||
|
|
||||||
if (indexFileBuffer.lengthInBytes > 3) {
|
|
||||||
// Root must be an object containing 2 elements
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (getTl(tlvObjectType) == 2) {
|
|
||||||
indexFileVersion = getIntegerValue();
|
|
||||||
storiesCount = getTl(tlvArrayType);
|
|
||||||
|
|
||||||
for (int i = 0; i < storiesCount; i++) {
|
|
||||||
if (getTl(tlvObjectType) == 6) {
|
|
||||||
var record = (
|
|
||||||
uuid: getStringValue(),
|
|
||||||
titleImage: getStringValue(),
|
|
||||||
titleSound: getStringValue(),
|
|
||||||
title: getStringValue(),
|
|
||||||
description: getStringValue(),
|
|
||||||
version: getIntegerValue()
|
|
||||||
);
|
|
||||||
|
|
||||||
logger.d('Found story: $record.title');
|
|
||||||
|
|
||||||
stories.add(record);
|
|
||||||
} else {
|
|
||||||
logger.e("Expected object of 6 elements at root");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.e("Expected object of 2 elements at root");
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logger.e('Exception reading index file: $e');
|
|
||||||
} finally {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -44,6 +44,9 @@ dependencies:
|
||||||
ffi: ^2.1.0
|
ffi: ^2.1.0
|
||||||
logger: ^2.2.0
|
logger: ^2.2.0
|
||||||
file_picker: ^7.0.2
|
file_picker: ^7.0.2
|
||||||
|
dqoi: ^1.3.0
|
||||||
|
audioplayers: ^5.2.1
|
||||||
|
event_bus: ^2.0.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
|
||||||
|
|
@ -25,12 +25,12 @@ static media_callback gMediaCallback = nullptr;
|
||||||
//---------------------------------------------------------------------------------------
|
//---------------------------------------------------------------------------------------
|
||||||
// VM Stuff
|
// VM Stuff
|
||||||
//---------------------------------------------------------------------------------------
|
//---------------------------------------------------------------------------------------
|
||||||
uint8_t rom_data[16*1024];
|
static uint8_t rom_data[16*1024];
|
||||||
uint8_t ram_data[16*1024];
|
static uint8_t ram_data[16*1024];
|
||||||
chip32_ctx_t chip32_ctx;
|
static chip32_ctx_t chip32_ctx;
|
||||||
|
|
||||||
|
|
||||||
chip32_result_t run_result;
|
static chip32_result_t run_result;
|
||||||
|
|
||||||
|
|
||||||
static uint8_t IndexBuf[260];
|
static uint8_t IndexBuf[260];
|
||||||
|
|
@ -67,10 +67,10 @@ uint8_t story_player_syscall(chip32_ctx_t *ctx, uint8_t code)
|
||||||
|
|
||||||
if (code == 1) // // Execute media
|
if (code == 1) // // Execute media
|
||||||
{
|
{
|
||||||
printf("SYSCALL 1\n");
|
std::cout << "[STORYVM] Syscall 1" << std::endl;
|
||||||
fflush(stdout);
|
// for (int i = 0; i< REGISTER_COUNT; i++) {
|
||||||
// UnloadTexture(*tex);
|
// std::cout << "[STORYVM] Reg: " << i << ", value: " << (int)ctx->registers[i] << std::endl;
|
||||||
// *tex =
|
// }
|
||||||
|
|
||||||
if (ctx->registers[R0] != 0)
|
if (ctx->registers[R0] != 0)
|
||||||
{
|
{
|
||||||
|
|
@ -80,18 +80,15 @@ uint8_t story_player_syscall(chip32_ctx_t *ctx, uint8_t code)
|
||||||
|
|
||||||
if (gMediaCallback)
|
if (gMediaCallback)
|
||||||
{
|
{
|
||||||
|
std::cout << "[STORYVM] Execute callback (image)" << std::endl;
|
||||||
gMediaCallback(0, image);
|
gMediaCallback(0, image);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// texture = LoadTexture(image_path); // FIXME
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// UnloadTexture(texture); // FIXME
|
std::cout << "[STORYVM] No image" << std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (ctx->registers[R1] != 0)
|
if (ctx->registers[R1] != 0)
|
||||||
{
|
{
|
||||||
// sound file name address is in R1
|
// sound file name address is in R1
|
||||||
|
|
@ -100,26 +97,20 @@ uint8_t story_player_syscall(chip32_ctx_t *ctx, uint8_t code)
|
||||||
|
|
||||||
if (gMediaCallback)
|
if (gMediaCallback)
|
||||||
{
|
{
|
||||||
|
std::cout << "[STORYVM] Execute callback (sound)" << std::endl;
|
||||||
gMediaCallback(1, sound);
|
gMediaCallback(1, sound);
|
||||||
}
|
}
|
||||||
|
|
||||||
// gMusic = LoadMusicStream(sound_path);
|
|
||||||
// gMusic.looping = false;
|
|
||||||
// gMusicLoaded = true;
|
|
||||||
|
|
||||||
|
|
||||||
// FIXME
|
|
||||||
// if (IsMusicReady(gMusic))
|
|
||||||
// {
|
|
||||||
// PlayMusicStream(gMusic);
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::cout << "[STORYVM] No sound" << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
retCode = SYSCALL_RET_WAIT_EV; // set the VM in pause
|
retCode = SYSCALL_RET_WAIT_EV; // set the VM in pause
|
||||||
}
|
}
|
||||||
else if (code == 2) // Wait Event
|
else if (code == 2) // Wait Event
|
||||||
{
|
{
|
||||||
printf("SYSCALL 2\n");
|
std::cout << "[STORYVM] Syscall 2 (wait for event)" << std::endl;
|
||||||
fflush(stdout);
|
|
||||||
retCode = SYSCALL_RET_WAIT_EV; // set the VM in pause
|
retCode = SYSCALL_RET_WAIT_EV; // set the VM in pause
|
||||||
}
|
}
|
||||||
return retCode;
|
return retCode;
|
||||||
|
|
@ -132,18 +123,24 @@ extern "C" void storyvm_run()
|
||||||
if (run_result == VM_OK)
|
if (run_result == VM_OK)
|
||||||
{
|
{
|
||||||
run_result = chip32_step(&chip32_ctx);
|
run_result = chip32_step(&chip32_ctx);
|
||||||
|
|
||||||
|
// for (int i = 0; i< REGISTER_COUNT; i++) {
|
||||||
|
// std::cout << "[STORYVM] Reg: " << i << ", value: " << (int)chip32_ctx.registers[i] << std::endl;
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
extern "C" void storyvm_stop()
|
extern "C" void storyvm_stop()
|
||||||
{
|
{
|
||||||
|
std::cout << "[STORYVM] Stop: " << std::endl;
|
||||||
run_result = VM_FINISHED;
|
run_result = VM_FINISHED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
extern "C" void storyvm_initialize(media_callback cb)
|
extern "C" void storyvm_initialize(media_callback cb)
|
||||||
{
|
{
|
||||||
|
std::cout << "[STORYVM] Initialize: " << (void *)cb << std::endl;
|
||||||
gMediaCallback = cb;
|
gMediaCallback = cb;
|
||||||
|
|
||||||
chip32_ctx.stack_size = 512;
|
chip32_ctx.stack_size = 512;
|
||||||
|
|
@ -161,13 +158,41 @@ extern "C" void storyvm_initialize(media_callback cb)
|
||||||
run_result = VM_FINISHED;
|
run_result = VM_FINISHED;
|
||||||
|
|
||||||
storyvm_stop();
|
storyvm_stop();
|
||||||
|
|
||||||
std::cout << "[STORYVM] Initialized" << std::endl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
enum VmEventType {EvNoEvent, EvStep, EvOkButton, EvPreviousButton, EvNextButton, EvAudioFinished, EvStop};
|
||||||
|
|
||||||
extern "C" void storyvm_send_event(int event)
|
extern "C" void storyvm_send_event(int event)
|
||||||
{
|
{
|
||||||
|
if (event == VmEventType::EvStep)
|
||||||
|
{
|
||||||
|
run_result = VM_OK;
|
||||||
|
}
|
||||||
|
else if (event == VmEventType::EvOkButton)
|
||||||
|
{
|
||||||
|
chip32_ctx.registers[R0] = 0x01;
|
||||||
|
run_result = VM_OK;
|
||||||
|
}
|
||||||
|
else if (event == VmEventType::EvPreviousButton)
|
||||||
|
{
|
||||||
|
chip32_ctx.registers[R0] = 0x02;
|
||||||
|
run_result = VM_OK;
|
||||||
|
}
|
||||||
|
else if (event == VmEventType::EvNextButton)
|
||||||
|
{
|
||||||
|
chip32_ctx.registers[R0] = 0x04;
|
||||||
|
run_result = VM_OK;
|
||||||
|
}
|
||||||
|
else if (event == VmEventType::EvAudioFinished)
|
||||||
|
{
|
||||||
|
chip32_ctx.registers[R0] = 0x08;
|
||||||
|
run_result = VM_OK;
|
||||||
|
}
|
||||||
|
else if (event == VmEventType::EvStop)
|
||||||
|
{
|
||||||
|
run_result = VM_FINISHED;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" void storyvm_start(const uint8_t *data, uint32_t size)
|
extern "C" void storyvm_start(const uint8_t *data, uint32_t size)
|
||||||
|
|
@ -177,12 +202,12 @@ extern "C" void storyvm_start(const uint8_t *data, uint32_t size)
|
||||||
memcpy(chip32_ctx.rom.mem, data, size);
|
memcpy(chip32_ctx.rom.mem, data, size);
|
||||||
run_result = VM_OK;
|
run_result = VM_OK;
|
||||||
chip32_initialize(&chip32_ctx);
|
chip32_initialize(&chip32_ctx);
|
||||||
|
std::cout << "[STORYVM] Start" << std::endl;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
run_result = VM_FINISHED;
|
run_result = VM_FINISHED;
|
||||||
|
std::cout << "[STORYVM] Not started (not enough memory)" << std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::cout << "[STORYVM] Start" << std::endl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue