diff --git a/lib/app.dart b/lib/app.dart index ba687ca..839a9fc 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,6 +1,4 @@ -import 'package:epubx/epubx.dart'; -import 'package:flipub/book_service.dart'; -import 'package:flipub/views/book_view.dart'; +import 'package:flipub/views/library_view.dart'; import 'package:flutter/material.dart'; const sharedPreferencesFontSizeKey = 'fontSize'; @@ -8,31 +6,14 @@ const sharedPreferencesFontSizeKey = 'fontSize'; class Flipub extends StatelessWidget { const Flipub({ super.key, - required this.bookService, }); - final BookService bookService; - @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, theme: ThemeData.dark(), - home: FutureBuilder( - future: bookService.parseBook( - 'book_1.epub', - ), - builder: (context, snapshot) { - if (snapshot.hasData) { - return BookView( - book: snapshot.data!, - ); - } - return const Center( - child: CircularProgressIndicator(), - ); - }, - ), + home: const LibrayView(), ); } } diff --git a/lib/book_service.dart b/lib/book_service.dart deleted file mode 100644 index 8c78a87..0000000 --- a/lib/book_service.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:epubx/epubx.dart'; -import 'package:flutter/services.dart'; - -class BookService { - Future parseBook(String fileName) async { - String fullPath = 'assets/books/$fileName'; - ByteData byteData = await rootBundle.load(fullPath); - List bytes = byteData.buffer.asUint8List(); - return EpubReader.readBook(bytes); - } -} diff --git a/lib/data/book_library.dart b/lib/data/book_library.dart new file mode 100644 index 0000000..fead4ad --- /dev/null +++ b/lib/data/book_library.dart @@ -0,0 +1,7 @@ +class BookLibrary { + const BookLibrary({ + required this.bookFileNames, + }); + + final List bookFileNames; +} diff --git a/lib/main.dart b/lib/main.dart index 71015ac..ffa805e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,15 +1,11 @@ -import 'package:flipub/book_service.dart'; import 'package:flutter/material.dart'; import 'package:flipub/app.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; Future main() async { - final BookService bookService = BookService(); runApp( - ProviderScope( - child: Flipub( - bookService: bookService, - ), + const ProviderScope( + child: Flipub(), ), ); } diff --git a/lib/providers/book_provider.dart b/lib/providers/book_provider.dart new file mode 100644 index 0000000..23ac0c4 --- /dev/null +++ b/lib/providers/book_provider.dart @@ -0,0 +1,15 @@ +import 'dart:async'; + +import 'package:epubx/epubx.dart'; +import 'package:flutter/services.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'book_provider.g.dart'; + +@riverpod +Future book(BookRef ref, String fileName) async { + String fullPath = 'assets/books/$fileName'; + ByteData byteData = await rootBundle.load(fullPath); + List bytes = byteData.buffer.asUint8List(); + return EpubReader.readBook(bytes); +} diff --git a/lib/providers/book_provider.g.dart b/lib/providers/book_provider.g.dart new file mode 100644 index 0000000..714531e --- /dev/null +++ b/lib/providers/book_provider.g.dart @@ -0,0 +1,155 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'book_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$bookHash() => r'f990cb53021798f8c0bde20f1eda2b34209d62cc'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +/// See also [book]. +@ProviderFor(book) +const bookProvider = BookFamily(); + +/// See also [book]. +class BookFamily extends Family> { + /// See also [book]. + const BookFamily(); + + /// See also [book]. + BookProvider call( + String fileName, + ) { + return BookProvider( + fileName, + ); + } + + @override + BookProvider getProviderOverride( + covariant BookProvider provider, + ) { + return call( + provider.fileName, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'bookProvider'; +} + +/// See also [book]. +class BookProvider extends AutoDisposeFutureProvider { + /// See also [book]. + BookProvider( + String fileName, + ) : this._internal( + (ref) => book( + ref as BookRef, + fileName, + ), + from: bookProvider, + name: r'bookProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$bookHash, + dependencies: BookFamily._dependencies, + allTransitiveDependencies: BookFamily._allTransitiveDependencies, + fileName: fileName, + ); + + BookProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.fileName, + }) : super.internal(); + + final String fileName; + + @override + Override overrideWith( + FutureOr Function(BookRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: BookProvider._internal( + (ref) => create(ref as BookRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + fileName: fileName, + ), + ); + } + + @override + AutoDisposeFutureProviderElement createElement() { + return _BookProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is BookProvider && other.fileName == fileName; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, fileName.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin BookRef on AutoDisposeFutureProviderRef { + /// The parameter `fileName` of this provider. + String get fileName; +} + +class _BookProviderElement extends AutoDisposeFutureProviderElement + with BookRef { + _BookProviderElement(super.provider); + + @override + String get fileName => (origin as BookProvider).fileName; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/providers/library_provider.dart b/lib/providers/library_provider.dart new file mode 100644 index 0000000..1fe041e --- /dev/null +++ b/lib/providers/library_provider.dart @@ -0,0 +1,17 @@ +import 'dart:async'; + +import 'package:flipub/data/book_library.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'library_provider.g.dart'; + +@riverpod +Future bookLibrary(BookLibraryRef ref) async { + return const BookLibrary( + bookFileNames: [ + 'book_1.epub', + 'book_2.epub', + 'book_3.epub', + ], + ); +} diff --git a/lib/providers/library_provider.g.dart b/lib/providers/library_provider.g.dart new file mode 100644 index 0000000..5e4d737 --- /dev/null +++ b/lib/providers/library_provider.g.dart @@ -0,0 +1,24 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'library_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$bookLibraryHash() => r'41b3aee9e5a3dcc8b69953d32e45cc1f35547512'; + +/// See also [bookLibrary]. +@ProviderFor(bookLibrary) +final bookLibraryProvider = AutoDisposeFutureProvider.internal( + bookLibrary, + name: r'bookLibraryProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$bookLibraryHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef BookLibraryRef = AutoDisposeFutureProviderRef; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/views/book_cover.dart b/lib/views/book_cover.dart new file mode 100644 index 0000000..61554f0 --- /dev/null +++ b/lib/views/book_cover.dart @@ -0,0 +1,33 @@ +import 'dart:typed_data'; + +import 'package:epubx/epubx.dart' show EpubBook; +import 'package:flutter/material.dart'; +import 'package:image/image.dart' as image; + +class BookCover extends StatelessWidget { + const BookCover({ + super.key, + required this.book, + }); + + final EpubBook book; + + @override + Widget build(BuildContext context) { + return book.CoverImage != null + ? Image.memory( + Uint8List.fromList( + image.encodePng(book.CoverImage!), + ), + width: 200, + height: 200, + ) + : const SizedBox( + height: 200, + width: 200, + child: Center( + child: Text('No cover'), + ), + ); + } +} diff --git a/lib/views/book_view.dart b/lib/views/book_view.dart index b8d5669..f291c93 100644 --- a/lib/views/book_view.dart +++ b/lib/views/book_view.dart @@ -1,18 +1,47 @@ import 'package:epubx/epubx.dart'; +import 'package:flipub/providers/book_provider.dart'; import 'package:flipub/views/chapter_view.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; class BookView extends ConsumerWidget { - final EpubBook book; + final String bookFileName; const BookView({ super.key, - required this.book, + required this.bookFileName, }); @override Widget build(BuildContext context, WidgetRef ref) { + final AsyncValue book = ref.watch(bookProvider(bookFileName)); + return book.when( + data: (EpubBook book) { + return _BookViewContent( + book: book, + ); + }, + loading: () => const Center( + child: CircularProgressIndicator(), + ), + error: (error, stackTrace) { + return Center( + child: Text('Error: $error'), + ); + }, + ); + } +} + +class _BookViewContent extends StatelessWidget { + final EpubBook book; + + const _BookViewContent({ + required this.book, + }); + + @override + Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(book.Title ?? 'Unknown title'), diff --git a/lib/views/library_view.dart b/lib/views/library_view.dart new file mode 100644 index 0000000..b3b4627 --- /dev/null +++ b/lib/views/library_view.dart @@ -0,0 +1,130 @@ +import 'dart:io'; + +import 'package:epubx/epubx.dart' show EpubBook; +import 'package:flipub/data/book_library.dart'; +import 'package:flipub/providers/book_provider.dart'; +import 'package:flipub/providers/library_provider.dart'; +import 'package:flipub/views/book_cover.dart'; +import 'package:flipub/views/book_view.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class LibrayView extends ConsumerWidget { + const LibrayView({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final library = ref.watch(bookLibraryProvider); + return library.when( + data: (library) { + return _LibraryViewContent(library: library); + }, + loading: () => const Center( + child: CircularProgressIndicator(), + ), + error: (error, stackTrace) { + return Center( + child: Text('Error: $error'), + ); + }, + ); + } +} + +class _LibraryViewContent extends StatelessWidget { + final BookLibrary library; + + const _LibraryViewContent({ + required this.library, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Library'), + ), + body: Padding( + padding: const EdgeInsets.all(48.0), + child: GridView.builder( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: Platform.isAndroid || Platform.isIOS ? 2 : 4, + ), + itemCount: library.bookFileNames.length, + itemBuilder: (context, index) { + return LibraryTile( + bookFileName: library.bookFileNames[index], + ); + }, + ), + ), + ); + } +} + +class LibraryTile extends ConsumerWidget { + const LibraryTile({super.key, required this.bookFileName}); + + final String bookFileName; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final book = ref.watch(bookProvider(bookFileName)); + return book.when( + data: (book) { + return _LibraryTileContent( + bookFileName: bookFileName, + book: book, + ); + }, + loading: () => const Center( + child: CircularProgressIndicator(), + ), + error: (error, stackTrace) { + return Center( + child: Text('Error: $error'), + ); + }, + ); + } +} + +class _LibraryTileContent extends StatelessWidget { + final String bookFileName; + final EpubBook book; + + const _LibraryTileContent({ + required this.bookFileName, + required this.book, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => BookView( + bookFileName: bookFileName, + ), + ), + ); + }, + child: Card( + child: FittedBox( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + BookCover(book: book), + Text(book.Title ?? 'Unknown title'), + ], + ), + ), + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index ded56d9..8f2bb40 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -393,7 +393,7 @@ packages: source: hosted version: "4.0.2" image: - dependency: transitive + dependency: "direct main" description: name: image sha256: "8e9d133755c3e84c73288363e6343157c383a0c6c56fc51afcc5d4d7180306d6" diff --git a/pubspec.yaml b/pubspec.yaml index caf162e..a53cf7f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,6 +42,7 @@ dependencies: shared_preferences: ^2.2.2 flutter_riverpod: ^2.4.10 riverpod_annotation: ^2.3.4 + image: ^3.3.0 dev_dependencies: flutter_test: