added filler, thumbnail and description info to chapter list

This commit is contained in:
Schnitzel5 2025-07-25 20:08:51 +02:00
parent 3dae39a86c
commit 6be2775fee
14 changed files with 313 additions and 22 deletions

View file

@ -30,7 +30,6 @@ import 'package:mangayomi/utils/discord_rpc.dart';
import 'package:mangayomi/utils/url_protocol/api.dart';
import 'package:mangayomi/modules/more/settings/appearance/providers/theme_provider.dart';
import 'package:mangayomi/modules/library/providers/file_scanner.dart';
// ignore: depend_on_referenced_packages
import 'package:media_kit/media_kit.dart';
import 'package:path_provider/path_provider.dart';
import 'package:window_manager/window_manager.dart';

View file

@ -49,6 +49,9 @@ class Manga {
String? customCoverFromTracker;
/// only update X days after `lastUpdate`
int? smartUpdateDays;
int? updatedAt;
@Backlink(to: "manga")
@ -76,6 +79,7 @@ class Manga {
this.isLocalArchive = false,
this.customCoverImage,
this.customCoverFromTracker,
this.smartUpdateDays,
this.updatedAt = 0,
});
@ -101,6 +105,7 @@ class Manga {
source = json['source'];
status = Status.values[json['status']];
customCoverFromTracker = json['customCoverFromTracker'];
smartUpdateDays = json['smartUpdateDays'];
updatedAt = json['updatedAt'];
}
@ -125,6 +130,7 @@ class Manga {
'source': source,
'status': status.index,
'customCoverFromTracker': customCoverFromTracker,
'smartUpdateDays': smartUpdateDays,
'updatedAt': updatedAt ?? 0,
};
}

View file

