Skip to content

Implement web #15

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 29 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c2fae0e
feat: Initialize flutter_readium_web package with core functionality
SifAa Jun 12, 2025
b4ac129
feat(example): add scroll controller to BookshelfPage
SifAa Jun 13, 2025
ddef257
feat(flutter_readium): add web as a supported platform
SifAa Jun 13, 2025
842485b
feat: implement getPublication method in FlutterReadium and FlutterRe…
SifAa Jun 18, 2025
1f221c0
chor: format example pubspec
SifAa Jun 19, 2025
95435fc
chore: update web pubspec.yaml with homepage, repository, and publish…
SifAa Jun 19, 2025
4d452da
feat(web): enhance publication handling by adding pubId to openPublic…
SifAa Jun 19, 2025
6848d89
Refactor preferences handling in ReadiumReader
SifAa Jun 23, 2025
b5b8604
chore: update @readium/navigator and @readium/navigator-html-injectab…
SifAa Jun 23, 2025
65b0c67
feat(web): implement EPUB preferences handling in JsPublicationChanne…
SifAa Jun 23, 2025
97929be
feat(example): add web to example app
SifAa Jun 23, 2025
2b11bba
fix(locator): ensure href only contains path in Locator object
SifAa Jun 23, 2025
927a1ed
fix(example): correct indentation for webManifestList asset in pubspec
SifAa Jun 23, 2025
8f19c82
fix(web): preferences handling has correct vertical scroll conversion…
SifAa Jun 24, 2025
100a345
feat(web): store default EPUB preferences in setEPUBPreferences method
SifAa Jun 24, 2025
3675147
feat: add pageMargins to EPUB preferences handling
SifAa Jun 26, 2025
13336e2
chore: clean up comments
SifAa Jun 26, 2025
dbcd9de
feat: implement setDefaultPreferences method for EPUB preferences han…
SifAa Jun 26, 2025
b06b16a
refactor: simplify widget return structure in ReadiumReaderWidget for…
SifAa Jun 26, 2025
1b08afa
feat(web): scroll up and down using keyboard
SifAa Jun 27, 2025
cca6b1c
feat: add build and installation scripts
SifAa Jun 27, 2025
987d98e
refactor(web): extract helper functions for fetching manifest and ini…
SifAa Jun 27, 2025
bb1484e
chor(example): additional publication manifest URL
SifAa Jun 27, 2025
bb0769c
chor(test): mock methods match FlutterReadiumPlatform
SifAa Jun 27, 2025
4af5b27
refactor: move web from own package into flutter_readium package
SifAa Jul 3, 2025
03a9b9e
Merge branch 'main' into add-web
SifAa Jul 3, 2025
6eb281b
chore(web): update @readium dependencies to latest versions
SifAa Jul 14, 2025
163df5f
chore(web): update readium dependencies to latest non beta versions
SifAa Aug 4, 2025
9894311
review follow up
SifAa Aug 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 35 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ which implements both TTS and Text+Audio sync on top of the native toolkits via
This plugin is supposed to support both EPUB and WebPubs, with or without pre-recorded audio.

## Plans

We will work on the main branch on a modernized and simple API using newest toolkits and utilize much more of the toolkit functionality.

See repo Issues for up-to-date progress.

General TODO:
- [X] Use Preferences API on both platforms.
- [X] Use Decorator API for highlighting.
- [X] Test TTS and Audio navigators for maturity, possibly replacing our own audio handlers.

- [x] Use Preferences API on both platforms.
- [x] Use Decorator API for highlighting.
- [x] Test TTS and Audio navigators for maturity, possibly replacing our own audio handlers.
- [ ] Finding: Readium native toolkits do not yet support MediaOverlays, making support for "karaoke books" difficult.
We either wait for this to be implemented, or do something similar to the `nota-lyt4` branch and use `audio_service` and `just_audio` plugins for MediaOverlay playback.

Expand All @@ -40,7 +42,7 @@ Also, update your Android and iOS projects as follows:
your `android/app/src/main/AndroidManifest.xml` file:

```html
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.WAKE_LOCK" />
```

### iOS
Expand Down Expand Up @@ -71,6 +73,34 @@ end
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<true />
</dict>
```

### Web

To use this plugin for web, follow these steps to ensure everything works correctly:

#### 1. Copy the JavaScript File

To use the JavaScript file from the plugin in your Flutter web app, run the following command in your terminal from the root directory of your app:

```bash
dart run flutter_readium:copy_js_file <destination_directory>
```

It is recommended to place the destination directory within the `web` directory or a subdirectory of it. Avoid saving it outside the `web` directory.

#### 2. Add Scripts to `index.html`

After copying the JavaScript file to your app, add Flutter's initialization JS code and the plugin JS to the `head` section of your `index.html` file:

```html
<!-- Flutter initialization JS code -->
<script src="flutter.js" defer></script>

