diff --git a/android/app/build.gradle b/android/app/build.gradle
index 77804881..c802ff9d 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -58,6 +58,7 @@ android {
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
+ manifestPlaceholders['foregroundServiceType'] = 'mediaPlayback';
}
signingConfigs {
release {
diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml
index 783a9750..c6445662 100644
--- a/android/app/src/debug/AndroidManifest.xml
+++ b/android/app/src/debug/AndroidManifest.xml
@@ -9,6 +9,8 @@
+
+
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index aeb2ad1e..d22329bb 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -5,6 +5,8 @@
+
+
diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml
index b74f31e4..7d91f297 100644
--- a/android/app/src/profile/AndroidManifest.xml
+++ b/android/app/src/profile/AndroidManifest.xml
@@ -7,6 +7,8 @@
+
+
diff --git a/ios/Runner/Info-Debug.plist b/ios/Runner/Info-Debug.plist
index 4a32f05d..076d039c 100755
--- a/ios/Runner/Info-Debug.plist
+++ b/ios/Runner/Info-Debug.plist
@@ -17,26 +17,26 @@
CFBundleLocalizations
en
- ar
+ ar
ca
- cs
+ cs
da
- de
- en_pirate
- es
- es_US
- eu
- fr
- he
- hr
- hu
- id
- it
- ja
- kk
- ko
- lb
- nb
+ de
+ en_pirate
+ es
+ es_US
+ eu
+ fr
+ he
+ hr
+ hu
+ id
+ it
+ ja
+ kk
+ ko
+ lb
+ nb
nl
nn
nl
@@ -96,6 +96,8 @@
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
+ NSPhotoLibraryUsageDescription
+ Allow user to upload images to their server or local device to change ebook covers
UIViewControllerBasedStatusBarAppearance
diff --git a/ios/Runner/Info-Release.plist b/ios/Runner/Info-Release.plist
index 62c7b62a..7d4c527c 100755
--- a/ios/Runner/Info-Release.plist
+++ b/ios/Runner/Info-Release.plist
@@ -1,61 +1,63 @@
-
- ITSAppUsesNonExemptEncryption
-
- CADisableMinimumFrameDurationOnPhone
-
- CFBundleDevelopmentRegion
- $(DEVELOPMENT_LANGUAGE)
- CFBundleDisplayName
- Jellybook
- CFBundleExecutable
- $(EXECUTABLE_NAME)
- CFBundleIdentifier
- $(PRODUCT_BUNDLE_IDENTIFIER)
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- jellybook
- CFBundlePackageType
- APPL
- CFBundleShortVersionString
- $(FLUTTER_BUILD_NAME)
- CFBundleSignature
- ????
- CFBundleVersion
- $(FLUTTER_BUILD_NUMBER)
- LSRequiresIPhoneOS
-
- UIApplicationSupportsIndirectInputEvents
-
- LSSupportsOpeningDocumentsInPlace
-
- UIFileSharingEnabled
-
- UILaunchStoryboardName
- LaunchScreen
- UIMainStoryboardFile
- Main
- UISupportedInterfaceOrientations
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- UISupportedInterfaceOrientations~ipad
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- UIViewControllerBasedStatusBarAppearance
-
- BGTaskSchedulerPermittedIdentifiers
-
- dev.flutter.background.refresh
-
-
+
+ ITSAppUsesNonExemptEncryption
+
+ CADisableMinimumFrameDurationOnPhone
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ Jellybook
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ jellybook
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ LSRequiresIPhoneOS
+
+ UIApplicationSupportsIndirectInputEvents
+
+ LSSupportsOpeningDocumentsInPlace
+
+ UIFileSharingEnabled
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UIViewControllerBasedStatusBarAppearance
+
+ NSPhotoLibraryUsageDescription
+ Allow user to upload images to their server or local device to change ebook covers
+ BGTaskSchedulerPermittedIdentifiers
+
+ dev.flutter.background.refresh
+
+
diff --git a/lib/providers/Author.dart b/lib/providers/Author.dart
new file mode 100644
index 00000000..c6573547
--- /dev/null
+++ b/lib/providers/Author.dart
@@ -0,0 +1,14 @@
+class Author {
+ final String name;
+ String? link;
+ List roles = [];
+
+ Author({
+ required this.name,
+ this.link,
+ });
+
+ void addRole(String role) {
+ roles.add(role);
+ }
+}
diff --git a/lib/providers/fixRichText.dart b/lib/providers/fixRichText.dart
index 95408f85..e743d4a9 100644
--- a/lib/providers/fixRichText.dart
+++ b/lib/providers/fixRichText.dart
@@ -10,7 +10,7 @@ String fixRichText(String text) {
.replaceAll("", "*")
.replaceAll("", "**")
.replaceAll("", "**")
- .replaceAll("", "\t")
+ .replaceAll("
", "\n\n")
.replaceAll("
", "")
.replaceAll("", "__")
.replaceAll("", "__")
diff --git a/lib/screens/EditScreen.dart b/lib/screens/EditScreen.dart
new file mode 100644
index 00000000..3a82d4d0
--- /dev/null
+++ b/lib/screens/EditScreen.dart
@@ -0,0 +1,884 @@
+// the purpose of this file is to allow you to edit a entry in your library
+
+import 'package:auto_size_text/auto_size_text.dart';
+import 'package:dio/dio.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_star/flutter_star.dart';
+import 'package:intl/intl.dart';
+import 'package:jellybook/providers/fixRichText.dart';
+import 'package:jellybook/widgets/ToggleEditPreviewButton.dart';
+import 'package:jellybook/widgets/roundedImageWithShadow.dart';
+import 'package:jellybook/providers/updateLike.dart';
+import 'package:isar/isar.dart';
+import 'package:flutter_markdown/flutter_markdown.dart';
+import 'package:markdown/markdown.dart' as md;
+import 'package:jellybook/models/entry.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:jellybook/variables.dart';
+import 'package:package_info_plus/package_info_plus.dart' as p_info;
+import 'package:openapi/openapi.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+import 'package:image_picker/image_picker.dart';
+import 'package:built_collection/built_collection.dart';
+import 'package:flutter/cupertino.dart';
+import 'dart:io';
+
+class EditScreen extends StatefulWidget {
+ bool offline;
+ Entry entry;
+
+ EditScreen({
+ super.key,
+ required this.entry,
+ this.offline = false,
+ });
+
+ @override
+ _EditScreenState createState() => _EditScreenState(
+ entry: entry,
+ offline: offline,
+ );
+}
+
+class _EditScreenState extends State {
+ bool editMode = true;
+ bool offline;
+ bool changed = false;
+ Entry entry;
+ _EditScreenState({
+ required this.entry,
+ this.offline = false,
+ });
+ double imageWidth = 0;
+ bool updatedImageWidth = false;
+ bool isEditingDescription = false;
+ final picker = ImagePicker();
+ TextEditingController _descriptionController = TextEditingController();
+ final TextEditingController _tagController = TextEditingController();
+ List tags = [];
+ TextEditingController _titleController = TextEditingController();
+ final FocusNode _titleFocusNode = FocusNode();
+ TextEditingController _releaseDateController = TextEditingController();
+
+// check if it is liked or not by checking the database
+ Future checkLiked(String id) async {
+ final isar = Isar.getInstance();
+ final entries = await isar?.entrys.where().idEqualTo(id).findFirst();
+
+ return entries?.isFavorited ?? false;
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ checkLiked(entry.id).then((value) {
+ entry.isFavorited = value;
+ });
+ tags = List.from(entry.tags);
+ _titleController = TextEditingController(text: entry.title);
+ _releaseDateController = TextEditingController(text: entry.releaseDate);
+ _descriptionController = TextEditingController(text: entry.description);
+ logger.i("isarId: ${entry.isarId}");
+ logger.i("rating: ${entry.rating}");
+ if (entry.rating < 0) {
+ entry.rating = 0;
+ }
+ }
+
+ @override
+ void dispose() {
+ super.dispose();
+ }
+
+ void handleImageSize(Size imageSize) {
+ // Do something with the image size obtained from the callback
+ logger.f('Image size: ${imageSize.width} x ${imageSize.height}');
+ // wait until build is done then set state but do it only once
+ if (!updatedImageWidth) {
+ setState(() {
+ imageWidth = imageSize.width;
+ updatedImageWidth = true;
+ });
+ }
+ }
+
+ // pick an image from the gallery
+ Future _pickImage() async {
+ final pickedFile = await picker.pickImage(source: ImageSource.gallery);
+ if (pickedFile != null) {
+ entry.imagePath = pickedFile.path;
+ setState(() {});
+ }
+ }
+
+ bool isTablet(BuildContext context) {
+ final shortestSide = MediaQuery.of(context).size.shortestSide;
+
+ return shortestSide > 600;
+ }
+
+ Future saveToDevice() async {
+ final isar = Isar.getInstance();
+ entry.tags = tags;
+ entry.description = _descriptionController.text;
+
+ await isar?.writeTxn(() async {
+ await isar.entrys.put(entry);
+ });
+ }
+
+ Future saveToServer() async {
+ final isar = Isar.getInstance();
+ bool imageChanged = false;
+ final entry2 =
+ await isar!.entrys.where().idEqualTo(this.entry.id).findFirst();
+ if (entry2!.imagePath != entry.imagePath) {
+ imageChanged = true;
+ }
+ entry.tags = tags;
+ entry.description = _descriptionController.text;
+
+ await saveToDevice();
+
+ final prefs = await SharedPreferences.getInstance();
+ p_info.PackageInfo packageInfo = await p_info.PackageInfo.fromPlatform();
+ final server = prefs.getString('server')!;
+ final token = prefs.getString('accessToken')!;
+ final version = packageInfo.version;
+ const _client = "JellyBook";
+ const _device = "Unknown Device";
+ const _deviceId = "Unknown Device id";
+
+ final url = server + '/Items/' + entry.id;
+ var headers = {
+ 'Accept': 'application/json',
+ 'Accept-Language': 'en-US,en;q=0.5',
+ 'Accept-Encoding': 'gzip, deflate',
+ 'Content-Type': 'application/json',
+ "X-Emby-Authorization":
+ "MediaBrowser Client=\"$_client\", Device=\"$_device\", DeviceId=\"$_deviceId\", Version=\"$version\", Token=\"$token\"",
+ 'Connection': 'keep-alive',
+ 'Origin': server,
+ 'Host': server.substring(server.indexOf("//") + 2, server.length),
+ };
+ final api = Openapi(basePathOverride: server).getItemUpdateApi();
+ // body of the request
+ try {
+ final response = await api.updateItem(
+ itemId: entry.id,
+ updateItemRequest: UpdateItemRequest(
+ (b) => {
+ b.name = entry.title,
+ b.overview = entry.description,
+ b.premiereDate = DateTime.parse(entry.releaseDate),
+ b.tags = ListBuilder(tags),
+ },
+ ),
+ headers: headers,
+ );
+ // logger.d(response.data.toString());
+ } catch (e) {
+ logger.e(e.toString());
+ // display error message
+ await showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return AlertDialog(
+ title: Text(
+ AppLocalizations.of(context)?.error ?? "Error",
+ ),
+ content: Text(
+ e.toString(),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () {
+ Navigator.pop(context);
+ },
+ child: Text(
+ AppLocalizations.of(context)?.ok ?? "Ok",
+ ),
+ ),
+ ],
+ );
+ },
+ );
+ }
+ if (imageChanged) {
+ final api2 = Openapi(basePathOverride: server).getImageApi();
+ // get the image encoded in base64
+ File imagefile = File(entry.imagePath);
+ MultipartFile file = await MultipartFile.fromFile(
+ entry.imagePath,
+ filename: entry.imagePath,
+ );
+ // get length of file
+ headers = {
+ 'Accept': 'application/json',
+ 'Accept-Language': 'en-US,en;q=0.5',
+ 'Accept-Encoding': 'gzip, deflate',
+ 'Content-Type': 'multipart/form-data',
+ "X-Emby-Authorization":
+ "MediaBrowser Client=\"$_client\", Device=\"$_device\", DeviceId=\"$_deviceId\", Version=\"$version\", Token=\"$token\"",
+ 'Connection': 'keep-alive',
+ 'Origin': server,
+ 'Host': server.substring(server.indexOf("//") + 2, server.length),
+ 'Content-Length': '${file.length}',
+ };
+ try {
+ final response = await api2.setItemImage(
+ itemId: entry.id,
+ imageType: ImageType.primary,
+ headers: headers,
+ );
+ // tell if the image was uploaded or not
+ } catch (e) {
+ logger.e(e.toString());
+ }
+ }
+ logger.d(url);
+ // if (entries?.isFavorited == false) {
+ // try {
+ // final response = await api.unmarkFavoriteItem(
+ // userId: userId, itemId: id, headers: headers, url: server);
+ // logger.d(response.data.toString());
+ // } catch (e) {
+ // logger.e(e.toString());
+ // }
+ // } else {
+ // try {
+ // final response = await api.markFavoriteItem(
+ // userId: userId, itemId: id, headers: headers, url: server);
+ // logger.d(response.data.toString());
+ // } catch (e) {
+ // logger.e(e.toString());
+ // }
+ // }
+ }
+
+ void toggleEditDescription() {
+ setState(() {
+ isEditingDescription = !isEditingDescription;
+ });
+ }
+
+ void _insertText(String textToInsertFront, String textToInsertBack) {
+ final text = _descriptionController.text;
+ final textSelection = _descriptionController.selection;
+ // if highlighting text, then wrap the text in the markdown
+ // if not, then just insert the markdown
+ if (textSelection.start != textSelection.end) {
+ final textBefore = text.substring(0, textSelection.start);
+ final textAfter = text.substring(textSelection.end, text.length);
+ final newText = '$textBefore$textToInsertFront'
+ '${text.substring(textSelection.start, textSelection.end)}'
+ '$textToInsertBack$textAfter';
+ _descriptionController.value = TextEditingValue(
+ text: newText,
+ selection: TextSelection.collapsed(
+ offset: textBefore.length +
+ textToInsertFront.length +
+ textSelection.end -
+ textSelection.start,
+ ),
+ );
+ } else {
+ final newText = '$text$textToInsertFront$textToInsertBack';
+ _descriptionController.value = TextEditingValue(
+ text: newText,
+ selection: TextSelection.collapsed(
+ offset: text.length + textToInsertFront.length,
+ ),
+ );
+ }
+ }
+
+ ButtonStyle _buttonStyle(bool isSelected) {
+ return TextButton.styleFrom(
+ foregroundColor: isSelected
+ ? Theme.of(context).primaryTextTheme.labelLarge!.color
+ : Colors.grey,
+ // set the background color to a non-transparent color if selected and a different non-transparent color if not selected
+ backgroundColor: isSelected
+ ? Theme.of(context).primaryTextTheme.labelLarge?.background?.color ??
+ Colors.purple.withOpacity(0.2)
+ : Colors.grey.withOpacity(0.2),
+ );
+ }
+
+ Widget _buildMarkdownToolbar() {
+ return Container(
+ // set the size of the container to be the same as the buttons
+ height: 40,
+ padding: const EdgeInsets.all(4),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(50),
+ color: Colors.purple.withOpacity(0.2),
+ // color: Theme.of(context).primaryColor.withOpacity(0.2),
+ ),
+ // Adjust the style accordingly
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: [
+ // Bold button
+ IconButton(
+ icon: Icon(Icons.format_bold),
+ onPressed: () => _insertText('**', '**'),
+ ),
+ // Italic button
+ IconButton(
+ icon: Icon(Icons.format_italic),
+ onPressed: () => _insertText('_', '_'),
+ ),
+ // Link button
+ IconButton(
+ icon: Icon(Icons.link),
+ onPressed: () => _insertText('[', '](url)'),
+ ),
+ // H1 button
+ IconButton(
+ icon: Icon(Icons.looks_one),
+ onPressed: () => _insertText('# ', ''),
+ ),
+ // List button
+ IconButton(
+ icon: Icon(Icons.list),
+ onPressed: () => _insertText('\n- ', ''),
+ ),
+ // Code button
+ IconButton(
+ icon: Icon(Icons.code),
+ onPressed: () => _insertText('`', '`'),
+ ),
+ // Add more buttons for italic, code blocks, etc.
+ ],
+ ),
+ );
+ }
+
+ void _showDatePicker(BuildContext context) {
+ // try to parse it into a date time
+ // if it fails, then use the current date time
+ DateTime dateTime;
+ try {
+ dateTime = DateTime.parse(entry.releaseDate);
+ } catch (e) {
+ try {
+ dateTime = DateTime.parse(
+ entry.releaseDate.replaceAll(' ', '-'),
+ );
+ } catch (e) {
+ dateTime = DateTime.now();
+ }
+ }
+ showModalBottomSheet(
+ context: context,
+ builder: (BuildContext builder) {
+ return Container(
+ height: MediaQuery.of(context).copyWith().size.height / 3,
+ child: CupertinoDatePicker(
+ mode: CupertinoDatePickerMode.date,
+ initialDateTime: dateTime,
+ onDateTimeChanged: (DateTime newDate) {
+ setState(() {
+ // entry.releaseDate = newDate.toString();
+ // only the month, day, and year
+ entry.releaseDate = DateFormat('dd MM yyyy').format(newDate);
+ });
+ },
+ ),
+ );
+ },
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ leading: IconButton(
+ icon: const Icon(Icons.arrow_back),
+ onPressed: () async {
+ // update the page so that the liked comics are at the top
+ Navigator.pop(context, entry);
+ },
+ ),
+ // trailing button to toggle edit mode
+ actions: [
+ IconButton(
+ // if edit mode is true, show a preview button, else show an edit button
+ icon: Icon(
+ editMode ? Icons.visibility : Icons.edit,
+ ),
+ onPressed: () {
+ setState(() {
+ editMode = !editMode;
+ });
+ },
+ ),
+ ],
+ ),
+ body: SingleChildScrollView(
+ child: !editMode
+ ? Padding(
+ padding: const EdgeInsets.all(16),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Padding(
+ padding: const EdgeInsets.fromLTRB(0, 0, 16, 0),
+ child: RoundedImageWithShadow(
+ imageUrl: entry.imagePath,
+ radius: 15,
+ ratio: 0.75,
+ size: Size(MediaQuery.of(context).size.width * 0.3,
+ MediaQuery.of(context).size.width * 0.3 * 1.5),
+ ),
+ ),
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ SizedBox(
+ width:
+ MediaQuery.of(context).size.width * 0.5,
+ // child: TextField(
+ // style: const TextStyle(
+ // fontSize: 20,
+ // ),
+ // controller: _titleController,
+ // focusNode: _titleFocusNode,
+ // onChanged: (value) {
+ // entry.title = value;
+ // },
+ // decoration: InputDecoration(
+ // hintText: 'Enter Title Here',
+ // border: InputBorder.none,
+ // ),
+ // ),
+ child: AutoSizeText(
+ entry.title,
+ maxLines: 2,
+ style: const TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 30,
+ ),
+ // textAlign: TextAlign.left,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(
+ height: 10,
+ ),
+ ConstrainedBox(
+ constraints: BoxConstraints(
+ maxWidth:
+ MediaQuery.of(context).size.width * 0.55,
+ ),
+ child: Row(
+ children: [
+ Text(
+ "Release Date: ",
+ style: TextStyle(
+ fontSize: 20,
+ ),
+ ),
+ const Spacer(flex: 1),
+ // regular text instead of button
+ Text(
+ entry.releaseDate,
+ style: const TextStyle(
+ fontSize: 12,
+ ),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(
+ height: 5,
+ ),
+ ConstrainedBox(
+ constraints: BoxConstraints(
+ maxWidth:
+ MediaQuery.of(context).size.width * 0.55,
+ ),
+ child: Row(
+ children: [
+ Text(
+ "Rating: ",
+ style: TextStyle(
+ fontSize: 20,
+ ),
+ ),
+ Spacer(flex: 1),
+ IgnorePointer(
+ child: CustomRating(
+ max: 5,
+ score: entry.rating / 2,
+ star: Star(
+ fillColor: Color.lerp(
+ Colors.red,
+ Colors.yellow,
+ entry.rating / 10,
+ )!,
+ emptyColor:
+ Colors.grey.withOpacity(0.5),
+ ),
+ onRating: (double score) {
+ entry.rating = score * 2;
+ setState(() {});
+ },
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ const SizedBox(
+ height: 10,
+ ),
+ // allow users to edit the description in rich text
+ // add a button that displays the rich text, and when clicked it will display the markdown that you can edit
+ // 2 buttons at the bottom, one says save to device and the says save to server
+ // Expanded(
+ // text saying description
+ Text(
+ "Description:",
+ style: TextStyle(
+ fontSize: 20,
+ ),
+ ),
+ MarkdownBody(
+ data: fixRichText(
+ _descriptionController.text,
+ ),
+ extensionSet: md.ExtensionSet(
+ md.ExtensionSet.gitHubFlavored.blockSyntaxes,
+ [
+ md.EmojiSyntax(),
+ ...md.ExtensionSet.gitHubFlavored.inlineSyntaxes
+ ],
+ ),
+ ),
+ const SizedBox(
+ height: 10,
+ ),
+
+ if (tags.isNotEmpty)
+ Padding(
+ padding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
+ child: SizedBox(
+ height: 50,
+ child: ListView.builder(
+ scrollDirection: Axis.horizontal,
+ itemCount: tags.length,
+ itemBuilder: (context, index) {
+ return Container(
+ padding:
+ const EdgeInsets.symmetric(horizontal: 5),
+ child: Chip(
+ label: Text(tags[index]),
+ ),
+ );
+ },
+ ),
+ ),
+ ),
+ ],
+ ),
+ )
+ : Padding(
+ padding: const EdgeInsets.all(16),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Padding(
+ padding: const EdgeInsets.fromLTRB(0, 0, 16, 0),
+ child: Stack(
+ children: [
+ RoundedImageWithShadow(
+ imageUrl: entry.imagePath,
+ radius: 15,
+ ratio: 0.75,
+ size: Size(
+ MediaQuery.of(context).size.width * 0.3,
+ MediaQuery.of(context).size.width *
+ 0.3 *
+ 1.5),
+ ),
+ Positioned.fill(
+ child: Material(
+ color: Colors.transparent,
+ child: InkWell(
+ onTap: () => (
+ _pickImage(),
+ changed = true,
+ ),
+ child: Container(
+ decoration: BoxDecoration(
+ color: Colors.grey.withOpacity(
+ 0.5,
+ ), // Semi-transparent gray overlay
+ borderRadius: BorderRadius.circular(
+ 15,
+ ), // Match the border radius of the image
+ ),
+ child: const Center(
+ child: Icon(
+ Icons
+ .camera_alt, // Icon indicating that you can upload an image
+ color: Colors.white,
+ size: 50,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ SizedBox(
+ width:
+ MediaQuery.of(context).size.width * 0.5,
+ child: TextField(
+ style: const TextStyle(
+ fontSize: 20,
+ ),
+ controller: _titleController,
+ focusNode: _titleFocusNode,
+ onChanged: (value) {
+ entry.title = value;
+ changed = true;
+ },
+ ),
+ ),
+ ],
+ ),
+ // Release date
+ ConstrainedBox(
+ constraints: BoxConstraints(
+ maxWidth:
+ MediaQuery.of(context).size.width * 0.55,
+ ),
+ child: Row(
+ children: [
+ Text(
+ "Release Date: ",
+ style: TextStyle(
+ fontSize: 20,
+ ),
+ ),
+ Spacer(flex: 1),
+ TextButton(
+ onPressed: () {
+ _showDatePicker(context);
+ changed = true;
+ },
+ child: Text(
+ entry.releaseDate,
+ style: TextStyle(
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(
+ height: 5,
+ ),
+ // Rating
+ // allign left
+ ConstrainedBox(
+ constraints: BoxConstraints(
+ maxWidth:
+ MediaQuery.of(context).size.width * 0.55,
+ ),
+ child: Row(
+ children: [
+ Text(
+ "Rating: ",
+ style: TextStyle(
+ fontSize: 20,
+ ),
+ ),
+ Spacer(flex: 1),
+ CustomRating(
+ max: 5,
+ score: entry.rating / 2,
+ star: Star(
+ fillColor: Color.lerp(Colors.red,
+ Colors.yellow, entry.rating / 10)!,
+ emptyColor: Colors.grey.withOpacity(0.5),
+ ),
+ onRating: (double score) {
+ entry.rating = score * 2;
+ changed = true;
+ setState(() {});
+ },
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ const SizedBox(
+ height: 10,
+ ),
+ // allow users to edit the description in rich text
+ // add a button that displays the rich text, and when clicked it will display the markdown that you can edit
+ // 2 buttons at the bottom, one says save to device and the says save to server
+ // Expanded(
+ // text saying description
+ Text(
+ "Description:",
+ style: TextStyle(
+ fontSize: 20,
+ ),
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: [],
+ ),
+ SizedBox(
+ height: 10,
+ ),
+ _buildMarkdownToolbar(),
+ SizedBox(
+ height: 10,
+ ),
+ // OutlinedButton(
+ // onPressed: () => setState(() {
+ // isEditingDescription = !isEditingDescription;
+ // }),
+ // child: Text('Preview')),
+ TextField(
+ controller: _descriptionController,
+ keyboardType: TextInputType.multiline,
+ maxLines: null, // Makes it expandable
+ decoration: InputDecoration(
+ border: OutlineInputBorder(),
+ ),
+ ),
+ const SizedBox(
+ height: 10,
+ ),
+
+ // Editing tags
+ TextField(
+ controller: _tagController,
+ decoration: InputDecoration(
+ labelText: 'Add a tag',
+ suffixIcon: IconButton(
+ icon: Icon(Icons.add),
+ onPressed: () {
+ // add the tag
+ setState(() {
+ changed = true;
+ if (_tagController.text.isNotEmpty) {
+ tags = List.from(tags)
+ ..add(_tagController.text);
+ }
+ _tagController.clear();
+ });
+ },
+ ),
+ ),
+ onSubmitted: (value) {
+ setState(() {
+ if (value.isNotEmpty) {
+ tags = List.from(tags)..add(value);
+ }
+ });
+ },
+ ),
+ ReorderableListView(
+ shrinkWrap: true,
+ physics: NeverScrollableScrollPhysics(),
+ onReorder: (int oldIndex, int newIndex) {
+ setState(() {
+ if (oldIndex < newIndex) {
+ newIndex -= 1;
+ }
+ changed = true;
+ setState(() {
+ tags = List.from(
+ tags
+ ..insert(
+ newIndex,
+ tags.removeAt(oldIndex),
+ ),
+ );
+ });
+ });
+ },
+ children: List.generate(tags.length, (index) {
+ return ListTile(
+ key: ValueKey(tags[index]),
+ title: Text(tags[index]),
+ trailing: IconButton(
+ icon: Icon(Icons.delete),
+ onPressed: () {
+ setState(() {
+ tags = List.from(tags..removeAt(index));
+ changed = true;
+ });
+ },
+ ),
+ );
+ }),
+ ),
+ SizedBox(
+ height: 10,
+ ),
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: [
+ TextButton(
+ onPressed: saveToDevice,
+ child: SizedBox(
+ width: MediaQuery.of(context).size.width * 0.40,
+ child: const Center(
+ child: Text("Save to device"),
+ ),
+ ),
+ ),
+ if (!offline)
+ TextButton(
+ onPressed: saveToServer,
+ // onPressed: saveToServer,
+ // child: Text("Save to server"),
+ child: SizedBox(
+ width: MediaQuery.of(context).size.width * 0.40,
+ child: const Center(
+ child: Text("Save to server"),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/screens/MainScreens/downloadsScreen.dart b/lib/screens/MainScreens/downloadsScreen.dart
index f1b5dd89..669a3689 100644
--- a/lib/screens/MainScreens/downloadsScreen.dart
+++ b/lib/screens/MainScreens/downloadsScreen.dart
@@ -1,6 +1,7 @@
// The purpose of this file is to have a screen with all the downloaded books with easy access to delete them
import 'package:flutter/material.dart';
+import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:jellybook/screens/collectionScreen.dart';
import 'package:jellybook/screens/infoScreen.dart';
import 'package:isar/isar.dart';
@@ -201,19 +202,20 @@ class _DownloadsScreenState extends State {
if (snapshot.data[index].rating < 0 &&
snapshot.data[index].description != '')
Flexible(
- child: RichText(
- text: TextSpan(
- text: fixRichText(
- snapshot.data[index].description),
- style: const TextStyle(
- fontStyle: FontStyle.italic,
- fontSize: 15,
- color: Colors.grey,
- ),
- ),
- overflow: TextOverflow.ellipsis,
- maxLines: 1,
+ child: MarkdownBody(
+ // text: TextSpan(
+ data: fixRichText(
+ snapshot.data[index].description),
),
+ // style: const TextStyle(
+ // fontStyle: FontStyle.italic,
+ // fontSize: 15,
+ // color: Colors.grey,
+ // ),
+ // ),
+ // overflow: TextOverflow.ellipsis,
+ // maxLines: 1,
+ // ),
),
],
),
diff --git a/lib/screens/MainScreens/settingsScreen.dart b/lib/screens/MainScreens/settingsScreen.dart
index c32cb3f6..16ad45e9 100644
--- a/lib/screens/MainScreens/settingsScreen.dart
+++ b/lib/screens/MainScreens/settingsScreen.dart
@@ -33,7 +33,9 @@ class _SettingsScreenState extends State {
getPackageInfo();
super.initState();
// Settings.init();
- setSharedPrefs();
+ setSharedPrefs().then((value) {
+ setState(() {});
+ });
}
themeListener() {
@@ -223,7 +225,7 @@ class _SettingsScreenState extends State {
Future themeSettings(BuildContext context) async {
return SettingsItem(
title: AppLocalizations.of(context)?.theme ?? 'theme',
- selected: prefs?.getString('theme') ?? 'system',
+ selected: await prefs!.getString('theme') ?? 'system',
backgroundColor: Theme.of(context).splashColor,
icon: Icons.color_lens,
values: {
diff --git a/lib/screens/collectionScreen.dart b/lib/screens/collectionScreen.dart
index 08d9041a..73f00979 100644
--- a/lib/screens/collectionScreen.dart
+++ b/lib/screens/collectionScreen.dart
@@ -1,6 +1,8 @@
// 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:flutter_markdown/flutter_markdown.dart';
+import 'package:markdown/markdown.dart' as md;
import 'package:isar/isar.dart';
import 'package:isar_flutter_libs/isar_flutter_libs.dart';
import 'package:jellybook/models/entry.dart';
@@ -163,19 +165,24 @@ class _collectionScreenState extends State {
if (snapshot.data[index].rating < 0 &&
snapshot.data[index].description != '')
Flexible(
- child: RichText(
- text: TextSpan(
- text:
- fixRichText(snapshot.data[index].description),
- // prevent overflow
- style: const TextStyle(
- fontStyle: FontStyle.italic,
- fontSize: 15,
- color: Colors.grey,
- ),
+ child: MarkdownBody(
+ data: fixRichText(snapshot.data[index].description),
+ extensionSet: md.ExtensionSet(
+ md.ExtensionSet.gitHubFlavored.blockSyntaxes,
+ [
+ md.EmojiSyntax(),
+ ...md.ExtensionSet.gitHubFlavored.inlineSyntaxes
+ ],
),
- overflow: TextOverflow.ellipsis,
- maxLines: 1,
+ // prevent overflow
+ // style: const TextStyle(
+ // fontStyle: FontStyle.italic,
+ // fontSize: 15,
+ // color: Colors.grey,
+ // ),
+ // ),
+ // overflow: TextOverflow.ellipsis,
+ // maxLines: 1,
),
),
],
diff --git a/lib/screens/infoScreen.dart b/lib/screens/infoScreen.dart
index 4198cf04..3e1070dc 100644
--- a/lib/screens/infoScreen.dart
+++ b/lib/screens/infoScreen.dart
@@ -7,18 +7,21 @@ import 'package:jellybook/models/login.dart';
import 'package:jellybook/providers/deleteComic.dart';
import 'package:jellybook/providers/fixRichText.dart';
import 'package:jellybook/screens/downloaderScreen.dart';
+import 'package:jellybook/screens/EditScreen.dart';
import 'package:jellybook/screens/readingScreen.dart';
import 'package:jellybook/widgets/roundedImageWithShadow.dart';
import 'package:like_button/like_button.dart';
import 'package:jellybook/providers/updateLike.dart';
import 'package:isar/isar.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
+import 'package:markdown/markdown.dart' as md;
import 'package:jellybook/models/entry.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:jellybook/variables.dart';
import 'package:package_info_plus/package_info_plus.dart' as p_info;
import 'package:openapi/openapi.dart';
import 'package:shared_preferences/shared_preferences.dart';
+import 'package:jellybook/providers/Author.dart';
class InfoScreen extends StatefulWidget {
bool offline;
@@ -499,6 +502,20 @@ class _InfoScreenState extends State {
logger.d("deleting comic");
await deleteComic(entry.id, context);
}
+ if (value == "edit") {
+ logger.d("editing comic");
+ final result = await Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (context) =>
+ EditScreen(entry: entry, offline: offline),
+ ),
+ );
+ setState(() {
+ if (result != null) entry = result;
+ });
+ // }
+ }
},
child: const Icon(Icons.more_vert, color: Colors.white),
itemBuilder: (context) {
@@ -515,6 +532,16 @@ class _InfoScreenState extends State {
],
),
),
+ PopupMenuItem(
+ value: "edit",
+ child: Row(
+ children: [
+ const Icon(Icons.edit),
+ const SizedBox(width: 10),
+ Text(AppLocalizations.of(context)?.edit ?? "Edit"),
+ ],
+ ),
+ ),
];
},
),
@@ -660,6 +687,13 @@ class _InfoScreenState extends State {
padding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
child: MarkdownBody(
data: fixRichText(entry.description),
+ extensionSet: md.ExtensionSet(
+ md.ExtensionSet.gitHubFlavored.blockSyntaxes,
+ [
+ md.EmojiSyntax(),
+ ...md.ExtensionSet.gitHubFlavored.inlineSyntaxes
+ ],
+ ),
selectable: false,
shrinkWrap: true,
styleSheet:
@@ -799,18 +833,3 @@ class _InfoScreenState extends State {
);
}
}
-
-class Author {
- final String name;
- String? link;
- List roles = [];
-
- Author({
- required this.name,
- this.link,
- });
-
- void addRole(String role) {
- roles.add(role);
- }
-}
diff --git a/lib/widgets/ToggleEditPreviewButton.dart b/lib/widgets/ToggleEditPreviewButton.dart
new file mode 100644
index 00000000..d49ef2b2
--- /dev/null
+++ b/lib/widgets/ToggleEditPreviewButton.dart
@@ -0,0 +1,86 @@
+import 'package:flutter/material.dart';
+
+class ToggleEditPreviewButton extends StatefulWidget {
+ final bool isEditing;
+ final VoidCallback onEdit;
+ final VoidCallback onPreview;
+ final double width;
+
+ const ToggleEditPreviewButton({
+ Key? key,
+ required this.isEditing,
+ required this.onEdit,
+ required this.onPreview,
+ this.width = 0.4,
+ }) : super(key: key);
+
+ @override
+ _ToggleEditPreviewButtonState createState() =>
+ _ToggleEditPreviewButtonState();
+}
+
+class _ToggleEditPreviewButtonState extends State {
+ late bool isEditing;
+
+ @override
+ void initState() {
+ super.initState();
+ isEditing = widget.isEditing;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: [
+ SizedBox(
+ width: MediaQuery.of(context).size.width * widget.width,
+ child: TextButton(
+ style: TextButton.styleFrom(
+ foregroundColor: isEditing
+ ? Theme.of(context).colorScheme.onPrimary
+ : Colors.grey,
+ backgroundColor: isEditing
+ ? Theme.of(context).colorScheme.primary
+ : Colors.grey.withOpacity(0.2),
+ ),
+ onPressed: () {
+ if (!isEditing) {
+ setState(() {
+ isEditing = true;
+ });
+ widget.onEdit();
+ }
+ },
+ child: const Text('Edit'),
+ ),
+ ),
+ SizedBox(
+ width: 10,
+ ),
+ SizedBox(
+ width: MediaQuery.of(context).size.width * widget.width,
+ child: TextButton(
+ style: TextButton.styleFrom(
+ foregroundColor: !isEditing
+ ? Theme.of(context).colorScheme.onPrimary
+ : Colors.grey,
+ backgroundColor: !isEditing
+ ? Theme.of(context).colorScheme.primary
+ : Colors.grey.withOpacity(0.2),
+ ),
+ onPressed: () {
+ if (isEditing) {
+ setState(() {
+ isEditing = false;
+ });
+ widget.onPreview();
+ }
+ },
+ child: const Text('Preview'),
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/widgets/roundedImageWithShadow.dart b/lib/widgets/roundedImageWithShadow.dart
index b3846e03..bd70a05d 100644
--- a/lib/widgets/roundedImageWithShadow.dart
+++ b/lib/widgets/roundedImageWithShadow.dart
@@ -1,3 +1,8 @@
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:fancy_shimmer_image/fancy_shimmer_image.dart';
+
import 'package:flutter/material.dart';
import 'package:fancy_shimmer_image/fancy_shimmer_image.dart';
@@ -8,6 +13,7 @@ class RoundedImageWithShadow extends StatefulWidget {
final Color shadowColor;
final Function(Size)? onImageSizeAvailable;
final String errorWidgetAsset;
+ final Size? size; // Optional size parameter
const RoundedImageWithShadow({
super.key,
@@ -17,6 +23,7 @@ class RoundedImageWithShadow extends StatefulWidget {
this.shadowColor = Colors.black,
this.onImageSizeAvailable,
this.errorWidgetAsset = 'assets/images/NoCoverArt.png',
+ this.size, // Add size to the constructor
});
@override
@@ -36,14 +43,13 @@ class _RoundedImageWithShadowState extends State {
});
}
- @override
- void dispose() {
- super.dispose();
- }
-
@override
Widget build(BuildContext context) {
return Container(
+ width: widget
+ .size?.width, // Use the width from the size parameter if provided
+ height: widget
+ .size?.height, // Use the height from the size parameter if provided
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(widget.radius),
boxShadow: [
@@ -59,32 +65,41 @@ class _RoundedImageWithShadowState extends State {
borderRadius: BorderRadius.circular(widget.radius),
child: AspectRatio(
aspectRatio: widget.ratio,
- child:
- widget.imageUrl == '' || widget.imageUrl.toLowerCase() == 'asset'
- ? LayoutBuilder(
- builder: (context, constraints) {
- imageSize = constraints.biggest;
+ child: widget.imageUrl == '' ||
+ widget.imageUrl.toLowerCase() == 'asset'
+ ? LayoutBuilder(
+ builder: (context, constraints) {
+ imageSize = widget.size ?? constraints.biggest;
- return Image.asset(
- widget.errorWidgetAsset,
- fit: BoxFit.cover,
- );
- },
- )
- : LayoutBuilder(
+ return Image.asset(
+ widget.errorWidgetAsset,
+ fit: BoxFit.cover,
+ width:
+ imageSize?.width, // Use the width from the imageSize
+ height: imageSize
+ ?.height, // Use the height from the imageSize
+ );
+ },
+ )
+ : widget.imageUrl.contains('http')
+ ? LayoutBuilder(
builder: (context, constraints) {
- imageSize = constraints.biggest;
+ imageSize = widget.size ?? constraints.biggest;
return FancyShimmerImage(
- width: constraints.biggest.width,
imageUrl: widget.imageUrl,
boxFit: BoxFit.cover,
errorWidget: Image.asset(
widget.errorWidgetAsset,
+ width: imageSize
+ ?.width, // Use the width from the imageSize
+ height: imageSize
+ ?.height, // Use the height from the imageSize
),
);
},
- ),
+ )
+ : Image.file(File(widget.imageUrl), fit: BoxFit.cover),
),
),
);