@ -108,19 +108,24 @@ const MangaSchema = CollectionSchema(
name: r'name',
type: IsarType.string,
),
r'source': PropertySchema(
r'smartUpdateDays': PropertySchema(
id: 18,
name: r'smartUpdateDays',
type: IsarType.long,
),
r'source': PropertySchema(
id: 19,
name: r'source',
type: IsarType.string,
),
r'status': PropertySchema(
id: 19,
id: 20,
name: r'status',
type: IsarType.byte,
enumMap: _MangastatusEnumValueMap,
),
r'updatedAt': PropertySchema(
id: 20,
id: 21,
name: r'updatedAt',
type: IsarType.long,
)
@ -258,9 +263,10 @@ void _mangaSerialize(
writer.writeLong(offsets[15], object.lastUpdate);
writer.writeString(offsets[16], object.link);
writer.writeString(offsets[17], object.name);
writer.writeString(offsets[18], object.source);
writer.writeByte(offsets[19], object.status.index);
writer.writeLong(offsets[20], object.updatedAt);
writer.writeLong(offsets[18], object.smartUpdateDays);
writer.writeString(offsets[19], object.source);
writer.writeByte(offsets[20], object.status.index);
writer.writeLong(offsets[21], object.updatedAt);
}
Manga _mangaDeserialize(
@ -290,10 +296,11 @@ Manga _mangaDeserialize(
lastUpdate: reader.readLongOrNull(offsets[15]),
link: reader.readStringOrNull(offsets[16]),
name: reader.readStringOrNull(offsets[17]),
source: reader.readStringOrNull(offsets[18]),
status: _MangastatusValueEnumMap[reader.readByteOrNull(offsets[19])] ??
smartUpdateDays: reader.readLongOrNull(offsets[18]),
source: reader.readStringOrNull(offsets[19]),
status: _MangastatusValueEnumMap[reader.readByteOrNull(offsets[20])] ??
Status.ongoing,
updatedAt: reader.readLongOrNull(offsets[20]),
updatedAt: reader.readLongOrNull(offsets[21]),
);
return object;
}
@ -343,11 +350,13 @@ P _mangaDeserializeProp<P>(
case 17:
return (reader.readStringOrNull(offset)) as P;
case 18:
return (reader.readStringOrNull(offset)) as P;
return (reader.readLongOrNull(offset)) as P;
case 19:
return (reader.readStringOrNull(offset)) as P;
case 20:
return (_MangastatusValueEnumMap[reader.readByteOrNull(offset)] ??
Status.ongoing) as P;
case 20:
case 21:
return (reader.readLongOrNull(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
@ -2591,6 +2600,75 @@ extension MangaQueryFilter on QueryBuilder<Manga, Manga, QFilterCondition> {
});
}
QueryBuilder<Manga, Manga, QAfterFilterCondition> smartUpdateDaysIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
property: r'smartUpdateDays',
));
});
}
QueryBuilder<Manga, Manga, QAfterFilterCondition> smartUpdateDaysIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNotNull(
property: r'smartUpdateDays',
));
});
}
QueryBuilder<Manga, Manga, QAfterFilterCondition> smartUpdateDaysEqualTo(
int? value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'smartUpdateDays',
value: value,
));
});
}
QueryBuilder<Manga, Manga, QAfterFilterCondition> smartUpdateDaysGreaterThan(
int? value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'smartUpdateDays',
value: value,
));
});
}
QueryBuilder<Manga, Manga, QAfterFilterCondition> smartUpdateDaysLessThan(
int? value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'smartUpdateDays',
value: value,
));
});
}
QueryBuilder<Manga, Manga, QAfterFilterCondition> smartUpdateDaysBetween(
int? lower,
int? upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.between(
property: r'smartUpdateDays',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
));
});
}
QueryBuilder<Manga, Manga, QAfterFilterCondition> sourceIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
@ -3100,6 +3178,18 @@ extension MangaQuerySortBy on QueryBuilder<Manga, Manga, QSortBy> {
});
}
QueryBuilder<Manga, Manga, QAfterSortBy> sortBySmartUpdateDays() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'smartUpdateDays', Sort.asc);
});
}
QueryBuilder<Manga, Manga, QAfterSortBy> sortBySmartUpdateDaysDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'smartUpdateDays', Sort.desc);
});
}
QueryBuilder<Manga, Manga, QAfterSortBy> sortBySource() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'source', Sort.asc);
@ -3330,6 +3420,18 @@ extension MangaQuerySortThenBy on QueryBuilder<Manga, Manga, QSortThenBy> {
});
}
QueryBuilder<Manga, Manga, QAfterSortBy> thenBySmartUpdateDays() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'smartUpdateDays', Sort.asc);
});
}
QueryBuilder<Manga, Manga, QAfterSortBy> thenBySmartUpdateDaysDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'smartUpdateDays', Sort.desc);
});
}
QueryBuilder<Manga, Manga, QAfterSortBy> thenBySource() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'source', Sort.asc);
@ -3485,6 +3587,12 @@ extension MangaQueryWhereDistinct on QueryBuilder<Manga, Manga, QDistinct> {
});
}
QueryBuilder<Manga, Manga, QDistinct> distinctBySmartUpdateDays() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'smartUpdateDays');
});
}
QueryBuilder<Manga, Manga, QDistinct> distinctBySource(
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
@ -3621,6 +3729,12 @@ extension MangaQueryProperty on QueryBuilder<Manga, Manga, QQueryProperty> {
});
}
QueryBuilder<Manga, int?, QQueryOperations> smartUpdateDaysProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'smartUpdateDays');
});
}
QueryBuilder<Manga, String?, QQueryOperations> sourceProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'source');

View file

