diff --git a/lib/eval/model/m_bridge.dart b/lib/eval/model/m_bridge.dart index 04ff30c4..f63f4ebc 100644 --- a/lib/eval/model/m_bridge.dart +++ b/lib/eval/model/m_bridge.dart @@ -3,7 +3,7 @@ import 'package:bot_toast/bot_toast.dart'; import 'package:dart_eval/dart_eval_bridge.dart'; import 'package:dart_eval/stdlib/core.dart'; import 'package:flutter/material.dart'; -import 'package:html/dom.dart'; +import 'package:html/dom.dart' hide Text; import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/intl.dart'; import 'package:js_packer/js_packer.dart'; @@ -631,13 +631,23 @@ void botToast(String title, double? fontSize, double alignX = 0, double alignY = 0.99}) { - BotToast.showSimpleNotification( - titleStyle: TextStyle(fontSize: fontSize), - onlyOne: true, - dismissDirections: [DismissDirection.horizontal, DismissDirection.down], - align: Alignment(alignX, alignY), - duration: Duration(seconds: second), - title: title); + final assets = [ + 'assets/app_icons/icon-black.png', + 'assets/app_icons/icon-red.png' + ]; + BotToast.showNotification( + onlyOne: true, + dismissDirections: [DismissDirection.horizontal, DismissDirection.down], + align: Alignment(alignX, alignY), + duration: Duration(seconds: second), + animationDuration: const Duration(milliseconds: 200), + animationReverseDuration: const Duration(milliseconds: 200), + leading: (_) => Image.asset((assets..shuffle()).first, height: 25), + title: (_) => Text( + title, + style: TextStyle(fontSize: fontSize), + ), + ); } (encrypt.Encrypter, encrypt.IV) _encrypt(String keyy, String ivv) { diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb index 4795a235..42e287eb 100644 --- a/lib/l10n/app_ar.arb +++ b/lib/l10n/app_ar.arb @@ -287,5 +287,11 @@ "next_chapter": "الفصل التالي", "next_5_chapters": "الفصول الخمسة التالية", "next_10_chapters": "الفصول العشرة التالية", - "next_25_chapters": "الفصول الخمسة والعشرون التالية" + "next_25_chapters": "الفصول الخمسة والعشرون التالية", + "cover_saved": "الغلاف المحفوظ", + "set_as_cover": "تعيين كغطاء", + "use_this_as_cover_art": "هل تريد استخدام هذا كفن الغلاف؟", + "save": "حفظ", + "picture_saved": "الصورة المحفوظة", + "cover_updated": "تم تحديث الغلاف" } \ No newline at end of file diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 5a26676e..6abe8f3c 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -287,5 +287,11 @@ "next_chapter": "Nächstes Kapitel", "next_5_chapters": "Nächsten 5 Kapitel", "next_10_chapters": "Nächsten 10 Kapitel", - "next_25_chapters": "Nächsten 25 Kapitel" + "next_25_chapters": "Nächsten 25 Kapitel", + "cover_saved": "Titelbild gespeichert", + "set_as_cover": "Als Titelbild festlegen", + "use_this_as_cover_art": "Dies als Titelbild verwenden?", + "save": "Speichern", + "picture_saved": "Bild gespeichert", + "cover_updated": "Cover aktualisiert" } \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 2ab2fd11..fcc3c008 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -289,5 +289,11 @@ "next_chapter": "Next chapter", "next_5_chapters": "Next 5 chapters", "next_10_chapters": "Next 10 chapters", - "next_25_chapters": "Next 25 chapters" + "next_25_chapters": "Next 25 chapters", + "cover_saved": "Cover saved", + "set_as_cover": "Set as cover", + "use_this_as_cover_art": "Use this as cover art?", + "save": "Save", + "picture_saved": "Picture saved", + "cover_updated": "Cover updated" } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 5f7c8425..44bab0d8 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -287,5 +287,11 @@ "next_chapter": "Próximo capítulo", "next_5_chapters": "Próximos 5 capítulos", "next_10_chapters": "Próximos 10 capítulos", - "next_25_chapters": "Próximos 25 capítulos" + "next_25_chapters": "Próximos 25 capítulos", + "cover_saved": "Portada guardada", + "set_as_cover": "Establecer como portada", + "use_this_as_cover_art": "¿Usar esto como portada?", + "save": "Guardar", + "picture_saved": "Imagen guardada", + "cover_updated": "Portada actualizada" } \ No newline at end of file diff --git a/lib/l10n/app_es_419.arb b/lib/l10n/app_es_419.arb index 84ea57eb..c8ae99ff 100644 --- a/lib/l10n/app_es_419.arb +++ b/lib/l10n/app_es_419.arb @@ -287,5 +287,11 @@ "next_chapter": "Siguiente capítulo", "next_5_chapters": "Siguientes 5 capítulos", "next_10_chapters": "Siguientes 10 capítulos", - "next_25_chapters": "Siguientes 25 capítulos" + "next_25_chapters": "Siguientes 25 capítulos", + "cover_saved": "Portada guardada", + "set_as_cover": "Establecer como portada", + "use_this_as_cover_art": "¿Usar esto como portada?", + "save": "Guardar", + "picture_saved": "Imagen guardada", + "cover_updated": "Portada actualizada" } \ No newline at end of file diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 137738c9..60f8b9e1 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -288,5 +288,11 @@ "next_chapter": "Chapitre suivant", "next_5_chapters": "5 chapitres suivants", "next_10_chapters": "10 chapitres suivants", - "next_25_chapters": "25 chapitres suivants" + "next_25_chapters": "25 chapitres suivants", + "cover_saved": "Couverture enregistrée", + "set_as_cover": "Définir comme couverture", + "use_this_as_cover_art": "Utiliser ceci comme illustration de couverture ?", + "save": "Enregistrer", + "picture_saved": "Image enregistrée", + "cover_updated": "Couverture mise à jour" } \ No newline at end of file diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index 35c7041c..9672f18d 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -287,5 +287,11 @@ "next_chapter": "Berikutnya bab", "next_5_chapters": "5 bab berikutnya", "next_10_chapters": "10 bab berikutnya", - "next_25_chapters": "25 bab berikutnya" + "next_25_chapters": "25 bab berikutnya", + "cover_saved": "Sampul disimpan", + "set_as_cover": "Atur sebagai sampul", + "use_this_as_cover_art": "Gunakan ini sebagai seni sampul?", + "save": "Simpan", + "picture_saved": "Gambar disimpan", + "cover_updated": "Penutup diperbarui" } \ No newline at end of file diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index d96ace03..08e5161c 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -287,5 +287,11 @@ "next_chapter": "Capitolo successivo", "next_5_chapters": "Prossimi 5 capitoli", "next_10_chapters": "Prossimi 10 capitoli", - "next_25_chapters": "Prossimi 25 capitoli" + "next_25_chapters": "Prossimi 25 capitoli", + "cover_saved": "Copertina salvata", + "set_as_cover": "Imposta come copertina", + "use_this_as_cover_art": "Usare questo come copertina?", + "save": "Salva", + "picture_saved": "Immagine salvata", + "cover_updated": "Copertina aggiornata" } \ No newline at end of file diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 8ff14810..e3dfe505 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -287,5 +287,11 @@ "next_chapter": "Próximo capítulo", "next_5_chapters": "Próximos 5 capítulos", "next_10_chapters": "Próximos 10 capítulos", - "next_25_chapters": "Próximos 25 capítulos" + "next_25_chapters": "Próximos 25 capítulos", + "cover_saved": "Capa salva", + "set_as_cover": "Definir como capa", + "use_this_as_cover_art": "Usar isso como arte de capa?", + "save": "Salvar", + "picture_saved": "Imagem salva", + "cover_updated": "Capa atualizada" } \ No newline at end of file diff --git a/lib/l10n/app_pt_BR.arb b/lib/l10n/app_pt_BR.arb index faae244a..130bcf26 100644 --- a/lib/l10n/app_pt_BR.arb +++ b/lib/l10n/app_pt_BR.arb @@ -287,5 +287,11 @@ "next_chapter": "Próximo capítulo", "next_5_chapters": "Próximos 5 capítulos", "next_10_chapters": "Próximos 10 capítulos", - "next_25_chapters": "Próximos 25 capítulos" + "next_25_chapters": "Próximos 25 capítulos", + "cover_saved": "Capa salva", + "set_as_cover": "Definir como capa", + "use_this_as_cover_art": "Usar isso como arte de capa?", + "save": "Salvar", + "picture_saved": "Foto salva", + "cover_updated": "Capa atualizada" } \ No newline at end of file diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 3a7fc222..032f16c4 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -287,5 +287,11 @@ "next_chapter": "Следующая глава", "next_5_chapters": "Следующие 5 глав", "next_10_chapters": "Следующие 10 глав", - "next_25_chapters": "Следующие 25 глав" + "next_25_chapters": "Следующие 25 глав", + "cover_saved": "Обложка сохранена", + "set_as_cover": "Установить как обложку", + "use_this_as_cover_art": "Использовать это как обложку?", + "save": "Сохранить", + "picture_saved": "Изображение сохранено", + "cover_updated": "Обложка обновлена" } \ No newline at end of file diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index fd06b9e6..bf03e854 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -287,5 +287,11 @@ "next_chapter": "Sonraki bölüm", "next_5_chapters": "Sonraki 5 bölüm", "next_10_chapters": "Sonraki 10 bölüm", - "next_25_chapters": "Sonraki 25 bölüm" + "next_25_chapters": "Sonraki 25 bölüm", + "cover_saved": "Kapak kaydedildi", + "set_as_cover": "Kapak olarak ayarla", + "use_this_as_cover_art": "Bu resmi kapak sanatı olarak kullan?", + "save": "Kaydet", + "picture_saved": "Resim kaydedildi", + "cover_updated": "Kapak güncellendi" } \ No newline at end of file diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 3d6112fb..d68e4c1b 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -289,5 +289,11 @@ "next_chapter": "下一章", "next_5_chapters": "下5章", "next_10_chapters": "下10章", - "next_25_chapters": "下25章" + "next_25_chapters": "下25章", + "cover_saved": "封面已保存", + "set_as_cover": "设置为封面", + "use_this_as_cover_art": "使用此作为封面?", + "save": "保存", + "picture_saved": "图片已保存", + "cover_updated": "封面已更新" } \ No newline at end of file diff --git a/lib/modules/anime/widgets/desktop.dart b/lib/modules/anime/widgets/desktop.dart index 8d30be14..a721130b 100644 --- a/lib/modules/anime/widgets/desktop.dart +++ b/lib/modules/anime/widgets/desktop.dart @@ -29,7 +29,8 @@ class DesktopControllerWidget extends StatefulWidget { required this.tempDuration}); @override - State createState() => _DesktopControllerWidgetState(); + State createState() => + _DesktopControllerWidgetState(); } class _DesktopControllerWidgetState extends State { @@ -797,13 +798,16 @@ class _CustomMaterialDesktopFullscreenButtonState Future setFullScreen({bool? value}) async { if (value != null) { - await windowManager.setTitleBarStyle( - value == false ? TitleBarStyle.normal : TitleBarStyle.hidden); - await windowManager.setFullScreen(value); - if (value == false) { - await windowManager.center(); + final isFullScreen = await windowManager.isFullScreen(); + if (value != isFullScreen) { + await windowManager.setTitleBarStyle( + value == false ? TitleBarStyle.normal : TitleBarStyle.hidden); + await windowManager.setFullScreen(value); + if (value == false) { + await windowManager.center(); + } + await windowManager.show(); } - await windowManager.show(); return value; } final isFullScreen = await windowManager.isFullScreen(); diff --git a/lib/modules/manga/detail/manga_detail_view.dart b/lib/modules/manga/detail/manga_detail_view.dart index 8750e8a1..20bf7889 100644 --- a/lib/modules/manga/detail/manga_detail_view.dart +++ b/lib/modules/manga/detail/manga_detail_view.dart @@ -9,6 +9,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:isar/isar.dart'; +import 'package:mangayomi/eval/model/m_bridge.dart'; import 'package:mangayomi/main.dart'; import 'package:mangayomi/models/chapter.dart'; import 'package:mangayomi/models/download.dart'; @@ -24,10 +25,12 @@ import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provi import 'package:mangayomi/modules/more/settings/appearance/providers/pure_black_dark_mode_state_provider.dart'; import 'package:mangayomi/modules/more/settings/track/widgets/track_listile.dart'; import 'package:mangayomi/providers/l10n_providers.dart'; +import 'package:mangayomi/providers/storage_provider.dart'; import 'package:mangayomi/services/get_source_baseurl.dart'; import 'package:mangayomi/sources/utils/utils.dart'; import 'package:mangayomi/utils/cached_network.dart'; import 'package:mangayomi/utils/extensions/build_context_extensions.dart'; +import 'package:mangayomi/utils/extensions/others.dart'; import 'package:mangayomi/utils/headers.dart'; import 'package:mangayomi/modules/manga/detail/providers/isar_providers.dart'; import 'package:mangayomi/modules/manga/detail/providers/state_providers.dart'; @@ -1581,7 +1584,7 @@ class _MangaDetailViewState extends ConsumerState ); } - _openImage(ImageProvider imageProvider) { + void _openImage(ImageProvider imageProvider) { showDialog( context: context, builder: (context) { @@ -1610,141 +1613,221 @@ class _MangaDetailViewState extends ConsumerState Positioned( bottom: 0, right: 0, - child: Row( + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, children: [ - Column( - children: [ - StreamBuilder( - stream: isar.trackPreferences - .filter() - .syncIdIsNotNull() - .watch(fireImmediately: true), - builder: (context, snapshot) { - List? entries = - snapshot.hasData ? snapshot.data! : []; - if (entries.isEmpty) { - return Container(); - } - return Column( - children: entries - .map((e) => Padding( - padding: const EdgeInsets.all(8.0), - child: MaterialButton( - padding: const EdgeInsets.all(0), - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(10)), - onPressed: () async { - final trackSearch = - await trackersSearchraggableMenu( - context, - isManga: widget.manga!.isManga!, - track: Track( - status: - TrackStatus.planToRead, - syncId: e.syncId!, - title: widget.manga!.name!), - ) as TrackSearch?; - if (trackSearch != null) { - isar.writeTxnSync(() { - isar.mangas.putSync(widget - .manga! - ..customCoverFromTracker = - trackSearch.coverUrl); - }); - if (context.mounted) { - Navigator.pop(context); - } + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: StreamBuilder( + stream: isar.trackPreferences + .filter() + .syncIdIsNotNull() + .watch(fireImmediately: true), + builder: (context, snapshot) { + List? entries = + snapshot.hasData ? snapshot.data! : []; + if (entries.isEmpty) { + return Container(); + } + return Column( + children: entries + .map((e) => Padding( + padding: const EdgeInsets.all(8.0), + child: MaterialButton( + padding: const EdgeInsets.all(0), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(10)), + onPressed: () async { + final trackSearch = + await trackersSearchraggableMenu( + context, + isManga: widget.manga!.isManga!, + track: Track( + status: + TrackStatus.planToRead, + syncId: e.syncId!, + title: widget.manga!.name!), + ) as TrackSearch?; + if (trackSearch != null) { + isar.writeTxnSync(() { + isar.mangas.putSync( + widget.manga! + ..customCoverImage = null + ..customCoverFromTracker = + trackSearch.coverUrl); + }); + if (context.mounted) { + Navigator.pop(context); + botToast( + context.l10n.cover_updated, + second: 3); } - }, - child: Container( - decoration: BoxDecoration( - borderRadius: - BorderRadius.circular(10), - color: - trackInfos(e.syncId!).$3), - width: 45, - height: 50, - child: Image.asset( - trackInfos(e.syncId!).$1, - height: 30, - ), + } + }, + child: Container( + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(10), + color: + trackInfos(e.syncId!).$3), + width: 45, + height: 50, + child: Image.asset( + trackInfos(e.syncId!).$1, + height: 30, ), ), - )) - .toList(), - ); - }, - ), - - PopupMenuButton( - itemBuilder: (context) { - return [ - if (widget.manga!.customCoverImage != null || - widget.manga!.customCoverFromTracker != - null) - PopupMenuItem( - value: 0, - child: Text(context.l10n.delete)), - PopupMenuItem( - value: 1, child: Text(context.l10n.edit)), - ]; - }, - onSelected: (value) async { - final manga = widget.manga!; - if (value == 0) { - isar.writeTxnSync(() { - isar.mangas.putSync(manga - ..customCoverImage = null - ..customCoverFromTracker = null); - }); - Navigator.pop(context); - } else if (value == 1) { - FilePickerResult? result = - await FilePicker.platform.pickFiles( - type: FileType.custom, - allowedExtensions: [ - 'png', - 'jpg', - 'jpeg' - ]); - if (result != null) { - if (result.files.first.size < 5000000) { - final customCoverImage = - File(result.files.first.path!) - .readAsBytesSync(); - isar.writeTxnSync(() { - isar.mangas.putSync(manga - ..customCoverImage = customCoverImage); - }); - } - } - if (context.mounted) { - Navigator.pop(context); - } - } - }, - child: const Padding( - padding: EdgeInsets.all(8.0), - child: CircleAvatar( - child: Icon(Icons.edit_outlined)), + ), + )) + .toList(), + ); + }, + ), + ), + SizedBox( + width: context.mediaWidth(1), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: context.isLight + ? Colors.white + : Colors.black), + child: GestureDetector( + onTap: () { + Navigator.pop(context); + }, + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Icon(Icons.close), + )), + ), ), - ), - // IconButton( - // onPressed: () async { - // Uint8List? bytes; - // if (isLocalArchive) { - // bytes = - // widget.manga!.customCoverImage as Uint8List?; - // } - // await Share.shareXFiles([ - // XFile.fromData(bytes!, - // name: widget.manga!.name, - // mimeType: 'image/jpeg') - // ]); - // }, - // icon: const CircleAvatar(child: Icon(Icons.share))), - ], + Padding( + padding: const EdgeInsets.all(8.0), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: context.isLight + ? Colors.white + : Colors.black), + child: Row( + children: [ + GestureDetector( + onTap: () async { + final bytes = await imageProvider + .getBytes(context); + if (bytes != null) { + await Share.shareXFiles([ + XFile.fromData(bytes, + name: widget.manga!.name, + mimeType: 'image/png') + ]); + } + }, + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Icon(Icons.share), + )), + GestureDetector( + onTap: () async { + final dir = await StorageProvider() + .getGalleryDirectory(); + if (context.mounted) { + final bytes = await imageProvider + .getBytes(context); + if (bytes != null && + context.mounted) { + final file = File( + '${dir!.path}/${widget.manga!.name}.png'); + file.writeAsBytesSync(bytes); + botToast(context.l10n.cover_saved, + second: 3); + } + } + }, + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Icon(Icons.save_outlined), + )), + PopupMenuButton( + itemBuilder: (context) { + return [ + if (widget.manga!.customCoverImage != + null || + widget.manga! + .customCoverFromTracker != + null) + PopupMenuItem( + value: 0, + child: + Text(context.l10n.delete)), + PopupMenuItem( + value: 1, + child: Text(context.l10n.edit)), + ]; + }, + onSelected: (value) async { + final manga = widget.manga!; + if (value == 0) { + isar.writeTxnSync(() { + isar.mangas.putSync(manga + ..customCoverImage = null + ..customCoverFromTracker = null); + }); + Navigator.pop(context); + } else if (value == 1) { + FilePickerResult? result = + await FilePicker.platform + .pickFiles( + type: FileType.custom, + allowedExtensions: [ + 'png', + 'jpg', + 'jpeg' + ]); + if (result != null && + context.mounted) { + if (result.files.first.size < + 5000000) { + final customCoverImage = + File(result.files.first.path!) + .readAsBytesSync(); + isar.writeTxnSync(() { + isar.mangas.putSync(manga + ..customCoverImage = + customCoverImage); + }); + botToast( + context.l10n.cover_updated, + second: 3); + } + } + if (context.mounted) { + Navigator.pop(context); + } + } + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + Icons.edit_outlined, + color: !context.isLight + ? Colors.white + : Colors.black, + )), + ), + ], + ), + ), + ), + ], + ), ), ], ), diff --git a/lib/modules/manga/reader/double_columm_view_center.dart b/lib/modules/manga/reader/double_columm_view_center.dart index bad5d831..09581ba5 100644 --- a/lib/modules/manga/reader/double_columm_view_center.dart +++ b/lib/modules/manga/reader/double_columm_view_center.dart @@ -14,6 +14,7 @@ import 'package:photo_view/photo_view_gallery.dart'; class DoubleColummView extends StatefulWidget { final bool cropBorders; final List datas; + final Function(UChapDataPreload datas) onLongPressData; final Function(double) scale; final BackgroundColor backgroundColor; final Function(bool) isFailedToLoadImage; @@ -21,6 +22,7 @@ class DoubleColummView extends StatefulWidget { {super.key, required this.datas, required this.scale, + required this.onLongPressData, required this.backgroundColor, required this.isFailedToLoadImage, required this.cropBorders}); @@ -199,6 +201,8 @@ class _DoubleColummViewState extends State return null; }, cropBorders: widget.cropBorders, + onLongPressData: (datas) => + widget.onLongPressData.call(datas), ), ), // if (widget.datas[1] != null) const SizedBox(width: 10), @@ -276,6 +280,8 @@ class _DoubleColummViewState extends State return null; }, cropBorders: widget.cropBorders, + onLongPressData: (datas) => + widget.onLongPressData.call(datas), ), ), ], diff --git a/lib/modules/manga/reader/double_columm_view_vertical.dart b/lib/modules/manga/reader/double_columm_view_vertical.dart index e33da8db..4726789f 100644 --- a/lib/modules/manga/reader/double_columm_view_vertical.dart +++ b/lib/modules/manga/reader/double_columm_view_vertical.dart @@ -11,6 +11,7 @@ import 'package:mangayomi/utils/extensions/build_context_extensions.dart'; class DoubleColummVerticalView extends StatelessWidget { final bool cropBorders; final List datas; + final Function(UChapDataPreload datas) onLongPressData; final Function(double) scale; final BackgroundColor backgroundColor; final Function(bool) isFailedToLoadImage; @@ -18,6 +19,7 @@ class DoubleColummVerticalView extends StatelessWidget { {super.key, required this.datas, required this.scale, + required this.onLongPressData, required this.backgroundColor, required this.isFailedToLoadImage, required this.cropBorders}); @@ -103,6 +105,7 @@ class DoubleColummVerticalView extends StatelessWidget { return null; }, cropBorders: cropBorders, + onLongPressData: (datas) => onLongPressData.call(datas), ), ), // if (datas[1] != null) const SizedBox(width: 10), @@ -174,6 +177,7 @@ class DoubleColummVerticalView extends StatelessWidget { return null; }, cropBorders: cropBorders, + onLongPressData: (datas) => onLongPressData.call(datas), ), ), ], diff --git a/lib/modules/manga/reader/image_view_center.dart b/lib/modules/manga/reader/image_view_center.dart index a7cfdcea..ea215f34 100644 --- a/lib/modules/manga/reader/image_view_center.dart +++ b/lib/modules/manga/reader/image_view_center.dart @@ -12,6 +12,7 @@ import 'package:mangayomi/utils/reg_exp_matcher.dart'; class ImageViewCenter extends ConsumerWidget { final UChapDataPreload datas; final bool cropBorders; + final Function(UChapDataPreload datas) onLongPressData; final Widget? Function(ExtendedImageState state) loadStateChanged; final Function(ExtendedImageGestureState state)? onDoubleTap; final GestureConfig Function(ExtendedImageState state)? @@ -20,6 +21,7 @@ class ImageViewCenter extends ConsumerWidget { super.key, required this.datas, required this.cropBorders, + required this.onLongPressData, required this.loadStateChanged, this.onDoubleTap, this.initGestureConfigHandler, @@ -47,15 +49,18 @@ class ImageViewCenter extends ConsumerWidget { source: datas.chapter!.manga.value!.source!, lang: datas.chapter!.manga.value!.lang!))); - return ExtendedImage( - image: image as ImageProvider, - fit: getBoxFit(scaleType), - filterQuality: FilterQuality.medium, - enableMemoryCache: true, - mode: ExtendedImageMode.gesture, - handleLoadingProgress: true, - loadStateChanged: loadStateChanged, - initGestureConfigHandler: initGestureConfigHandler, - onDoubleTap: onDoubleTap); + return GestureDetector( + onLongPress: () => onLongPressData.call(datas), + child: ExtendedImage( + image: image as ImageProvider, + fit: getBoxFit(scaleType), + filterQuality: FilterQuality.medium, + enableMemoryCache: true, + mode: ExtendedImageMode.gesture, + handleLoadingProgress: true, + loadStateChanged: loadStateChanged, + initGestureConfigHandler: initGestureConfigHandler, + onDoubleTap: onDoubleTap), + ); } } diff --git a/lib/modules/manga/reader/image_view_vertical.dart b/lib/modules/manga/reader/image_view_vertical.dart index 59c91e1b..a3dd6d42 100644 --- a/lib/modules/manga/reader/image_view_vertical.dart +++ b/lib/modules/manga/reader/image_view_vertical.dart @@ -14,6 +14,7 @@ import 'package:mangayomi/modules/manga/reader/widgets/circular_progress_indicat class ImageViewVertical extends ConsumerWidget { final UChapDataPreload datas; + final Function(UChapDataPreload datas) onLongPressData; final bool cropBorders; final Function(bool) failedToLoadImage; @@ -21,6 +22,7 @@ class ImageViewVertical extends ConsumerWidget { const ImageViewVertical( {super.key, required this.datas, + required this.onLongPressData, required this.cropBorders, required this.failedToLoadImage}); @@ -47,79 +49,83 @@ class ImageViewVertical extends ConsumerWidget { lang: datas.chapter!.manga.value!.lang!))); final scaleType = ref.watch(scaleTypeStateProvider); final l10n = l10nLocalizations(context)!; - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (datas.index == 0) - SizedBox( - height: MediaQuery.of(context).padding.top, - ), - ExtendedImage( - image: image as ImageProvider, - filterQuality: FilterQuality.medium, - handleLoadingProgress: true, - fit: getBoxFit(scaleType), - enableMemoryCache: true, - enableLoadState: true, - loadStateChanged: (state) { - if (state.extendedImageLoadState == LoadState.completed) { - failedToLoadImage(false); - } - if (state.extendedImageLoadState == LoadState.loading) { - final ImageChunkEvent? loadingProgress = state.loadingProgress; - final double progress = - loadingProgress?.expectedTotalBytes != null - ? loadingProgress!.cumulativeBytesLoaded / - loadingProgress.expectedTotalBytes! - : 0; - return Container( - color: Colors.black, - height: context.mediaHeight(0.8), - child: CircularProgressIndicatorAnimateRotate( - progress: progress), - ); - } - if (state.extendedImageLoadState == LoadState.failed) { - failedToLoadImage(true); - return Container( + return GestureDetector( + onLongPress: () => onLongPressData.call(datas), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (datas.index == 0) + SizedBox( + height: MediaQuery.of(context).padding.top, + ), + ExtendedImage( + image: image as ImageProvider, + filterQuality: FilterQuality.medium, + handleLoadingProgress: true, + fit: getBoxFit(scaleType), + enableMemoryCache: true, + enableLoadState: true, + loadStateChanged: (state) { + if (state.extendedImageLoadState == LoadState.completed) { + failedToLoadImage(false); + } + if (state.extendedImageLoadState == LoadState.loading) { + final ImageChunkEvent? loadingProgress = + state.loadingProgress; + final double progress = + loadingProgress?.expectedTotalBytes != null + ? loadingProgress!.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : 0; + return Container( color: Colors.black, height: context.mediaHeight(0.8), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(l10n.image_loading_error, - style: TextStyle( - color: Colors.white.withOpacity(0.7))), - Padding( - padding: const EdgeInsets.all(8.0), - child: GestureDetector( - onLongPress: () { - state.reLoadImage(); - failedToLoadImage(false); - }, - onTap: () { - state.reLoadImage(); - failedToLoadImage(false); - }, - child: Container( - decoration: BoxDecoration( - color: context.primaryColor, - borderRadius: BorderRadius.circular(30)), - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, horizontal: 16), - child: Text( - l10n.retry, + child: CircularProgressIndicatorAnimateRotate( + progress: progress), + ); + } + if (state.extendedImageLoadState == LoadState.failed) { + failedToLoadImage(true); + return Container( + color: Colors.black, + height: context.mediaHeight(0.8), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(l10n.image_loading_error, + style: TextStyle( + color: Colors.white.withOpacity(0.7))), + Padding( + padding: const EdgeInsets.all(8.0), + child: GestureDetector( + onLongPress: () { + state.reLoadImage(); + failedToLoadImage(false); + }, + onTap: () { + state.reLoadImage(); + failedToLoadImage(false); + }, + child: Container( + decoration: BoxDecoration( + color: context.primaryColor, + borderRadius: BorderRadius.circular(30)), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, horizontal: 16), + child: Text( + l10n.retry, + ), ), - ), - )), - ), - ], - )); - } - return null; - }), - ], + )), + ), + ], + )); + } + return null; + }), + ], + ), ); } } diff --git a/lib/modules/manga/reader/providers/crop_borders_provider.dart b/lib/modules/manga/reader/providers/crop_borders_provider.dart index 70577b38..57bca15b 100644 --- a/lib/modules/manga/reader/providers/crop_borders_provider.dart +++ b/lib/modules/manga/reader/providers/crop_borders_provider.dart @@ -1,10 +1,8 @@ import 'dart:async'; -import 'dart:io'; -import 'package:extended_image/extended_image.dart'; import 'package:flutter/foundation.dart'; import 'package:mangayomi/messages/crop_borders.pb.dart'; import 'package:mangayomi/modules/manga/reader/reader_view.dart'; -import 'package:mangayomi/utils/reg_exp_matcher.dart'; +import 'package:mangayomi/utils/extensions/others.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'crop_borders_provider.g.dart'; @@ -16,18 +14,8 @@ Future cropBorders(CropBordersRef ref, Uint8List? imageBytes; if (cropBorder) { - if (datas.archiveImage != null) { - imageBytes = datas.archiveImage; - } else if (datas.isLocale!) { - imageBytes = File('${datas.path!.path}${padIndex(datas.index! + 1)}.jpg') - .readAsBytesSync(); - } else { - File? cachedImage; - if (datas.url != null) { - cachedImage = await getCachedImageFile(datas.url!); - } - imageBytes = cachedImage?.readAsBytesSync(); - } + imageBytes = await datas.getImageBytes; + if (imageBytes == null) { return null; } diff --git a/lib/modules/manga/reader/reader_view.dart b/lib/modules/manga/reader/reader_view.dart index fac3cdb4..c6707b44 100644 --- a/lib/modules/manga/reader/reader_view.dart +++ b/lib/modules/manga/reader/reader_view.dart @@ -9,9 +9,11 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:mangayomi/eval/model/m_bridge.dart'; import 'package:mangayomi/main.dart'; import 'package:mangayomi/messages/generated.dart'; import 'package:mangayomi/models/chapter.dart'; +import 'package:mangayomi/models/manga.dart'; import 'package:mangayomi/models/settings.dart'; import 'package:mangayomi/modules/anime/widgets/desktop.dart'; import 'package:mangayomi/modules/manga/reader/double_columm_view_vertical.dart'; @@ -19,10 +21,12 @@ import 'package:mangayomi/modules/manga/reader/double_columm_view_center.dart'; import 'package:mangayomi/modules/manga/reader/providers/crop_borders_provider.dart'; import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart'; import 'package:mangayomi/providers/l10n_providers.dart'; +import 'package:mangayomi/providers/storage_provider.dart'; import 'package:mangayomi/sources/utils/utils.dart'; import 'package:mangayomi/modules/manga/reader/providers/push_router.dart'; import 'package:mangayomi/services/get_chapter_pages.dart'; import 'package:mangayomi/utils/extensions/build_context_extensions.dart'; +import 'package:mangayomi/utils/extensions/others.dart'; import 'package:mangayomi/utils/headers.dart'; import 'package:mangayomi/modules/manga/reader/image_view_center.dart'; import 'package:mangayomi/modules/manga/reader/image_view_vertical.dart'; @@ -34,6 +38,7 @@ import 'package:mangayomi/utils/reg_exp_matcher.dart'; import 'package:photo_view/photo_view.dart'; import 'package:photo_view/photo_view_gallery.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; +import 'package:share_plus/share_plus.dart'; import 'package:window_manager/window_manager.dart'; typedef DoubleClickAnimationListener = void Function(); @@ -241,6 +246,134 @@ class _MangaChapterPageGalleryState ref.read(fullScreenReaderStateProvider.notifier).set(!value!); } + void _onLongPressImageDialog( + UChapDataPreload datas, BuildContext context) async { + Widget button(String label, IconData icon, Function() onPressed) => + Expanded( + child: Padding( + padding: const EdgeInsets.all(15), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.transparent, + elevation: 0, + shadowColor: Colors.transparent), + onPressed: onPressed, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(4), + child: Icon(icon), + ), + Text(label) + ], + )), + ), + ); + final imageBytes = await datas.getImageBytes; + if (imageBytes != null && context.mounted) { + final name = + "${widget.chapter.manga.value!.name} ${widget.chapter.name} - ${datas.pageIndex}" + .replaceAll(RegExp(r'[^a-zA-Z0-9 .()\-\s]'), '_'); + showModalBottomSheet( + context: context, + constraints: BoxConstraints( + maxWidth: context.mediaWidth(1), + ), + builder: (context) { + return SizedBox( + height: 120, + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20)), + color: context.themeData.scaffoldBackgroundColor), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Container( + height: 7, + width: 35, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + color: context.secondaryColor.withOpacity(0.4)), + ), + ), + Row( + children: [ + button(context.l10n.set_as_cover, Icons.image_outlined, + () async { + final res = await showDialog( + context: context, + builder: (context) { + return AlertDialog( + content: + Text(context.l10n.use_this_as_cover_art), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: Text(context.l10n.cancel)), + const SizedBox( + width: 15, + ), + TextButton( + onPressed: () { + final manga = + widget.chapter.manga.value!; + isar.writeTxnSync(() { + isar.mangas.putSync(manga + ..customCoverImage = + imageBytes); + }); + if (mounted) { + Navigator.pop(context, "ok"); + } + }, + child: Text(context.l10n.ok)), + ], + ) + ], + ); + }); + if (res != null && res == "ok" && context.mounted) { + Navigator.pop(context); + botToast(context.l10n.cover_updated, second: 3); + } + }), + button(context.l10n.share, Icons.share_outlined, + () async { + await Share.shareXFiles([ + XFile.fromData(imageBytes, + name: name, mimeType: 'image/png') + ]); + }), + button(context.l10n.save, Icons.save_outlined, () async { + final dir = + await StorageProvider().getGalleryDirectory(); + final file = File("${dir!.path}/$name.png"); + file.writeAsBytesSync(imageBytes); + if (context.mounted) { + botToast(context.l10n.picture_saved, second: 3); + } + }), + ], + ), + ], + ), + ), + ); + }, + ); + } + } + @override Widget build(BuildContext context) { final backgroundColor = ref.watch(backgroundColorStateProvider); @@ -404,6 +537,10 @@ class _MangaChapterPageGalleryState backgroundColor, isFailedToLoadImage: (val) {}, cropBorders: cropBorders, + onLongPressData: (datas) { + _onLongPressImageDialog( + datas, context); + }, ) : ImageViewVertical( datas: @@ -412,6 +549,10 @@ class _MangaChapterPageGalleryState // _failedToLoadImage.value = value; }, cropBorders: cropBorders, + onLongPressData: (datas) { + _onLongPressImageDialog( + datas, context); + }, ), ); }, @@ -462,6 +603,10 @@ class _MangaChapterPageGalleryState } }, cropBorders: cropBorders, + onLongPressData: (datas) { + _onLongPressImageDialog( + datas, context); + }, ); }, itemCount: @@ -658,6 +803,10 @@ class _MangaChapterPageGalleryState .forward(); }, cropBorders: cropBorders, + onLongPressData: (datas) { + _onLongPressImageDialog( + datas, context); + }, ); }, itemCount: _uChapDataPreload.length, @@ -1666,15 +1815,17 @@ class _MangaChapterPageGalleryState flex: 2, child: GestureDetector( behavior: HitTestBehavior.translucent, - onTap: usePageTapZones - ? () { - if (_isReverseHorizontal) { - _onBtnTapped(_currentIndex! + 1, false); - } else { - _onBtnTapped(_currentIndex! - 1, true); - } - } - : null, + onTap: () { + if (usePageTapZones) { + if (_isReverseHorizontal) { + _onBtnTapped(_currentIndex! + 1, false); + } else { + _onBtnTapped(_currentIndex! - 1, true); + } + } else { + _isViewFunction(); + } + }, onDoubleTapDown: _isVerticalContinous() ? (TapDownDetails details) { _toggleScale(details.globalPosition); @@ -1711,15 +1862,17 @@ class _MangaChapterPageGalleryState flex: 2, child: GestureDetector( behavior: HitTestBehavior.translucent, - onTap: usePageTapZones - ? () { - if (_isReverseHorizontal) { - _onBtnTapped(_currentIndex! - 1, true); - } else { - _onBtnTapped(_currentIndex! + 1, false); - } - } - : null, + onTap: () { + if (usePageTapZones) { + if (_isReverseHorizontal) { + _onBtnTapped(_currentIndex! - 1, true); + } else { + _onBtnTapped(_currentIndex! + 1, false); + } + } else { + _isViewFunction(); + } + }, onDoubleTapDown: _isVerticalContinous() ? (TapDownDetails details) { _toggleScale(details.globalPosition); @@ -1749,7 +1902,7 @@ class _MangaChapterPageGalleryState ? _isViewFunction() : usePageTapZones ? _onBtnTapped(_currentIndex! - 1, true) - : null; + : _isViewFunction(); }, onDoubleTapDown: _isVerticalContinous() ? (TapDownDetails details) { @@ -1773,7 +1926,7 @@ class _MangaChapterPageGalleryState ? _isViewFunction() : usePageTapZones ? _onBtnTapped(_currentIndex! + 1, false) - : null; + : _isViewFunction(); }, onDoubleTapDown: _isVerticalContinous() ? (TapDownDetails details) { diff --git a/lib/modules/more/backup_and_restore/providers/backup.dart b/lib/modules/more/backup_and_restore/providers/backup.dart index 3e0bb8c3..7da9f17f 100644 --- a/lib/modules/more/backup_and_restore/providers/backup.dart +++ b/lib/modules/more/backup_and_restore/providers/backup.dart @@ -126,6 +126,10 @@ void doBackUp(DoBackUpRef ref, encoder.addFile(File(backupFilePath)); encoder.close(); Directory(backupFilePath).deleteSync(recursive: true); + final assets = [ + 'assets/app_icons/icon-black.png', + 'assets/app_icons/icon-red.png' + ]; if (context != null) { Navigator.pop(context); BotToast.showNotification( @@ -133,8 +137,7 @@ void doBackUp(DoBackUpRef ref, animationReverseDuration: const Duration(milliseconds: 200), duration: const Duration(seconds: 5), backButtonBehavior: BackButtonBehavior.none, - leading: (cancel) => - Image.asset('assets/app_icons/icon-red.png', height: 40), + leading: (_) => Image.asset((assets..shuffle()).first, height: 25), title: (_) => const Text( "Backup created!", style: TextStyle(fontWeight: FontWeight.bold), diff --git a/lib/providers/storage_provider.dart b/lib/providers/storage_provider.dart index e5ba290a..806d7d59 100644 --- a/lib/providers/storage_provider.dart +++ b/lib/providers/storage_provider.dart @@ -20,7 +20,7 @@ class StorageProvider { final RegExp _regExpChar = RegExp(r'[^a-zA-Z0-9 .()\-\s]'); Future requestPermission() async { Permission permission = Permission.manageExternalStorage; - if (Platform.isAndroid || Platform.isIOS) { + if (Platform.isAndroid) { if (await permission.isGranted) { return true; } else { @@ -105,6 +105,17 @@ class StorageProvider { } } + Future getGalleryDirectory() async { + String gPath = (await getDirectory())!.path; + if (Platform.isAndroid) { + gPath = "/storage/emulated/0/Pictures/Mangayomi/"; + } else { + gPath = path.join(gPath, 'Pictures'); + } + await Directory(gPath).create(recursive: true); + return Directory(gPath); + } + Future initDB(String? path, {bool? inspector = false}) async { Directory? dir; if (path == null) { diff --git a/lib/utils/extensions/others.dart b/lib/utils/extensions/others.dart index acbd466e..87a9a992 100644 --- a/lib/utils/extensions/others.dart +++ b/lib/utils/extensions/others.dart @@ -1,5 +1,53 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; +import 'dart:ui'; +import 'package:extended_image/extended_image.dart'; +import 'package:flutter/material.dart'; +import 'package:mangayomi/modules/manga/reader/reader_view.dart'; +import 'package:mangayomi/utils/reg_exp_matcher.dart'; + extension LetExtension on T { R let(R Function(T) block) { return block(this); } } + +extension ImageProviderExtension on ImageProvider { + Future getBytes(BuildContext context, + {ImageByteFormat format = ImageByteFormat.png}) async { + final imageStream = resolve(createLocalImageConfiguration(context)); + final Completer completer = Completer(); + final ImageStreamListener listener = ImageStreamListener( + (imageInfo, synchronousCall) async { + final bytes = await imageInfo.image.toByteData(format: format); + if (!completer.isCompleted) { + completer.complete(bytes?.buffer.asUint8List()); + } + }, + ); + imageStream.addListener(listener); + final imageBytes = await completer.future; + imageStream.removeListener(listener); + return imageBytes; + } +} + +extension UChapDataPreloadExtensions on UChapDataPreload { + Future get getImageBytes async { + Uint8List? imageBytes; + if (archiveImage != null) { + imageBytes = archiveImage; + } else if (isLocale!) { + imageBytes = + File('${path!.path}${padIndex(index! + 1)}.jpg').readAsBytesSync(); + } else { + File? cachedImage; + if (url != null) { + cachedImage = await getCachedImageFile(url!); + } + imageBytes = cachedImage?.readAsBytesSync(); + } + return imageBytes; + } +} diff --git a/lib/utils/headers.dart b/lib/utils/headers.dart index 049a5e8b..67368d1a 100644 --- a/lib/utils/headers.dart +++ b/lib/utils/headers.dart @@ -8,14 +8,14 @@ part 'headers.g.dart'; Map headers(HeadersRef ref, {required String source, required String lang}) { final mSource = getSource(lang, source); - + if (mSource == null) return {}; Map headers = {}; - if (mSource?.headers?.isNotEmpty ?? false) { - headers = (jsonDecode(mSource!.headers!) as Map) + if (mSource.headers?.isNotEmpty ?? false) { + headers = (jsonDecode(mSource.headers!) as Map) .map((key, value) => MapEntry(key.toString(), value.toString())); } - final cookies = MInterceptor.getCookiesPref(mSource!.baseUrl!); + final cookies = MInterceptor.getCookiesPref(mSource.baseUrl!); headers.addAll(cookies); return headers;