improved calendar

This commit is contained in:
Schnitzel5 2025-08-06 17:48:32 +02:00
parent 715f077c13
commit e565d14ef3
7 changed files with 309 additions and 126 deletions

View file

@ -16,7 +16,8 @@ import 'package:mangayomi/utils/headers.dart';
import 'package:table_calendar/table_calendar.dart';
class CalendarScreen extends ConsumerStatefulWidget {
const CalendarScreen({super.key});
final ItemType? itemType;
const CalendarScreen({super.key, this.itemType});
@override
ConsumerState<CalendarScreen> createState() => _CalendarScreenState();
@ -32,6 +33,7 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
DateTime? _selectedDay;
DateTime? _rangeStart;
DateTime? _rangeEnd;
late ItemType? itemType = widget.itemType ?? ItemType.manga;
@override
void initState() {
@ -50,129 +52,169 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
Widget build(BuildContext context) {
final l10n = context.l10n;
final locale = ref.watch(l10nLocaleStateProvider);
final data = ref.watch(getCalendarStreamProvider);
final data = ref.watch(getCalendarStreamProvider(itemType: itemType));
return Scaffold(
appBar: AppBar(title: Text(l10n.calendar)),
body: data.when(
data: (data) {
if (data.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(l10n.calendar_no_data, textAlign: TextAlign.center),
),
);
}
if (_selectedDay != null) {
_selectedEntries.value = _getEntriesForDay(_selectedDay!, data);
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
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),
Text(
l10n.calendar_info,
softWrap: true,
style: TextStyle(
fontSize: 13,
color: context.secondaryColor,
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),
Text(
l10n.calendar_info,
softWrap: true,
style: TextStyle(
fontSize: 13,
color: context.secondaryColor,
),
),
],
),
),
],
),
),
),
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: const CalendarStyle(outsideDaysVisible: false),
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: 8.0),
Expanded(
child: ValueListenableBuilder<List<Manga>>(
valueListenable: _selectedEntries,
builder: (context, value, _) {
return CustomScrollView(
slivers: [
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)}",
),
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!];
});
}
},
),
),
itemBuilder: (context, element) {
return CalendarListTileWidget(
manga: element,
selectedDay: _selectedDay,
);
},
order: GroupedListOrder.ASC,
),
],
);
},
],
),
),
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)),
],
),
);

View file

@ -6,10 +6,14 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
part 'calendar_provider.g.dart';
@riverpod
Stream<List<Manga>> getCalendarStream(Ref ref) async* {
Stream<List<Manga>> getCalendarStream(
Ref ref, {
ItemType? itemType,
}) async* {
yield* isar.mangas
.filter()
.idIsNotNull()
.itemTypeEqualTo(itemType ?? ItemType.manga)
.anyOf([
Status.ongoing,
Status.unknown,

View file

@ -6,23 +6,156 @@ part of 'calendar_provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$getCalendarStreamHash() => r'9095ecdef36259f84eb6562a2ca9e253f50d4b8d';
String _$getCalendarStreamHash() => r'dcdad165b2da2420bafa8b70c4b3a0fb336e5021';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
/// See also [getCalendarStream].
@ProviderFor(getCalendarStream)
final getCalendarStreamProvider =
AutoDisposeStreamProvider<List<Manga>>.internal(
getCalendarStream,
name: r'getCalendarStreamProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$getCalendarStreamHash,
dependencies: null,
allTransitiveDependencies: null,
);
const getCalendarStreamProvider = GetCalendarStreamFamily();
/// See also [getCalendarStream].
class GetCalendarStreamFamily extends Family<AsyncValue<List<Manga>>> {
/// See also [getCalendarStream].
const GetCalendarStreamFamily();
/// See also [getCalendarStream].
GetCalendarStreamProvider call({
ItemType? itemType,
}) {
return GetCalendarStreamProvider(
itemType: itemType,
);
}
@override
GetCalendarStreamProvider getProviderOverride(
covariant GetCalendarStreamProvider provider,
) {
return call(
itemType: provider.itemType,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'getCalendarStreamProvider';
}
/// See also [getCalendarStream].
class GetCalendarStreamProvider extends AutoDisposeStreamProvider<List<Manga>> {
/// See also [getCalendarStream].
GetCalendarStreamProvider({
ItemType? itemType,
}) : this._internal(
(ref) => getCalendarStream(
ref as GetCalendarStreamRef,
itemType: itemType,
),
from: getCalendarStreamProvider,
name: r'getCalendarStreamProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$getCalendarStreamHash,
dependencies: GetCalendarStreamFamily._dependencies,
allTransitiveDependencies:
GetCalendarStreamFamily._allTransitiveDependencies,
itemType: itemType,
);
GetCalendarStreamProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.itemType,
}) : super.internal();
final ItemType? itemType;
@override
Override overrideWith(
Stream<List<Manga>> Function(GetCalendarStreamRef provider) create,
) {
return ProviderOverride(
origin: this,
override: GetCalendarStreamProvider._internal(
(ref) => create(ref as GetCalendarStreamRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
itemType: itemType,
),
);
}
@override
AutoDisposeStreamProviderElement<List<Manga>> createElement() {
return _GetCalendarStreamProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is GetCalendarStreamProvider && other.itemType == itemType;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, itemType.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef GetCalendarStreamRef = AutoDisposeStreamProviderRef<List<Manga>>;
mixin GetCalendarStreamRef on AutoDisposeStreamProviderRef<List<Manga>> {
/// The parameter `itemType` of this provider.
ItemType? get itemType;
}
class _GetCalendarStreamProviderElement
extends AutoDisposeStreamProviderElement<List<Manga>>
with GetCalendarStreamRef {
_GetCalendarStreamProviderElement(super.provider);
@override
ItemType? get itemType => (origin as GetCalendarStreamProvider).itemType;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View file

@ -1891,7 +1891,8 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
elevation: 0,
),
onPressed: () {},
onPressed: () =>
context.push("/calendarScreen", extra: widget.manga!.itemType),
child: Column(
children: [
Icon(

View file

@ -6,7 +6,7 @@ part of 'update_manga_detail_providers.dart';
// RiverpodGenerator
// **************************************************************************
String _$updateMangaDetailHash() => r'33c6bd0f1de57e2e839ae695a0301893b9a94624';
String _$updateMangaDetailHash() => r'85660b206c2bce558760118936758a261519cad8';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -234,7 +234,10 @@ class RouterNotifier extends ChangeNotifier {
name: "playerAdvancedScreen",
child: const PlayerAdvancedScreen(),
),
_genericRoute(name: "calendarScreen", child: const CalendarScreen()),
_genericRoute<ItemType?>(
name: "calendarScreen",
builder: (itemType) => CalendarScreen(itemType: itemType),
),
_genericRoute<Manga>(
name: "migrate",
builder: (manga) => MigrationScreen(manga: manga),

View file

@ -69,15 +69,15 @@ String dateFormat(
date.isAfter(fiveDaysAgo) ||
date.isAfter(sixDaysAgo) ||
date.isAfter(aWeekAgo)) {
final difference = today.difference(date).inDays;
final difference = today.difference(date).inDays.abs();
return switch (difference) {
1 =>
showInDaysFuture
? l10n.in_n_day(difference.abs())
? l10n.in_n_day(difference)
: l10n.n_day_ago(difference),
!= 7 =>
showInDaysFuture
? l10n.in_n_days(difference.abs())
? l10n.in_n_days(difference)
: l10n.n_days_ago(difference),
_ => l10n.a_week_ago,
};