@ -1855,6 +1855,7 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
child: Row(
children: [
Expanded(child: widget.action!),
Expanded(child: _smartUpdateDays()),
Expanded(
child: widget.itemType == ItemType.novel
? SizedBox.shrink()
@ -1905,6 +1906,33 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
);
}
Widget _smartUpdateDays() {
return SizedBox(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
elevation: 0,
),
onPressed: () {},
child: Column(
children: [
Icon(
Icons.hourglass_empty,
size: 20,
color: context.secondaryColor,
),
const SizedBox(height: 4),
Text(
"${widget.manga?.smartUpdateDays ?? "N/A"}",
style: TextStyle(fontSize: 11, color: context.secondaryColor),
textAlign: TextAlign.center,
),
],
),
),
);
}
/// Tracker button
Widget _action() {
return StreamBuilder(

View file

@ -5,6 +5,7 @@ import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/models/update.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/services/get_detail.dart';
import 'package:mangayomi/utils/extensions/others.dart';
import 'package:mangayomi/utils/utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -62,7 +63,7 @@ Future<dynamic> updateMangaDetail(
return;
}
isar.writeTxnSync(() {
isar.mangas.putSync(manga);
final mangaId = isar.mangas.putSync(manga);
manga.lastUpdate = DateTime.now().millisecondsSinceEpoch;
List<Chapter> chapters = [];
@ -129,6 +130,28 @@ Future<dynamic> updateMangaDetail(
oldChap.manga.saveSync();
}
}
final List<int> daysBetweenUploads = [];
for (var i = 0; i + 1 < chaps.length; i++) {
if (chaps[i].dateUpload != null && chaps[i + 1].dateUpload != null) {
final date1 = DateTime.fromMillisecondsSinceEpoch(
int.parse(chaps[i].dateUpload!),
);
final date2 = DateTime.fromMillisecondsSinceEpoch(
int.parse(chaps[i + 1].dateUpload!),
);
daysBetweenUploads.add(date1.difference(date2).abs().inDays);
}
}
if (daysBetweenUploads.isNotEmpty) {
final median = daysBetweenUploads.median();
isar.mangas.putSync(
manga
..id = mangaId
..smartUpdateDays = median != 0
? median
: daysBetweenUploads.arithmeticMean(),
);
}
});
} catch (e, s) {
if (showToast) botToast('$e\n$s');

View file

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

View file

@ -1,5 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mangayomi/modules/widgets/custom_extended_image_provider.dart';
import 'package:mangayomi/modules/widgets/progress_center.dart';
import 'package:mangayomi/utils/constant.dart';
import 'package:marquee/marquee.dart';
import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/models/manga.dart';
@ -10,6 +13,8 @@ import 'package:mangayomi/utils/extensions/chapter.dart';
import 'package:mangayomi/utils/extensions/string_extensions.dart';
import 'package:mangayomi/modules/manga/detail/providers/state_providers.dart';
import 'package:mangayomi/modules/manga/download/download_page_widget.dart';
import 'package:photo_view/photo_view.dart';
import 'package:photo_view/photo_view_gallery.dart';
class ChapterListTileWidget extends ConsumerWidget {
final Chapter chapter;
@ -33,6 +38,9 @@ class ChapterListTileWidget extends ConsumerWidget {
onLongPress: () => _handleInteraction(ref),
onSecondaryTap: () => _handleInteraction(ref),
child: ListTile(
tileColor: (chapter.isFiller ?? false)
? context.primaryColor.withValues(alpha: 0.15)
: null,
textColor: chapter.isRead!
? context.isLight
? Colors.black.withValues(alpha: 0.4)
@ -44,14 +52,43 @@ class ChapterListTileWidget extends ConsumerWidget {
onTap: () async => _handleInteraction(ref, context),
title: Row(
children: [
if (chapter.thumbnailUrl != null)
_thumbnailPreview(context, chapter.thumbnailUrl),
chapter.isBookmarked!
? Icon(Icons.bookmark, size: 16, color: context.primaryColor)
: Container(),
Flexible(child: _buildTitle(chapter.name!, context)),
chapter.description != null
? Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTitle(chapter.name!, context),
Text(
chapter.description!,
style: const TextStyle(fontSize: 11),
overflow: TextOverflow.ellipsis,
),
],
),
)
: Flexible(child: _buildTitle(chapter.name!, context)),
],
),
subtitle: Row(
children: [
if (chapter.isFiller ?? false)
Row(
children: [
Icon(Icons.label, size: 16, color: context.primaryColor),
Text(
" Filler ",
style: TextStyle(
fontSize: 11,
color: context.primaryColor,
),
),
],
),
if ((chapter.manga.value!.isLocalArchive ?? false) == false)
Text(
chapter.dateUpload == null || chapter.dateUpload!.isEmpty
@ -172,4 +209,62 @@ class ChapterListTileWidget extends ConsumerWidget {
},
);
}
Widget _thumbnailPreview(BuildContext context, String? imageUrl) {
final imageProvider = CustomExtendedNetworkImageProvider(
toImgUrl(imageUrl ?? ""),
);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 8),
child: GestureDetector(
onTap: () {
_openImage(context, imageProvider);
},
child: SizedBox(
width: 50,
height: 65,
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(5)),
image: DecorationImage(image: imageProvider, fit: BoxFit.cover),
),
),
),
),
);
}
void _openImage(BuildContext context, ImageProvider imageProvider) {
showDialog(
context: context,
builder: (context) {
return Scaffold(
backgroundColor: Colors.transparent,
body: Stack(
children: [
GestureDetector(
onTap: () => Navigator.pop(context),
child: PhotoViewGallery.builder(
backgroundDecoration: const BoxDecoration(
color: Colors.transparent,
),
itemCount: 1,
builder: (context, index) {
return PhotoViewGalleryPageOptions(
imageProvider: imageProvider,
minScale: PhotoViewComputedScale.contained,
maxScale: 2.0,
);
},
loadingBuilder: (context, event) {
return const ProgressCenter();
},
),
),
],
),
);
},
);
}
}

