mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-04-21 07:41:58 +00:00
430 lines
17 KiB
Dart
430 lines
17 KiB
Dart
import 'dart:typed_data';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:isar_community/isar.dart';
|
|
import 'package:mangayomi/models/chapter.dart';
|
|
import 'package:mangayomi/models/manga.dart';
|
|
import 'package:mangayomi/modules/calendar/providers/calendar_provider.dart';
|
|
import 'package:mangayomi/modules/widgets/custom_extended_image_provider.dart';
|
|
import 'package:mangayomi/modules/widgets/custom_sliver_grouped_list_view.dart';
|
|
import 'package:mangayomi/modules/widgets/progress_center.dart';
|
|
import 'package:mangayomi/providers/l10n_providers.dart';
|
|
import 'package:mangayomi/utils/constant.dart';
|
|
import 'package:mangayomi/utils/date.dart';
|
|
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
|
|
import 'package:mangayomi/utils/headers.dart';
|
|
import 'package:table_calendar/table_calendar.dart';
|
|
|
|
class CalendarScreen extends ConsumerStatefulWidget {
|
|
final ItemType? itemType;
|
|
const CalendarScreen({super.key, this.itemType});
|
|
|
|
@override
|
|
ConsumerState<CalendarScreen> createState() => _CalendarScreenState();
|
|
}
|
|
|
|
class _CalendarScreenState extends ConsumerState<CalendarScreen> {
|
|
late final ValueNotifier<List<Manga>> _selectedEntries;
|
|
CalendarFormat _calendarFormat = CalendarFormat.month;
|
|
RangeSelectionMode _rangeSelectionMode = RangeSelectionMode.toggledOff;
|
|
final firstDay = DateTime.now();
|
|
final lastDay = DateTime.now().add(const Duration(days: 1000));
|
|
DateTime _focusedDay = DateTime.now();
|
|
DateTime? _selectedDay;
|
|
DateTime? _rangeStart;
|
|
DateTime? _rangeEnd;
|
|
late ItemType? itemType = widget.itemType ?? ItemType.manga;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_selectedDay = _focusedDay;
|
|
_selectedEntries = ValueNotifier([]);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_selectedEntries.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final l10n = context.l10n;
|
|
final locale = ref.watch(l10nLocaleStateProvider);
|
|
final data = ref.watch(getCalendarStreamProvider(itemType: itemType));
|
|
return Scaffold(
|
|
appBar: AppBar(title: Text(l10n.calendar)),
|
|
body: data.when(
|
|
data: (data) {
|
|
if (_selectedDay != null) {
|
|
_selectedEntries.value = _getEntriesForDay(_selectedDay!, data);
|
|
}
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 15),
|
|
child: CustomScrollView(
|
|
slivers: [
|
|
SliverToBoxAdapter(
|
|
child: Column(
|
|
children: [
|
|
ListTile(
|
|
title: Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 3),
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
Icons.warning_amber_outlined,
|
|
color: context.secondaryColor,
|
|
),
|
|
const SizedBox(width: 10),
|
|
Flexible(
|
|
child: Text(
|
|
l10n.calendar_info,
|
|
softWrap: true,
|
|
overflow: TextOverflow.clip,
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
color: context.secondaryColor,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 15),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: SegmentedButton(
|
|
emptySelectionAllowed: true,
|
|
showSelectedIcon: false,
|
|
style: TextButton.styleFrom(
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(50),
|
|
),
|
|
),
|
|
segments: [
|
|
ButtonSegment(
|
|
value: ItemType.manga.index,
|
|
label: Padding(
|
|
padding: const EdgeInsets.all(12),
|
|
child: Text(l10n.manga),
|
|
),
|
|
),
|
|
ButtonSegment(
|
|
value: ItemType.anime.index,
|
|
label: Padding(
|
|
padding: const EdgeInsets.all(12),
|
|
child: Text(l10n.anime),
|
|
),
|
|
),
|
|
ButtonSegment(
|
|
value: ItemType.novel.index,
|
|
label: Padding(
|
|
padding: const EdgeInsets.all(12),
|
|
child: Text(l10n.novel),
|
|
),
|
|
),
|
|
],
|
|
selected: {itemType?.index},
|
|
onSelectionChanged: (newSelection) {
|
|
if (newSelection.isNotEmpty &&
|
|
newSelection.first != null) {
|
|
setState(() {
|
|
itemType =
|
|
ItemType.values[newSelection.first!];
|
|
});
|
|
}
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
TableCalendar(
|
|
firstDay: firstDay,
|
|
lastDay: lastDay,
|
|
focusedDay: _focusedDay,
|
|
locale: locale.toLanguageTag(),
|
|
selectedDayPredicate: (day) =>
|
|
isSameDay(_selectedDay, day),
|
|
rangeStartDay: _rangeStart,
|
|
rangeEndDay: _rangeEnd,
|
|
calendarFormat: _calendarFormat,
|
|
rangeSelectionMode: _rangeSelectionMode,
|
|
eventLoader: (day) => _getEntriesForDay(day, data),
|
|
startingDayOfWeek: StartingDayOfWeek.monday,
|
|
calendarStyle: CalendarStyle(
|
|
outsideDaysVisible: true,
|
|
weekendTextStyle: TextStyle(
|
|
color: context.primaryColor,
|
|
),
|
|
),
|
|
onDaySelected: (selectedDay, focusedDay) =>
|
|
_onDaySelected(selectedDay, focusedDay, data),
|
|
onRangeSelected: (start, end, focusedDay) =>
|
|
_onRangeSelected(start, end, focusedDay, data),
|
|
onFormatChanged: (format) {
|
|
if (_calendarFormat != format) {
|
|
setState(() {
|
|
_calendarFormat = format;
|
|
});
|
|
}
|
|
},
|
|
onPageChanged: (focusedDay) {
|
|
_focusedDay = focusedDay;
|
|
},
|
|
),
|
|
const SizedBox(height: 15),
|
|
],
|
|
),
|
|
),
|
|
ValueListenableBuilder<List<Manga>>(
|
|
valueListenable: _selectedEntries,
|
|
builder: (context, value, _) {
|
|
return CustomSliverGroupedListView<Manga, String>(
|
|
elements: value,
|
|
groupBy: (element) {
|
|
return dateFormat(
|
|
_selectedDay?.millisecondsSinceEpoch.toString() ??
|
|
DateTime.now()
|
|
.add(Duration(days: element.smartUpdateDays!))
|
|
.millisecondsSinceEpoch
|
|
.toString(),
|
|
context: context,
|
|
ref: ref,
|
|
forHistoryValue: true,
|
|
useRelativeTimesTamps: false,
|
|
);
|
|
},
|
|
groupSeparatorBuilder: (String groupByValue) => Padding(
|
|
padding: const EdgeInsets.only(bottom: 8, left: 12),
|
|
child: Row(
|
|
children: [
|
|
Text(
|
|
"${dateFormat(null, context: context, stringDate: groupByValue, ref: ref, useRelativeTimesTamps: true, showInDaysFuture: true)} - ${dateFormat(null, context: context, stringDate: groupByValue, ref: ref, useRelativeTimesTamps: false)}",
|
|
),
|
|
],
|
|
),
|
|
),
|
|
itemBuilder: (context, element) {
|
|
return CalendarListTileWidget(
|
|
manga: element,
|
|
selectedDay: _selectedDay,
|
|
);
|
|
},
|
|
order: GroupedListOrder.ASC,
|
|
);
|
|
},
|
|
),
|
|
SliverToBoxAdapter(child: const SizedBox(height: 15)),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
error: (Object error, StackTrace stackTrace) {
|
|
return Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Text(l10n.calendar_no_data, textAlign: TextAlign.center),
|
|
),
|
|
);
|
|
},
|
|
loading: () {
|
|
return const ProgressCenter();
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
List<Manga> _getEntriesForDay(DateTime day, List<Manga> data) {
|
|
return data.where((e) {
|
|
final lastChapter = e.chapters
|
|
.filter()
|
|
.sortByDateUploadDesc()
|
|
.findFirstSync();
|
|
final lastDate = int.tryParse(lastChapter?.dateUpload ?? "");
|
|
final start = lastDate != null
|
|
? DateTime.fromMillisecondsSinceEpoch(lastDate)
|
|
: DateTime.now();
|
|
final temp = start.add(Duration(days: e.smartUpdateDays!));
|
|
final predictedDay = "${temp.year}-${temp.month}-${temp.day}";
|
|
final selectedDay = "${day.year}-${day.month}-${day.day}";
|
|
return predictedDay == selectedDay;
|
|
}).toList();
|
|
}
|
|
|
|
List<Manga> _getEntriesForRange(
|
|
DateTime start,
|
|
DateTime end,
|
|
List<Manga> data,
|
|
) {
|
|
final days = _daysInRange(start, end);
|
|
|
|
return [for (final d in days) ..._getEntriesForDay(d, data)];
|
|
}
|
|
|
|
void _onDaySelected(
|
|
DateTime selectedDay,
|
|
DateTime focusedDay,
|
|
List<Manga> data,
|
|
) {
|
|
if (!isSameDay(_selectedDay, selectedDay)) {
|
|
setState(() {
|
|
_selectedDay = selectedDay;
|
|
_focusedDay = focusedDay;
|
|
_rangeStart = null;
|
|
_rangeEnd = null;
|
|
_rangeSelectionMode = RangeSelectionMode.toggledOff;
|
|
});
|
|
|
|
_selectedEntries.value = _getEntriesForDay(selectedDay, data);
|
|
}
|
|
}
|
|
|
|
void _onRangeSelected(
|
|
DateTime? start,
|
|
DateTime? end,
|
|
DateTime focusedDay,
|
|
List<Manga> data,
|
|
) {
|
|
setState(() {
|
|
_selectedDay = null;
|
|
_focusedDay = focusedDay;
|
|
_rangeStart = start;
|
|
_rangeEnd = end;
|
|
_rangeSelectionMode = RangeSelectionMode.toggledOn;
|
|
});
|
|
|
|
if (start != null && end != null) {
|
|
_selectedEntries.value = _getEntriesForRange(start, end, data);
|
|
} else if (start != null) {
|
|
_selectedEntries.value = _getEntriesForDay(start, data);
|
|
} else if (end != null) {
|
|
_selectedEntries.value = _getEntriesForDay(end, data);
|
|
}
|
|
}
|
|
|
|
List<DateTime> _daysInRange(DateTime first, DateTime last) {
|
|
final dayCount = last.difference(first).inDays + 1;
|
|
return List.generate(
|
|
dayCount,
|
|
(index) => DateTime.utc(first.year, first.month, first.day + index),
|
|
);
|
|
}
|
|
}
|
|
|
|
class CalendarListTileWidget extends ConsumerWidget {
|
|
final Manga manga;
|
|
final DateTime? selectedDay;
|
|
const CalendarListTileWidget({
|
|
required this.manga,
|
|
required this.selectedDay,
|
|
super.key,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
return Material(
|
|
borderRadius: BorderRadius.circular(5),
|
|
color: Colors.transparent,
|
|
clipBehavior: Clip.antiAliasWithSaveLayer,
|
|
child: InkWell(
|
|
onTap: () => context.push('/manga-reader/detail', extra: manga.id),
|
|
onLongPress: () {},
|
|
onSecondaryTap: () {},
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 13, vertical: 5),
|
|
child: Container(
|
|
height: 45,
|
|
decoration: BoxDecoration(borderRadius: BorderRadius.circular(5)),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Expanded(
|
|
child: Row(
|
|
children: [
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(5),
|
|
child: Material(
|
|
child: GestureDetector(
|
|
onTap: () {
|
|
context.push(
|
|
'/manga-reader/detail',
|
|
extra: manga.id,
|
|
);
|
|
},
|
|
child: Ink.image(
|
|
fit: BoxFit.cover,
|
|
width: 40,
|
|
height: 45,
|
|
image: manga.customCoverImage != null
|
|
? MemoryImage(
|
|
manga.customCoverImage as Uint8List,
|
|
)
|
|
as ImageProvider
|
|
: CustomExtendedNetworkImageProvider(
|
|
toImgUrl(
|
|
manga.customCoverFromTracker ??
|
|
manga.imageUrl!,
|
|
),
|
|
headers: ref.watch(
|
|
headersProvider(
|
|
source: manga.source!,
|
|
lang: manga.lang!,
|
|
sourceId: manga.sourceId,
|
|
),
|
|
),
|
|
),
|
|
child: InkWell(child: Container()),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
manga.name!,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: Theme.of(
|
|
context,
|
|
).textTheme.bodyLarge!.color,
|
|
),
|
|
),
|
|
Text(
|
|
context.l10n.n_chapters(
|
|
manga.chapters.countSync(),
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
fontStyle: FontStyle.italic,
|
|
color: context.secondaryColor,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|