<!-- Plugin JS code -->
<script src="readiumReader.js" defer></script>
```

If the plugin's JS file is not saved in the same directory as `index.html`, update the source path accordingly.
5 changes: 5 additions & 0 deletions bin/build_js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Build for flutter_readium web
echo "Building flutter_readium web"
cd ./flutter_readium/
npm i
npm run build
5 changes: 5 additions & 0 deletions bin/code_gen
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/bash

set -e

cd ./flutter_readium_platform_interface; dart run build_runner build --delete-conflicting-outputs
19 changes: 19 additions & 0 deletions bin/forAll
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/bin/bash

if [ "x$1" == "x" ]
then
echo "Possible usage:"
echo " $0 dart pub outdated"
echo " $0 dart pub upgrade"
exit 1
fi

dir=(
"./flutter_readium"
"./flutter_readium_platform_interface"
"./flutter_readium_web"
)
for i in "${dir[@]}"; do
echo -e "\033[35;1m=== $@ $i ===\033[0m"
(cd "$i" || exit; $@)
done
13 changes: 13 additions & 0 deletions bin/install
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/bash

set -e

./bin/forAll flutter pub get

# this is not working as expected
if [ "$(uname)" == "Darwin" ]; then
(cd ./flutter_readium/example/ios; pod install --repo-update)
fi


./bin/build_js
16 changes: 16 additions & 0 deletions flutter_readium/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,19 @@ Android and/or iOS.
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.

## Editing on web

When making changes to the TypeScript files, convert the main TypeScript file (`ReadiumReader.ts`) to JavaScript using:

```bash
npm run build
```

Run this command from the root of the plugin.

To test the changes, follow the installation instructions in the example app directory.

```bash
dart run flutter_readium:copy_js_file <destination_directory>
```
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ fun epubPreferencesFromMap(
fontWeight = prefMap["fontWeight"]?.toDoubleOrNull() ?: defaults?.fontWeight,
scroll = prefMap["verticalScroll"]?.toBoolean() ?: defaults?.scroll,
backgroundColor = prefMap["backgroundColor"]?.let { readiumColorFromCSS(it) } ?: defaults?.backgroundColor,
textColor = prefMap["textColor"]?.let { readiumColorFromCSS(it) } ?: defaults?.textColor
textColor = prefMap["textColor"]?.let { readiumColorFromCSS(it) } ?: defaults?.textColor,
pageMargins = prefMap["pageMargins"]?.toDoubleOrNull() ?: defaults?.pageMargins,
)
return newPreferences
} catch (ex: Exception) {
Expand Down
42 changes: 42 additions & 0 deletions flutter_readium/bin/copy_js_file.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// ignore_for_file: avoid_print

import 'dart:io';
import 'dart:isolate';

void main(List<String> args) async {
if (args.isEmpty) {
print('Usage: dart run flutter_readium:copy_js_file <destination_directory>');
return;
}

final destinationDir = args[0];
final packageUri = Uri.parse('package:flutter_readium/helpers/readiumReader.js');
final resolvedUri = await Isolate.resolvePackageUri(packageUri);

if (resolvedUri == null) {
print('Error: Could not resolve package URI');
return;
}

final sourcePath = resolvedUri.toFilePath();
final sourceFile = File(sourcePath);

if (!sourceFile.existsSync()) {
print('Error: Source file not found: $sourcePath');
return;
}

final destinationPath = '$destinationDir/readiumReader.js';
// final destinationFile = File(destinationPath);

try {
// Ensure the destination directory exists
Directory(destinationDir).createSync(recursive: true);

// Copy the file
sourceFile.copySync(destinationPath);
print('File copied to $destinationPath');
} catch (e) {
print('Error copying file: $e');
}
}
4 changes: 4 additions & 0 deletions flutter_readium/example/assets/webManifestList.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[
"https://merkur.live.dbb.dk/opds2/publication/free/merkur:libraryid:37881/manifest.json",
"https://publication-server.readium.org/bW9ieS1kaWNrLmVwdWI/manifest.json"
]
7 changes: 5 additions & 2 deletions flutter_readium/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:path_provider/path_provider.dart';

import 'extensions/text_settings_theme.dart' show themes;
import 'pages/index.dart';
import 'state/index.dart';

Expand All @@ -29,7 +28,11 @@ Future<void> main() async {
lazy: false,
),
BlocProvider(
create: (final _) => TextSettingsBloc()..add(ChangeTheme(themes[0])),
create: (final _) {
final bloc = TextSettingsBloc();
bloc.setDefaultPreferences();
return bloc;
},
),
// BlocProvider(
// create: (final _) => TtsSettingsBloc(),
Expand Down
59 changes: 39 additions & 20 deletions flutter_readium/example/lib/pages/bookshelf.page.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import 'dart:convert';
import 'dart:io';

import 'package:file_picker/file_picker.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
Expand All @@ -19,40 +21,47 @@ class BookshelfPage extends StatefulWidget {

class BookshelfPageState extends State<BookshelfPage> {
final _flutterReadiumPlugin = FlutterReadium();
final ScrollController _scrollController = ScrollController();
List<Publication> _testPublications = [];
bool _isLoading = true;
// Pubs loaded from assets folder should not be delete-able as they would just be re-added on restart
// we should probably make it so they will only be loaded once
final List<String> _identifiersFromAsset = ['dk-nota-714304'];

// TODO: find a better solution for initialLocator.
final initialLocator = Locator(
href: '/OPS/main2.xml',
title: 'Test',
// locations: Locations(cssSelector: '#chapter_245810 > div > p:nth-child(19)'),
locations: Locations(
progression: 0.38461538461538464,
position: 26,
totalProgression: 0.176056338028169,
),
type: 'text/html',
);

@override
void initState() {
super.initState();
_initialize();
}

Future<void> _initialize() async {
// should only be done first time app is started. how to do that?
final localPublications = await PublicationUtils.moveAssetPublicationsToReadiumStorage();
final loadedPublications = <Publication>[];

for (String localPubPath in localPublications) {
Publication? publication = await openPublicationFromUrl(localPubPath);
if (publication != null) {
loadedPublications.add(publication);
if (kIsWeb) {
// Web: Load publications from JSON asset
final String response = await rootBundle.loadString('assets/webManifestList.json');
final List<dynamic> manifestHrefs = json.decode(response);
for (final href in manifestHrefs) {
try {
Publication? pub;
pub = await openPublicationFromUrl(href.toString());
if (pub != null) {
loadedPublications.add(pub);
await _flutterReadiumPlugin.closePublication(pub.identifier);
}
} on Exception catch (e) {
debugPrint('Error opening publication: $e');
}
}
} else {
// should only be done first time app is started. how to do that?
final localPublications = await PublicationUtils.moveAssetPublicationsToReadiumStorage();

for (String localPubPath in localPublications) {
Publication? publication = await openPublicationFromUrl(localPubPath);
if (publication != null) {
loadedPublications.add(publication);
}
}
}

Expand All @@ -64,7 +73,9 @@ class BookshelfPageState extends State<BookshelfPage> {

Future<Publication?> openPublicationFromUrl(String pubUrl) async {
try {
Publication pub = await _flutterReadiumPlugin.openPublication(pubUrl);
Publication pub = kIsWeb
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could generalize this, so all platforms use the same function here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What @ddfreiling has written for native uses openPublication, but that is not possible with ts-toolkit. We need to make another PR with implements getPublication in native and then we can generalize it. Until then this should stay

? await _flutterReadiumPlugin.getPublication(pubUrl)
: await _flutterReadiumPlugin.openPublication(pubUrl);
debugPrint('openPublication success: ${pub.metadata.title}');
return pub;
} on PlatformException catch (e) {
Expand All @@ -73,6 +84,12 @@ class BookshelfPageState extends State<BookshelfPage> {
}
}

@override
void dispose() {
_scrollController.dispose();
super.dispose();
}

@override
Widget build(final BuildContext context) => Scaffold(
restorationId: 'bookshelf_page',
Expand All @@ -87,9 +104,11 @@ class BookshelfPageState extends State<BookshelfPage> {
children: [
Expanded(
child: CupertinoScrollbar(
controller: _scrollController,
thickness: 5.0,
thumbVisibility: true,
child: ListView.builder(
controller: _scrollController,
itemCount: _testPublications.length,
itemBuilder: (final context, final index) {
final publication = _testPublications[index];
Expand Down
3 changes: 2 additions & 1 deletion flutter_readium/example/lib/pages/player.page.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_readium/flutter_readium.dart';
import 'package:pointer_interceptor/pointer_interceptor.dart';

import '../state/index.dart';
import '../widgets/index.dart';
Expand Down Expand Up @@ -66,7 +67,7 @@ class _PlayerPageState extends State<PlayerPage> with RestorationMixin {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (final context) => const TextSettingsWidget(),
builder: (final context) => PointerInterceptor(child: const TextSettingsWidget()),
);
},
tooltip: 'Open text style settings',
Expand Down
Loading