View file

@ -26,9 +26,9 @@ class PlayerScreen extends ConsumerStatefulWidget {
class _PlayerScreenState extends ConsumerState<PlayerScreen> {
int _total = 0;
int _received = 0;
late http.StreamedResponse _response;
http.StreamedResponse? _response;
final List<int> _bytes = [];
late StreamSubscription<List<int>>? _subscription;
StreamSubscription<List<int>>? _subscription;
@override
void dispose() {
@ -661,8 +661,8 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
),
),
);
_total = _response.contentLength ?? 0;
_subscription = _response.stream.listen((value) {
_total = _response?.contentLength ?? 0;
_subscription = _response?.stream.listen((value) {
setState(() {
_bytes.addAll(value);
_received += value.length;

View file

@ -91,6 +91,7 @@ class DiscordRPC {
: "Reading";
final title = chapter.manga.value!.name;
final chapterTitle = chapter.name;
final imageUrl = chapter.manga.value!.imageUrl;
final rpcShowTitle = ref.read(rpcShowTitleStateProvider);
final rpcShowCoverImage = ref.read(rpcShowCoverImageStateProvider);
await updateActivity(
@ -98,9 +99,10 @@ class DiscordRPC {
state: rpcShowTitle && rpcShowReadingWatchingProgress
? chapterTitle
: "-----",
assets: rpcShowCoverImage
assets:
rpcShowCoverImage && imageUrl != null && imageUrl.startsWith("http")
? RPCAssets(
largeImage: chapter.manga.value!.imageUrl,
largeImage: imageUrl,
largeText: rpcShowTitle ? chapter.manga.value!.name : "-----",
smallImage: "app-icon",
smallText: "Mangayomi",

View file

@ -20,6 +20,21 @@ extension LetExtension<T> on T {
}
}
extension MedianExtension on List<int> {
int median() {
var middle = length ~/ 2;
if (length % 2 == 1) {
return this[middle];
} else {
return ((this[middle - 1] + this[middle]) / 2).round();
}
}
int arithmeticMean() {
return isNotEmpty ? (reduce((e1, e2) => e1 + e2) / length).round() : 0;
}
}
extension ImageProviderExtension on ImageProvider {
Future<Uint8List?> getBytes(
BuildContext context, {

View file

@ -14,6 +14,7 @@
#include <media_kit_video/media_kit_video_plugin.h>
#include <screen_retriever_linux/screen_retriever_linux_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
#include <volume_controller/volume_controller_plugin.h>
#include <window_manager/window_manager_plugin.h>
#include <window_to_front/window_to_front_plugin.h>
@ -42,6 +43,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
g_autoptr(FlPluginRegistrar) volume_controller_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "VolumeControllerPlugin");
volume_controller_plugin_register_with_registrar(volume_controller_registrar);
g_autoptr(FlPluginRegistrar) window_manager_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
window_manager_plugin_register_with_registrar(window_manager_registrar);

View file

@ -11,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
media_kit_video
screen_retriever_linux
url_launcher_linux
volume_controller
window_manager
window_to_front
)

View file

@ -1160,7 +1160,7 @@ packages:
source: hosted
version: "0.5.3"
media_kit:
dependency: transitive
dependency: "direct main"
description:
path: media_kit
ref: HEAD

View file

@ -41,6 +41,10 @@ dependencies:
flutter_web_auth_2: ^3.1.2
numberpicker: ^2.1.2
encrypt: ^5.0.3
media_kit:
git:
url: https://github.com/Schnitzel5/media-kit.git
path: media_kit
media_kit_video:
git:
url: https://github.com/Schnitzel5/media-kit.git