feat: add more options to manage covers

This commit is contained in:
kodjomoustapha 2024-02-16 19:31:06 +01:00
parent 94ebf47e4e
commit f71c40f0f7
26 changed files with 680 additions and 281 deletions

View file

@ -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) {

View file

@ -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": "تم تحديث الغلاف"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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": "Обложка обновлена"
}

View file

@ -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"
}

View file

@ -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": "封面已更新"
}

View file

@ -29,7 +29,8 @@ class DesktopControllerWidget extends StatefulWidget {
required this.tempDuration});
@override
State<DesktopControllerWidget> createState() => _DesktopControllerWidgetState();
State<DesktopControllerWidget> createState() =>
_DesktopControllerWidgetState();
}
class _DesktopControllerWidgetState extends State<DesktopControllerWidget> {
@ -797,13 +798,16 @@ class _CustomMaterialDesktopFullscreenButtonState
Future<bool> 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();

View file

@ -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<MangaDetailView>
);
}
_openImage(ImageProvider imageProvider) {
void _openImage(ImageProvider imageProvider) {
showDialog(
context: context,
builder: (context) {
@ -1610,141 +1613,221 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
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<TrackPreference>? 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<TrackPreference>? 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<int>(
value: 0,
child: Text(context.l10n.delete)),
PopupMenuItem<int>(
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<int>(
value: 0,
child:
Text(context.l10n.delete)),
PopupMenuItem<int>(
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,
)),
),
],
),
),
),
],
),
),
],
),

View file

@ -14,6 +14,7 @@ import 'package:photo_view/photo_view_gallery.dart';
class DoubleColummView extends StatefulWidget {
final bool cropBorders;
final List<UChapDataPreload?> 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<DoubleColummView>
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<DoubleColummView>
return null;
},
cropBorders: widget.cropBorders,
onLongPressData: (datas) =>
widget.onLongPressData.call(datas),
),
),
],

View file

@ -11,6 +11,7 @@ import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
class DoubleColummVerticalView extends StatelessWidget {
final bool cropBorders;
final List<UChapDataPreload?> 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),
),
),
],

View file

@ -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<Object>,
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<Object>,
fit: getBoxFit(scaleType),
filterQuality: FilterQuality.medium,
enableMemoryCache: true,
mode: ExtendedImageMode.gesture,
handleLoadingProgress: true,
loadStateChanged: loadStateChanged,
initGestureConfigHandler: initGestureConfigHandler,
onDoubleTap: onDoubleTap),
);
}
}

View file

@ -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<Object>,
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<Object>,
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;
}),
],
),
);
}
}

View file

@ -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<Uint8List?> 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;
}

View file

@ -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) {

View file

@ -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),

View file

@ -20,7 +20,7 @@ class StorageProvider {
final RegExp _regExpChar = RegExp(r'[^a-zA-Z0-9 .()\-\s]');
Future<bool> 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<Directory?> 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<Isar> initDB(String? path, {bool? inspector = false}) async {
Directory? dir;
if (path == null) {

View file

@ -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<T> on T {
R let<R>(R Function(T) block) {
return block(this);
}
}
extension ImageProviderExtension on ImageProvider {
Future<Uint8List?> getBytes(BuildContext context,
{ImageByteFormat format = ImageByteFormat.png}) async {
final imageStream = resolve(createLocalImageConfiguration(context));
final Completer<Uint8List?> completer = Completer<Uint8List?>();
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<Uint8List?> 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;
}
}

View file

@ -8,14 +8,14 @@ part 'headers.g.dart';
Map<String, String> headers(HeadersRef ref,
{required String source, required String lang}) {
final mSource = getSource(lang, source);
if (mSource == null) return {};
Map<String, String> 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;