From a91b74a44e96b630ed0f60c45cc35038e61ac5f3 Mon Sep 17 00:00:00 2001 From: Kara Date: Sat, 19 Nov 2022 23:10:29 -0500 Subject: [PATCH] Added Categories - Added categories - Version bump - Removed `file_utils` - Home screen revamp - (Hopefully) fixed bug in iOS with images not displaying properly for cbr/cbz - Changed version on Kotlin --- android/build.gradle | 2 +- lib/main.dart | 6 +- lib/models/entry.dart | 10 +- lib/models/entry.g.dart | 7 +- lib/models/folder.dart | 29 ++ lib/models/folder.g.dart | 50 +++ lib/providers/deleteComic.dart | 10 +- lib/providers/fetchBooks.dart | 27 +- lib/providers/fetchCategories.dart | 87 ++++- lib/providers/folderProvider.dart | 101 +++++ lib/providers/login.dart | 2 +- lib/screens/collectionScreen.dart | 294 +++++++++++++++ lib/screens/downloaderScreen.dart | 39 +- lib/screens/homeScreen.dart | 573 +++++++++++++++++++++++------ lib/screens/settingsScreen.dart | 2 +- pubspec.lock | 35 +- pubspec.yaml | 5 +- 17 files changed, 1110 insertions(+), 169 deletions(-) create mode 100644 lib/models/folder.dart create mode 100644 lib/models/folder.g.dart create mode 100644 lib/providers/folderProvider.dart create mode 100644 lib/screens/collectionScreen.dart diff --git a/android/build.gradle b/android/build.gradle index 83ae2200..05826857 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.6.10' + ext.kotlin_version = '1.7.20' repositories { google() mavenCentral() diff --git a/lib/main.dart b/lib/main.dart index c8a0b1b3..8a2a54dd 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart'; import 'package:jellybook/screens/loginScreen.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:jellybook/models/entry.dart'; +import 'package:jellybook/models/folder.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -10,16 +11,19 @@ Future main() async { await Hive.initFlutter(); Hive.registerAdapter(EntryAdapter()); + Hive.registerAdapter(FolderAdapter()); await Hive.openBox('bookShelf'); + await Hive.openBox('folders'); // if running in debug mode then clear the hive box if (kDebugMode) { try { await Hive.box('bookShelf').clear(); + await Hive.box('folders').clear(); } catch (e) { debugPrint(e.toString()); } - debugPrint("cleared hive box"); + debugPrint("cleared hive boxes"); } runApp(MyApp()); diff --git a/lib/models/entry.dart b/lib/models/entry.dart index efb33f63..2398ea6a 100644 --- a/lib/models/entry.dart +++ b/lib/models/entry.dart @@ -55,6 +55,10 @@ class Entry extends HiveObject { @HiveField(14) late String type = 'comic'; + // parentId + @HiveField(15) + late String parentId = ''; + Entry({ required this.id, required this.title, @@ -71,10 +75,6 @@ class Entry extends HiveObject { this.folderPath = '', this.filePath = '', this.type = 'comic', + this.parentId = '', }); - - // void _requireInitialized() { - // Hive.openBox('bookShelf'); - // } - } diff --git a/lib/models/entry.g.dart b/lib/models/entry.g.dart index 4cbefb3f..eb9fec44 100644 --- a/lib/models/entry.g.dart +++ b/lib/models/entry.g.dart @@ -32,13 +32,14 @@ class EntryAdapter extends TypeAdapter { folderPath: fields[12] as String, filePath: fields[13] as String, type: fields[14] as String, + parentId: fields[15] as String, ); } @override void write(BinaryWriter writer, Entry obj) { writer - ..writeByte(15) + ..writeByte(16) ..writeByte(0) ..write(obj.id) ..writeByte(1) @@ -68,7 +69,9 @@ class EntryAdapter extends TypeAdapter { ..writeByte(13) ..write(obj.filePath) ..writeByte(14) - ..write(obj.type); + ..write(obj.type) + ..writeByte(15) + ..write(obj.parentId); } @override diff --git a/lib/models/folder.dart b/lib/models/folder.dart new file mode 100644 index 00000000..5602a7c2 --- /dev/null +++ b/lib/models/folder.dart @@ -0,0 +1,29 @@ +// The purpose of this file is to define the model for the folders that will house collections of books/compics + +import 'package:hive_flutter/hive_flutter.dart'; + +part 'folder.g.dart'; + +// Folder should have a name, a image, and a list of book ids +@HiveType(typeId: 1) +class Folder { + @HiveField(0) + String name; + + @HiveField(1) + String id; + + @HiveField(2) + String image = ''; + + @HiveField(3) + List bookIds = ['']; + + // image and bookIds are optional + Folder({ + required this.name, + required this.id, + this.image = '', + this.bookIds = const [''], + }); +} diff --git a/lib/models/folder.g.dart b/lib/models/folder.g.dart new file mode 100644 index 00000000..806c3d0e --- /dev/null +++ b/lib/models/folder.g.dart @@ -0,0 +1,50 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'folder.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class FolderAdapter extends TypeAdapter { + @override + final int typeId = 1; + + @override + Folder read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return Folder( + name: fields[0] as String, + id: fields[1] as String, + image: fields[2] as String, + bookIds: (fields[3] as List).cast(), + ); + } + + @override + void write(BinaryWriter writer, Folder obj) { + writer + ..writeByte(4) + ..writeByte(0) + ..write(obj.name) + ..writeByte(1) + ..write(obj.id) + ..writeByte(2) + ..write(obj.image) + ..writeByte(3) + ..write(obj.bookIds); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is FolderAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/providers/deleteComic.dart b/lib/providers/deleteComic.dart index 195da917..2f66f735 100644 --- a/lib/providers/deleteComic.dart +++ b/lib/providers/deleteComic.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:jellybook/models/entry.dart'; -import 'package:file_utils/file_utils.dart'; +import 'dart:io'; Future deleteComic(String id, context) async { bool delete = false; @@ -40,9 +40,13 @@ Future confirmedDelete(String id, context) async { if (entry.downloaded == true) { debugPrint("Deleting file"); debugPrint(entry.folderPath); - final List path = [entry.folderPath]; + final String path = entry.folderPath; debugPrint(path.toString()); - FileUtils.rmdir(path); + try { + Directory(path).deleteSync(recursive: true); + } catch (e) { + debugPrint("error deleting directory: $e"); + } debugPrint("Deleted comic: " + entry.title); debugPrint("Deleted comic path: " + entry.folderPath); diff --git a/lib/providers/fetchBooks.dart b/lib/providers/fetchBooks.dart index 48b1d9c9..857ff664 100644 --- a/lib/providers/fetchBooks.dart +++ b/lib/providers/fetchBooks.dart @@ -4,10 +4,10 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as Http; import 'dart:convert'; +import 'package:jellybook/providers/folderProvider.dart'; // database imports import 'package:jellybook/models/entry.dart'; -import 'package:hive/hive.dart'; import 'package:hive_flutter/hive_flutter.dart'; // get comics @@ -26,8 +26,7 @@ Future>> getComics( final response = Http.get( Uri.parse('$url/Users/$userId/Items' + '?StartIndex=0' + - '&Fields=PrimaryImageAspectRatio,SortName,Path,SongCount,ChildCount,MediaSourceCount,Tags,Overview' + - '&Filters=IsNotFolder' + + '&Fields=PrimaryImageAspectRatio,SortName,Path,SongCount,ChildCount,MediaSourceCount,Tags,Overview,ParentId' + '&ImageTypeLimit=1' + '&ParentId=$comicsId' + '&Recursive=true' + @@ -86,6 +85,7 @@ Future>> getComics( if (responseData['Items'][i]['CommunityRating'] != null) 'rating': responseData['Items'][i]['CommunityRating'].toDouble(), 'tags': responseData['Items'][i]['Tags'] ?? [], + 'parentId': responseData['Items'][i]['ParentId'] ?? '', }); debugPrint(responseData['Items'][i]['Name']); } @@ -118,8 +118,17 @@ Future>> getComics( type: comicFileTypes.contains( responseData['Items'][i]['Path'].split('.').last.toLowerCase()) ? 'comic' - : 'book', + : bookFileTypes.contains(responseData['Items'][i]['Path'] + .split('.') + .last + .toLowerCase()) + ? 'book' + : responseData['Items'][i]['Type'] == 'Folder' + ? 'folder' + : 'unknown', + parentId: responseData['Items'][i]['ParentId'] ?? '', ); + // debugPrint("type: ${entry.type}"); // add the entry to the database (with the name being the id) // check that the entry doesn't already exist @@ -131,15 +140,21 @@ Future>> getComics( }); if (!exists) { box.add(entry); - debugPrint("book added"); + // debugPrint("book added"); } else { - debugPrint("book already exists"); + // debugPrint("book already exists"); } } catch (e) { debugPrint(e.toString()); } } + // set the list of folders from the list of entries and then category ids + // List categoryIds = prefs.getStringList('categoryIds') ?? []; + // List entriesList = box.values.toList(); + // debugPrint("got entries list"); + // CreateFolders.createFolders(entriesList, categoryIds); + return comics; } diff --git a/lib/providers/fetchCategories.dart b/lib/providers/fetchCategories.dart index 833e1b1d..68dacc48 100644 --- a/lib/providers/fetchCategories.dart +++ b/lib/providers/fetchCategories.dart @@ -6,8 +6,14 @@ import 'dart:convert'; import 'package:jellybook/providers/fetchBooks.dart'; // import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:jellybook/providers/folderProvider.dart'; +import 'package:jellybook/models/entry.dart'; +import 'package:jellybook/models/folder.dart'; +import 'package:hive_flutter/hive_flutter.dart'; -Future>> getServerCategories(context) async { +// have optional perameter to have the function return the list of folders +Future>> getServerCategories(context, + {bool returnFolders = false}) async { debugPrint("getting server categories"); final prefs = await SharedPreferences.getInstance(); final token = prefs.getString('accessToken') ?? ""; @@ -16,7 +22,7 @@ Future>> getServerCategories(context) async { final client = prefs.getString('client') ?? "JellyBook"; final device = prefs.getString('device') ?? ""; final deviceId = prefs.getString('deviceId') ?? ""; - final version = prefs.getString('version') ?? "1.0.6"; + final version = prefs.getString('version') ?? "1.0.7"; debugPrint("got prefs"); Map headers = getHeaders(url, client, device, deviceId, version, token); @@ -49,21 +55,96 @@ Future>> getServerCategories(context) async { categories.remove('music'); categories.remove('collections'); - List selected = await chooseCategories(categories, context); + List selected = []; + if (categories.contains('Comics')) { + selected.add('Comics'); + } else if (categories.contains('comics')) { + selected.add('comics'); + } else if (categories.contains('Books')) { + selected.add('Books'); + } else if (categories.contains('books')) { + selected.add('books'); + } else { + hasComics = false; + } + // get length of prefs.getStringList('categories') (its a List?) + // List selected = prefs.getStringList('categories') ?? ['Error']; + // if (selected[0] == 'Error') { + // selected = await chooseCategories(categories, context); + // debugPrint("selected: $selected"); + // prefs.setStringList('categories', selected); + // } + // if (prefs.getStringList('categories')!.length == 0) { + // selected = await chooseCategories(categories, context); + // prefs.setStringList('categories', selected); + // } else { + // selected = prefs.getStringList('categories')!; + // } List>>> comicsArray = []; + List comicsIds = []; debugPrint("selected: " + selected.toString()); for (int i = 0; i < data['Items'].length; i++) { if (selected.contains(data['Items'][i]['Name'])) { comicsId = data['Items'][i]['Id']; + comicsIds.add(comicsId); etag = data['Items'][i]['Etag']; comicsArray.add(getComics(comicsId, etag)); } } + // once all the comics are fetched, combine them into one list List> comics = []; for (int i = 0; i < comicsArray.length; i++) { + // comicsArray[i].then((value) { + // comics.addAll(value); + // }); comics.addAll(await comicsArray[i]); } + + prefs.setStringList('comicsIds', comicsIds); + + if (returnFolders) { + List categoriesList = []; + for (int i = 0; i < selected.length; i++) { + categoriesList.add(selected[i]); + } + + var boxEntries = Hive.box('bookShelf'); + List entriesList = boxEntries.values.toList(); + debugPrint("got entries list"); + var boxFolders = Hive.box('folders'); + // List foldersList = boxFolders.values.toList(); + // List folders = await boxFolders.values.toList(); + List folders = + await CreateFolders.getFolders(entriesList, categoriesList); + + debugPrint("created folders"); + // convert folders to maps + List> makeFolders() { + List> foldersMap = []; + folders.forEach((folder) { + foldersMap.add( + { + 'id': folder.id, + 'name': folder.name, + 'image': folder.image, + 'bookIds': folder.bookIds, + }, + ); + }); + return foldersMap; + } + + List> foldersMap = makeFolders(); + debugPrint("converted folders to maps"); + + // wait for the folders to be created + + // return the list of folders + return foldersMap; + } else { + return comics; + } debugPrint("Returning comics"); return comics; } else { diff --git a/lib/providers/folderProvider.dart b/lib/providers/folderProvider.dart new file mode 100644 index 00000000..a29fb9f3 --- /dev/null +++ b/lib/providers/folderProvider.dart @@ -0,0 +1,101 @@ +// The purpose of this file is to take the list of entries and create folders if their parentId is not one of the categories + +import 'package:shared_preferences/shared_preferences.dart'; + +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:jellybook/models/folder.dart'; +import 'package:jellybook/models/entry.dart'; +import 'package:flutter/foundation.dart'; + +// This function takes the list of entries and creates folders if their parentId is not one of the categories +// It adds the folder to the list of folders in a hive box +class CreateFolders { + static Future createFolders( + List entries, List categories) async { + // Get the list of folders from the hive box + // debugPrint("creating folders"); + var box = Hive.box('folders'); + // var foldersBox = Hive.box('folders'); + + final prefs = await SharedPreferences.getInstance(); + var categories = prefs.getString('categories') ?? ''; + // debugPrint("categories: $categories"); + + // new plan, create the list of folders first, then add the entries to the folders + + // instead of having a List we will have a Map and convert it to a List at the end + List> newFolders = []; + // List newFolders = []; + List newFolderIds = []; + for (int i = 0; i < entries.length; i++) { + // debugPrint('Entry: ${entries[i].type}'); + if (entries[i].type == 'folder') { + if (!categories.contains(entries[i].id)) { + // if the folder is not in the list of categories then add it to the list of folders + try { + // add the folder to the list of folders + newFolders.add({ + 'id': entries[i].id, + 'name': entries[i].title, + 'image': entries[i].imagePath, + 'bookIds': [], + }); + // newFolders.add(Folder( + // id: entries[i].id, + // name: entries[i].title, + // image: entries[i].imagePath, + // bookIds: [], + // )); + newFolderIds.add(entries[i].id); + } catch (e) { + debugPrint("error creating folder: $e"); + } + } + } + } + + // now add the entries to the folders + for (int i = 0; i < entries.length; i++) { + if (entries[i].type == 'book' || entries[i].type == 'comic') { + if (newFolderIds.contains(entries[i].parentId)) { + int index = newFolderIds.indexOf(entries[i].parentId); + newFolders[index]['bookIds'].add(entries[i].id); + } + } + } + + // now we debugPrint the new folders + debugPrint("newFolders: ${newFolders.length}"); + for (int i = 0; i < newFolders.length; i++) { + debugPrint("newFolder: ${newFolders[i]['name']}"); + debugPrint("newFolder Id: ${newFolders[i]['id']}"); + debugPrint("newFolder: ${newFolders[i]['bookIds']}"); + } + + // now we have a list of folders, we need to add them to the hive box + for (int i = 0; i < newFolders.length; i++) { + // wait until everything is done before adding the folders to the hive box + await box.add(Folder( + id: newFolders[i]['id'], + name: newFolders[i]['name'], + image: newFolders[i]['image'], + bookIds: newFolders[i]['bookIds'].cast(), + )); + } + } + + // fetch the list of folders from the hive box + static Future> getFolders( + List entries, List categories) async { + // if (box.isNotEmpty) { + // var folders = box.values.toList(); + // return folders; + // } else { + var folders = await CreateFolders.createFolders(entries, categories); + // if folders is finished then return the list of folders + var foldersBox = Hive.box('folders'); + var foldersList = foldersBox.values.toList(); + debugPrint("foldersList: ${foldersList.length}"); + return foldersList; + } +} diff --git a/lib/providers/login.dart b/lib/providers/login.dart index a0a17a98..2454525d 100644 --- a/lib/providers/login.dart +++ b/lib/providers/login.dart @@ -37,7 +37,7 @@ class Login { const _client = "JellyBook"; const _device = "Unknown Device"; const _deviceId = "Unknown Device id"; - const _version = "1.0.6"; + const _version = "1.0.7"; if ((!url.contains("http://") || !url.contains("https://")) == false) { debugPrint("URL does not contain http:// or https://"); diff --git a/lib/screens/collectionScreen.dart b/lib/screens/collectionScreen.dart new file mode 100644 index 00000000..110e42c2 --- /dev/null +++ b/lib/screens/collectionScreen.dart @@ -0,0 +1,294 @@ +// The purpose of this file is to create a list of entries from a selected folder + +// import 'package:shared_preferences/shared_preferences.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:jellybook/models/folder.dart'; +import 'package:jellybook/models/entry.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_star/flutter_star.dart'; +import 'package:jellybook/screens/infoScreen.dart'; + +class collectionScreen extends StatelessWidget { + final String folderId; + final String name; + final String image; + final List bookIds; + + collectionScreen({ + required this.folderId, + required this.name, + required this.image, + required this.bookIds, + }); + + // make a list of entries from the the list of bookIds + Future>> getEntries() async { + var box = Hive.box('bookShelf'); + List> entries = []; + var entryList = box.values.toList(); + debugPrint("bookIds: ${bookIds.length}"); + for (int i = 0; i < bookIds.length; i++) { + // get the first entry that matches the bookId + var entry = entryList.firstWhere((element) => element.id == bookIds[i]); + entries.add({ + 'id': entry.id, + 'title': entry.title, + 'imagePath': entry.imagePath != '' + ? entry.imagePath + : 'https://via.placeholder.com/200x316?text=No+Image', + 'rating': entry.rating, + 'description': entry.description, + 'path': entry.path, + 'year': entry.releaseDate, + 'type': entry.type, + 'tags': entry.tags, + 'url': entry.url, + }); + debugPrint("entry: ${entry.id}"); + debugPrint("entry: ${entry.title}"); + debugPrint("entry: ${entry.imagePath}"); + debugPrint("entry: ${entry.rating}"); + debugPrint("entry: ${entry.type}"); + debugPrint("entry: ${entry.description}"); + debugPrint("entry: ${entry.path}"); + debugPrint("entry: ${entry.tags}"); + debugPrint("entry: ${entry.url}"); + } + return entries; + // checkeach field of the entry to make sure it is not null + } + + // getter for the list of entries calling getEntries() + Future>> get entries async { + return await getEntries(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(name), + ), + body: FutureBuilder( + future: entries, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasData && + snapshot.data.length > 0 && + snapshot.connectionState == ConnectionState.done) { + return ListView.builder( + itemCount: snapshot.data.length, + itemBuilder: (BuildContext context, int index) { + return ListTile( + onTap: () { + if (snapshot.data[index]['type'] == 'book' || + snapshot.data[index]['type'] == 'comic') { + Navigator.push( + context, + PageRouteBuilder( + pageBuilder: + (context, animation, secondaryAnimation) => + InfoScreen( + comicId: snapshot.data[index]['id'], + title: snapshot.data[index]['title'], + imageUrl: snapshot.data[index]['imagePath'], + stars: snapshot.data[index]['rating'], + description: snapshot.data[index]['description'], + path: snapshot.data[index]['type'], + year: snapshot.data[index]['year'], + url: snapshot.data[index]['url'], + tags: snapshot.data[index]['tags'], + ), + transitionsBuilder: + (context, animation, secondaryAnimation, child) { + var begin = Offset(1.0, 0.0); + var end = Offset.zero; + var curve = Curves.ease; + + var tween = Tween(begin: begin, end: end) + .chain(CurveTween(curve: curve)); + + return SlideTransition( + position: animation.drive(tween), + child: child, + ); + }, + ), + ); + } else if (snapshot.data[index]['type'] == 'folder') { + Navigator.push( + context, + PageRouteBuilder( + pageBuilder: + (context, animation, secondaryAnimation) => + collectionScreen( + folderId: snapshot.data[index]['id'], + name: snapshot.data[index]['title'], + image: snapshot.data[index]['imagePath'], + bookIds: snapshot.data[index]['bookIds'], + ), + transitionsBuilder: + (context, animation, secondaryAnimation, child) { + var begin = Offset(1.0, 0.0); + var end = Offset.zero; + var curve = Curves.ease; + + var tween = Tween(begin: begin, end: end) + .chain(CurveTween(curve: curve)); + + return SlideTransition( + position: animation.drive(tween), + child: child, + ); + }, + ), + ); + } + }, + title: Text(snapshot.data[index]['title']), + leading: Image.network(snapshot.data[index]['imagePath']), + // have the subitle be the rating + subtitle: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + if (snapshot.data[index]['rating'] >= 0) + // allign the stars to the very left of the row + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + if (snapshot.data[index]['rating'] >= 0) + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Padding( + padding: + const EdgeInsets.fromLTRB(0, 0, 0, 0), + child: CustomRating( + max: 5, + score: snapshot.data[index]['rating'] / 2, + star: Star( + fillColor: Color.lerp( + Colors.red, + Colors.yellow, + snapshot.data[index]['rating'] / + 10)!, + emptyColor: + Colors.grey.withOpacity(0.5), + ), + onRating: (double score) {}, + ), + ), + Padding( + padding: + const EdgeInsets.fromLTRB(8, 0, 0, 0), + child: Text( + "${(snapshot.data[index]['rating'] / 2).toStringAsFixed(2)} / 5.00", + style: const TextStyle( + fontStyle: FontStyle.italic, + fontSize: 15, + ), + ), + ), + // add a padding for 80% of the screen width + Padding( + padding: EdgeInsets.fromLTRB( + 0, + 0, + MediaQuery.of(context).size.width * + 0.29, + 0), + ), + ], + ), + ], + ), + // if the rating is less than 0 and the description is not empty + // then display the first 50 characters of the description + if (snapshot.data[index]['rating'] < 0 && + snapshot.data[index]['description'] != '') + Text( + snapshot.data[index]['description'].length > 50 + ? snapshot.data[index]['description'] + .substring(0, 42) + + "..." + : snapshot.data[index]['description'], + style: const TextStyle( + fontStyle: FontStyle.italic, + fontSize: 15, + ), + ), + ], + ), + ); + + // subtitle: snapshot.data[index]['rating'] != null + // ? Row( + // mainAxisSize: MainAxisSize.min, + // children: [ + // for (int i = 0; + // i < snapshot.data[index]['rating']; + // i++) + // Icon( + // Icons.star, + // color: Colors.yellow, + // ), + // ], + // ) + // : null, + }, + ); + } else { + return Center( + child: CircularProgressIndicator(), + ); + } + }, + ), + ); + // return FutureBuilder( + // // wait until the list of entries is created before building the screen + // future: getEntries(), + // builder: (context, snapshot) { + // if (snapshot.hasData) { + // debugPrint("snapshot: ${snapshot.data}"); + // return Scaffold( + // appBar: AppBar( + // title: Text(name), + // backgroundColor: Colors.blueGrey[900], + // ), + // body: Container( + // child: ListView.builder( + // itemCount: snapshot.data!.length, + // itemBuilder: (context, index) { + // // have the image, title, and rating + // return Container( + // child: ListTile( + // leading: Image.network( + // snapshot.data![index]['imagePath'], + // width: 100, + // height: 100, + // fit: BoxFit.cover, + // ), + // title: Text(snapshot.data![index]['title']), + // ), + // ); + // }, + // ), + // ), + // ); + // } else { + // return Scaffold( + // appBar: AppBar( + // title: Text('Loading'), + // backgroundColor: Colors.blueGrey[900], + // ), + // body: Container( + // child: Center( + // child: CircularProgressIndicator(), + // ), + // ), + // ); + // } + // }, + // ); + } +} diff --git a/lib/screens/downloaderScreen.dart b/lib/screens/downloaderScreen.dart index c81007cd..a245c46c 100644 --- a/lib/screens/downloaderScreen.dart +++ b/lib/screens/downloaderScreen.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'dart:async'; -import 'package:file_utils/file_utils.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; @@ -106,7 +105,10 @@ class _DownloadScreenState extends State { debugPrint(dir); debugPrint('Folder does not exist'); try { - FileUtils.mkdir([dirLocation]); + // make directory + await Directory(dirLocation).create(recursive: true); + debugPrint('Directory created'); + // FileUtils.mkdir([dirLocation]); // set the location of the folder dir = dirLocation + '/' + fileName; // if (entry.path.contains('cbz')) { @@ -135,11 +137,23 @@ class _DownloadScreenState extends State { } String fileName2 = await fileNameFromTitle(entry.title); - FileUtils.mkdir([dirLocation + '/']); + // FileUtils.mkdir([dirLocation + '/']); + // make directory + try { + await Directory(dirLocation).create(recursive: true); + } catch (e) { + debugPrint(e.toString()); + } debugPrint('Comic folder created'); debugPrint(dirLocation + '/' + fileName2); - FileUtils.mkdir([dirLocation + '/' + fileName2]); + // FileUtils.mkdir([dirLocation + '/' + fileName2]); + // make directory + try { + await Directory(dirLocation + '/' + fileName2).create(recursive: true); + } catch (e) { + debugPrint(e.toString()); + } comicFolder = dirLocation + '/' + fileName2; if (dir.contains('.zip')) { var bytes = File(dirLocation + '/' + fileName).readAsBytesSync(); @@ -154,7 +168,14 @@ class _DownloadScreenState extends State { ..createSync(recursive: true) ..writeAsBytesSync(data); } else { - FileUtils.mkdir([dirLocation + '/' + fileName2 + '/' + filename]); + // FileUtils.mkdir([dirLocation + '/' + fileName2 + '/' + filename]); + // make directory + try { + await Directory(dirLocation + '/' + fileName2 + '/' + filename) + .create(recursive: true); + } catch (e) { + debugPrint(e.toString()); + } } } debugPrint('Unzipped'); @@ -182,7 +203,13 @@ class _DownloadScreenState extends State { try { var file = File(dirLocation + '/' + fileName); // var file = File(dirLocation + '/' + fileName + '.pdf'); - FileUtils.mkdir([dirLocation + '/' + fileName2]); + // FileUtils.mkdir([dirLocation + '/' + fileName2]); + // make directory + try { + await Directory('$dirLocation/$fileName2').create(recursive: true); + } catch (e) { + debugPrint(e.toString()); + } file.renameSync(dirLocation + '/' + fileName2 + '/' + fileName); entry.folderPath = dirLocation + '/' + fileName2; entry.filePath = dirLocation + '/' + fileName2 + '/' + fileName; diff --git a/lib/screens/homeScreen.dart b/lib/screens/homeScreen.dart index 9d162ec0..a81abe3c 100644 --- a/lib/screens/homeScreen.dart +++ b/lib/screens/homeScreen.dart @@ -9,6 +9,9 @@ import 'package:jellybook/providers/fetchBooks.dart'; import 'package:jellybook/screens/settingsScreen.dart'; import 'package:jellybook/providers/fetchCategories.dart'; import 'package:jellybook/screens/databaseViewer.dart'; +import 'package:jellybook/providers/folderProvider.dart'; +import 'package:jellybook/models/folder.dart'; +import 'package:jellybook/screens/collectionScreen.dart'; class HomeScreen extends StatefulWidget { @override @@ -85,134 +88,472 @@ class _HomeScreenState extends State { // ), // ], ), - body: FutureBuilder( - future: getServerCategories(context), - builder: (context, snapshot) { - if (snapshot.hasData) { - return GridView.builder( - itemCount: snapshot.data?.length, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2), - itemBuilder: (context, index) { - return Card( - child: InkWell( - onTap: () { - Navigator.push( - context, - PageRouteBuilder( - pageBuilder: - (context, animation, secondaryAnimation) => - InfoScreen( - title: snapshot.data![index]['name'] ?? "null", - imageUrl: - (snapshot.data![index]['imagePath'] ?? "null"), - description: - snapshot.data![index]['description'] ?? "null", - tags: snapshot.data![index]['tags'] ?? ["null"], - url: snapshot.data![index]['url'] ?? "null", - year: - snapshot.data![index]['releaseDate'] ?? "null", - stars: snapshot.data![index]['rating'] ?? -1, - path: snapshot.data![index]['path'] ?? "null", - comicId: snapshot.data![index]['id'] ?? "null", + body: Column( + children: [ + const SizedBox( + height: 10, + ), + // text align left side of the column + const Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: EdgeInsets.only(left: 10), + child: Text( + "Collections", + style: TextStyle( + // size is the size of a title + fontSize: 30, + // decoration: TextDecoration.underline, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + FutureBuilder( + future: getServerCategories(context, returnFolders: true), + builder: (context, AsyncSnapshot snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + debugPrint("snapshot data: ${snapshot.data}"); + return Container( + width: double.infinity, + height: MediaQuery.of(context).size.height / 6 * 1.2, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: snapshot.data?.length, + itemBuilder: (context, index) { + return Container( + width: MediaQuery.of(context).size.width / 3, + child: Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), ), - transitionsBuilder: - (context, animation, secondaryAnimation, child) { - var begin = Offset(1.0, 0.0); - var end = Offset.zero; - var curve = Curves.ease; + child: InkWell( + onTap: () { + debugPrint("tapped"); + debugPrint( + "snapshot data: ${snapshot.data[index]['id']}"); + debugPrint( + "snapshot data: ${snapshot.data[index]['name']}"); + debugPrint( + "snapshot data: ${snapshot.data[index]['image']}"); + debugPrint( + "snapshot data: ${snapshot.data[index]['bookIds']}"); + Navigator.push( + context, + PageRouteBuilder( + pageBuilder: (context, animation, + secondaryAnimation) => + collectionScreen( + folderId: snapshot.data[index]['id'], + name: snapshot.data[index]['name'], + image: snapshot.data[index]['image'], + bookIds: snapshot.data[index]['bookIds'], + ), + transitionsBuilder: (context, animation, + secondaryAnimation, child) { + var begin = Offset(1.0, 0.0); + var end = Offset.zero; + var curve = Curves.ease; - var tween = Tween(begin: begin, end: end) - .chain(CurveTween(curve: curve)); + var tween = Tween(begin: begin, end: end) + .chain(CurveTween(curve: curve)); - return SlideTransition( - position: animation.drive(tween), - child: child, - ); - }, + return SlideTransition( + position: animation.drive(tween), + child: child, + ); + }, + ), + ); + }, + child: Column( + children: [ + SizedBox( + height: 10 * + MediaQuery.of(context).size.height / + 1000, + ), + Padding( + padding: EdgeInsets.only( + left: 10 * + MediaQuery.of(context).size.width / + 1000, + right: 10 * + MediaQuery.of(context).size.width / + 1000), + child: FittedBox( + fit: BoxFit.fitWidth, + child: Text( + snapshot.data![index]['name'], + textAlign: TextAlign.center, + style: TextStyle( + fontSize: + MediaQuery.of(context).size.width / + 4 * + 0.13, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + // start all images at the same height rather than same offset + SizedBox( + height: 5 * + MediaQuery.of(context).size.height / + 1000, + ), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.4), + spreadRadius: 5, + blurRadius: 7, + offset: Offset(0, 3), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + // add a shadow to the image + child: Image.network( + snapshot.data![index]['image'], + height: + MediaQuery.of(context).size.height / + 6 * + 0.8, + fit: BoxFit.fitWidth, + ), + ), + ), + const SizedBox( + height: 10, + ), + ], + ), + ), ), ); }, - child: Column( - children: [ - SizedBox( - height: 10, - ), - Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.4), - spreadRadius: 5, - blurRadius: 7, - offset: Offset(0, 3), + ), + ); + } else { + return Container( + height: 100, + child: Center( + child: CircularProgressIndicator(), + ), + ); + } + }, + ), + const SizedBox( + height: 10, + ), + // page break + const Divider( + height: 5, + thickness: 5, + indent: 0, + endIndent: 0, + ), + const SizedBox( + height: 10, + ), + // text align left side of the column + const Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: EdgeInsets.only(left: 10), + child: Text( + "Library", + style: TextStyle( + // size is the size of a title + fontSize: 30, + // decoration: TextDecoration.underline, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + const SizedBox( + height: 10, + ), + // vertical list of books + FutureBuilder( + future: getServerCategories(context, returnFolders: false), + builder: (context, AsyncSnapshot snapshot) { + if (snapshot.hasData) { + debugPrint("snapshot data: ${snapshot.data}"); + return Expanded( + // use grid view to make the list of books + child: GridView.builder( + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + ), + itemBuilder: (context, index) { + return Card( + child: InkWell( + onTap: () { + Navigator.push( + context, + PageRouteBuilder( + pageBuilder: + (context, animation, secondaryAnimation) => + InfoScreen( + title: + snapshot.data![index]['name'] ?? "null", + imageUrl: (snapshot.data![index] + ['imagePath'] ?? + "null"), + description: snapshot.data![index] + ['description'] ?? + "null", + tags: + snapshot.data![index]['tags'] ?? ["null"], + url: snapshot.data![index]['url'] ?? "null", + year: snapshot.data![index]['releaseDate'] ?? + "null", + stars: snapshot.data![index]['rating'] ?? -1, + path: snapshot.data![index]['path'] ?? "null", + comicId: + snapshot.data![index]['id'] ?? "null", + ), + transitionsBuilder: (context, animation, + secondaryAnimation, child) { + var begin = Offset(1.0, 0.0); + var end = Offset.zero; + var curve = Curves.ease; + + var tween = Tween(begin: begin, end: end) + .chain(CurveTween(curve: curve)); + + return SlideTransition( + position: animation.drive(tween), + child: child, + ); + }, ), + ); + }, + child: Column( + children: [ + SizedBox( + height: 10, + ), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.4), + spreadRadius: 5, + blurRadius: 7, + offset: Offset(0, 3), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + // add a shadow to the image + child: Image.network( + snapshot.data![index]['imagePath'], + height: MediaQuery.of(context).size.height / + 6 * + 0.8, + fit: BoxFit.fitWidth, + ), + ), + ), + const SizedBox( + height: 10, + ), + // auto size text to fit the width of the card (max 2 lines) + Flexible( + child: Text(snapshot.data![index]['name'], + textAlign: TextAlign.center, + style: TextStyle( + // auto size the text + fontSize: + MediaQuery.of(context).size.width / + 4 * + 0.13, + fontWeight: FontWeight.bold, + )), + ), + const SizedBox( + height: 5, + ), + if (snapshot.data![index]['releaseDate'] != + "null") + Text(snapshot.data![index]['releaseDate'], + style: TextStyle( + fontSize: + MediaQuery.of(context).size.width / + 4 * + 0.12, + // set color to a very light grey + color: Colors.grey)), + // check if screen is > 600 + if (MediaQuery.of(context).size.width > 600) + Flexible( + child: SingleChildScrollView( + child: Text( + snapshot.data![index]['description'], + textAlign: TextAlign.center, + style: TextStyle( + fontSize: MediaQuery.of(context) + .size + .width / + 2 * + 0.03, + )), + ), + ), + // add a icon button to the card ], ), - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - // add a shadow to the image - child: Image.network( - snapshot.data![index]['imagePath'], - width: - MediaQuery.of(context).size.width / 4 * 0.8, - fit: BoxFit.fitWidth, - ), - ), - ), - const SizedBox( - height: 10, - ), - Flexible( - child: Text(snapshot.data![index]['name'], - textAlign: TextAlign.center, - style: TextStyle( - fontSize: MediaQuery.of(context).size.width / - 4 * - 0.12, - fontWeight: FontWeight.bold, - )), - ), - const SizedBox( - height: 5, ), - if (snapshot.data![index]['releaseDate'] != "null") - Text(snapshot.data![index]['releaseDate'], - style: TextStyle( - fontSize: MediaQuery.of(context).size.width / - 4 * - 0.12, - // set color to a very light grey - color: Colors.grey)), - // check if screen is > 600 - if (MediaQuery.of(context).size.width > 600) - Flexible( - child: SingleChildScrollView( - child: Text(snapshot.data![index]['description'], - textAlign: TextAlign.center, - style: TextStyle( - fontSize: - MediaQuery.of(context).size.width / - 2 * - 0.03, - )), - ), - ), - // add a icon button to the card - ], - ), + ); + }, + itemCount: snapshot.data!.length, + ), + ); + } else { + return Container( + height: 100, + child: Center( + child: CircularProgressIndicator(), ), ); - }, - ); - // fixed version - } else { - return const Center( - child: CircularProgressIndicator(), - ); - } - }, + } + }, + ), + const SizedBox( + height: 10, + ), + + // FutureBuilder( + // future: getServerCategories(context), + // builder: (context, snapshot) { + // if (snapshot.hasData) { + // return GridView.builder( + // itemCount: snapshot.data?.length, + // gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + // crossAxisCount: 2), + // itemBuilder: (context, index) { + // return Card( + // child: InkWell( + // onTap: () { + // Navigator.push( + // context, + // PageRouteBuilder( + // pageBuilder: + // (context, animation, secondaryAnimation) => + // InfoScreen( + // title: snapshot.data![index]['name'] ?? "null", + // imageUrl: (snapshot.data![index]['imagePath'] ?? + // "null"), + // description: snapshot.data![index] + // ['description'] ?? + // "null", + // tags: snapshot.data![index]['tags'] ?? ["null"], + // url: snapshot.data![index]['url'] ?? "null", + // year: snapshot.data![index]['releaseDate'] ?? + // "null", + // stars: snapshot.data![index]['rating'] ?? -1, + // path: snapshot.data![index]['path'] ?? "null", + // comicId: snapshot.data![index]['id'] ?? "null", + // ), + // transitionsBuilder: (context, animation, + // secondaryAnimation, child) { + // var begin = Offset(1.0, 0.0); + // var end = Offset.zero; + // var curve = Curves.ease; + // + // var tween = Tween(begin: begin, end: end) + // .chain(CurveTween(curve: curve)); + // + // return SlideTransition( + // position: animation.drive(tween), + // child: child, + // ); + // }, + // ), + // ); + // }, + // child: Column( + // children: [ + // SizedBox( + // height: 10, + // ), + // Container( + // decoration: BoxDecoration( + // borderRadius: BorderRadius.circular(8), + // boxShadow: [ + // BoxShadow( + // color: Colors.black.withOpacity(0.4), + // spreadRadius: 5, + // blurRadius: 7, + // offset: Offset(0, 3), + // ), + // ], + // ), + // child: ClipRRect( + // borderRadius: BorderRadius.circular(8), + // // add a shadow to the image + // child: Image.network( + // snapshot.data![index]['imagePath'], + // width: MediaQuery.of(context).size.width / + // 4 * + // 0.8, + // fit: BoxFit.fitWidth, + // ), + // ), + // ), + // const SizedBox( + // height: 10, + // ), + // Flexible( + // child: Text(snapshot.data![index]['name'], + // textAlign: TextAlign.center, + // style: TextStyle( + // fontSize: + // MediaQuery.of(context).size.width / + // 4 * + // 0.12, + // fontWeight: FontWeight.bold, + // )), + // ), + // const SizedBox( + // height: 5, + // ), + // if (snapshot.data![index]['releaseDate'] != "null") + // Text(snapshot.data![index]['releaseDate'], + // style: TextStyle( + // fontSize: + // MediaQuery.of(context).size.width / + // 4 * + // 0.12, + // // set color to a very light grey + // color: Colors.grey)), + // ], + // ), + // ), + // ); + // }, + // ); + // // fixed version + // } else { + // return const Center( + // child: CircularProgressIndicator(), + // ); + // } + // }, + // ), + ], ), ); } diff --git a/lib/screens/settingsScreen.dart b/lib/screens/settingsScreen.dart index f6b6d7fb..6c561d09 100644 --- a/lib/screens/settingsScreen.dart +++ b/lib/screens/settingsScreen.dart @@ -136,7 +136,7 @@ class _SettingsScreenState extends State { child: Column( children: [ Text( - 'Version 1.0.6', + 'Version 1.0.7', style: TextStyle(fontSize: 20), ), SizedBox( diff --git a/pubspec.lock b/pubspec.lock index 97a7f7bc..4a2f39df 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -21,7 +21,7 @@ packages: name: archive url: "https://pub.dartlang.org" source: hosted - version: "3.3.2" + version: "3.3.4" args: dependency: transitive description: @@ -70,7 +70,7 @@ packages: name: build_resolvers url: "https://pub.dartlang.org" source: hosted - version: "2.0.10" + version: "2.1.0" build_runner: dependency: "direct dev" description: @@ -204,13 +204,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "6.1.4" - file_utils: - dependency: "direct main" - description: - name: file_utils - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.1" fixnum: dependency: transitive description: @@ -229,7 +222,7 @@ packages: name: flutter_launcher_icons url: "https://pub.dartlang.org" source: hosted - version: "0.10.0" + version: "0.11.0" flutter_lints: dependency: "direct dev" description: @@ -323,14 +316,7 @@ packages: name: glob url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" - globbing: - dependency: transitive - description: - name: globbing - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" + version: "2.1.1" graphs: dependency: transitive description: @@ -386,7 +372,7 @@ packages: name: image url: "https://pub.dartlang.org" source: hosted - version: "3.1.3" + version: "3.2.2" io: dependency: transitive description: @@ -597,6 +583,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.3" + pointycastle: + dependency: transitive + description: + name: pointycastle + url: "https://pub.dartlang.org" + source: hosted + version: "3.6.2" pool: dependency: transitive description: @@ -624,7 +617,7 @@ packages: name: pub_semver url: "https://pub.dartlang.org" source: hosted - version: "2.1.2" + version: "2.1.3" pubspec_parse: dependency: transitive description: @@ -895,7 +888,7 @@ packages: name: xml url: "https://pub.dartlang.org" source: hosted - version: "5.4.1" + version: "6.1.0" yaml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 00e4cf52..96103322 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.6 +version: 1.0.7 environment: sdk: '>=2.18.2 <3.0.0' @@ -46,7 +46,6 @@ dependencies: like_button: ^2.0.5 path_provider: ^2.0.11 permission_handler: ^10.2.0 - file_utils: ^1.0.1 turn_page_transition: ^0.2.0 unrar_file: ^1.1.0 flutter_settings_screens: ^0.3.3-null-safety+2 @@ -68,7 +67,7 @@ dev_dependencies: # package. See that file for information about deactivating specific lint # rules and activating additional ones. flutter_lints: ^2.0.0 - flutter_launcher_icons: ^0.10.0 + flutter_launcher_icons: ^0.11.0 hive_generator: ^2.0.0 build_runner: ^2.3.2 # objectbox_generator: ^1.6.2