New Setting: Mark duplicate chapter as read

When this setting is turned on, the app will automatically mark chapters as read if they have the same chapter number as a chapter you've already read.

This is useful for sources where:

- multiple scanlators upload the same chapter number
- duplicate entries appear due to different naming formats
- a series has repeated or alternative releases of the same chapter

With the setting enabled, you won’t have to manually mark each duplicate as read.
This commit is contained in:
NBA2K1 2026-04-26 00:51:59 +02:00
parent 85f154d738
commit 4a44dd6658
29 changed files with 822 additions and 555 deletions

View file

@ -537,6 +537,7 @@
"video_audio_info": "Preferred languages, pitch correction, audio channels",
"player": "Player",
"markEpisodeAsSeenSetting": "At what point to mark the episode as seen",
"mark_duplicate_chapters_read": "Mark duplicate chapter numbers as read",
"default_skip_intro_length": "Default Skip intro length",
"default_playback_speed_length": "Default Playback speed length",
"updateProgressAfterReading": "Update progress after reading",

View file

@ -2513,6 +2513,12 @@ abstract class AppLocalizations {
/// **'At what point to mark the episode as seen'**
String get markEpisodeAsSeenSetting;
/// No description provided for @mark_duplicate_chapters_read.
///
/// In en, this message translates to:
/// **'Mark duplicate chapter numbers as read'**
String get mark_duplicate_chapters_read;
/// No description provided for @default_skip_intro_length.
///
/// In en, this message translates to:

View file

@ -1361,6 +1361,10 @@ class AppLocalizationsAr extends AppLocalizations {
String get markEpisodeAsSeenSetting =>
'في أي نقطة لوضع علامة على الحلقة كمشاهدة';
@override
String get mark_duplicate_chapters_read =>
'Mark duplicate chapter numbers as read';
@override
String get default_skip_intro_length => 'طول تخطي المقدمة الافتراضي';

View file

@ -1348,6 +1348,10 @@ class AppLocalizationsAs extends AppLocalizations {
String get markEpisodeAsSeenSetting =>
'খণ্ডটো কেতিয়া দেখা বুলি চিহ্নিত কৰিব';
@override
String get mark_duplicate_chapters_read =>
'Mark duplicate chapter numbers as read';
@override
String get default_skip_intro_length => 'ডিফল্ট ইনট্ৰ\' এৰি দিয়াৰ দৈৰ্ঘ্য';

View file

@ -1352,6 +1352,10 @@ class AppLocalizationsDe extends AppLocalizations {
String get markEpisodeAsSeenSetting =>
'Zu welchem Zeitpunkt die Episode als gesehen markieren';
@override
String get mark_duplicate_chapters_read =>
'Mark duplicate chapter numbers as read';
@override
String get default_skip_intro_length =>
'Standardlänge für Intro überspringen';

View file

@ -1347,6 +1347,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get markEpisodeAsSeenSetting =>
'At what point to mark the episode as seen';
@override
String get mark_duplicate_chapters_read =>
'Mark duplicate chapter numbers as read';
@override
String get default_skip_intro_length => 'Default Skip intro length';

View file

@ -1357,6 +1357,10 @@ class AppLocalizationsEs extends AppLocalizations {
String get markEpisodeAsSeenSetting =>
'En qué punto marcar el episodio como visto';
@override
String get mark_duplicate_chapters_read =>
'Mark duplicate chapter numbers as read';
@override
String get default_skip_intro_length =>
'Duración predeterminada para saltar la introducción';

View file

@ -1359,6 +1359,10 @@ class AppLocalizationsFr extends AppLocalizations {
String get markEpisodeAsSeenSetting =>
'À quel moment marquer l\'épisode comme vu';
@override
String get mark_duplicate_chapters_read =>
'Mark duplicate chapter numbers as read';
@override
String get default_skip_intro_length =>
'Longueur par défaut du passage de l\'intro';

View file

@ -1347,6 +1347,10 @@ class AppLocalizationsHi extends AppLocalizations {
String get markEpisodeAsSeenSetting =>
'एपिसोड को कब देखा गया के रूप में चिह्नित करना है';
@override
String get mark_duplicate_chapters_read =>
'Mark duplicate chapter numbers as read';
@override
String get default_skip_intro_length => 'डिफ़ॉल्ट परिचय छोड़ने की अवधि';

View file

@ -1351,6 +1351,10 @@ class AppLocalizationsId extends AppLocalizations {
String get markEpisodeAsSeenSetting =>
'Pada titik mana menandai episode sebagai terlihat';
@override
String get mark_duplicate_chapters_read =>
'Mark duplicate chapter numbers as read';
@override
String get default_skip_intro_length => 'Panjang lewati intro default';

View file

@ -1358,6 +1358,10 @@ class AppLocalizationsIt extends AppLocalizations {
String get markEpisodeAsSeenSetting =>
'In quale momento contrassegnare l\'episodio come visto';
@override
String get mark_duplicate_chapters_read =>
'Mark duplicate chapter numbers as read';
@override
String get default_skip_intro_length =>
'Durata predefinita per saltare l\'introduzione';

View file

@ -1332,6 +1332,10 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get markEpisodeAsSeenSetting => 'エピソードを視聴済みにするタイミング';
@override
String get mark_duplicate_chapters_read =>
'Mark duplicate chapter numbers as read';
@override
String get default_skip_intro_length => 'デフォルトイントロスキップ長';

View file

@ -1355,6 +1355,10 @@ class AppLocalizationsPt extends AppLocalizations {
String get markEpisodeAsSeenSetting =>
'Em que ponto marcar o episódio como visto';
@override
String get mark_duplicate_chapters_read =>
'Mark duplicate chapter numbers as read';
@override
String get default_skip_intro_length =>
'Duração padrão para pular a introdução';

View file

@ -1368,6 +1368,10 @@ class AppLocalizationsRu extends AppLocalizations {
String get markEpisodeAsSeenSetting =>
'В какой момент отметить эпизод как просмотренный';
@override
String get mark_duplicate_chapters_read =>
'Mark duplicate chapter numbers as read';
@override
String get default_skip_intro_length =>
'Стандартная длина пропуска вступления';

View file

@ -1347,6 +1347,10 @@ class AppLocalizationsTh extends AppLocalizations {
String get markEpisodeAsSeenSetting =>
'กำหนดจุดที่จะทำเครื่องหมายตอนว่าดูแล้ว';
@override
String get mark_duplicate_chapters_read =>
'Mark duplicate chapter numbers as read';
@override
String get default_skip_intro_length => 'ค่าพื้นฐานความยาวการเข้าอินโทร';

View file

@ -1349,6 +1349,10 @@ class AppLocalizationsTr extends AppLocalizations {
String get markEpisodeAsSeenSetting =>
'Bölümün izlendiği olarak işaretleneceği nokta';
@override
String get mark_duplicate_chapters_read =>
'Mark duplicate chapter numbers as read';
@override
String get default_skip_intro_length => 'Varsayılan Giriş Atla süresi';

View file

@ -1316,6 +1316,10 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get markEpisodeAsSeenSetting => '标记剧集为已看的时间点';
@override
String get mark_duplicate_chapters_read =>
'Mark duplicate chapter numbers as read';
@override
String get default_skip_intro_length => '默认跳过介绍长度';

View file

@ -336,6 +336,8 @@ class Settings {
bool? showPageGaps;
bool? autoReadDuplicateChapters;
bool? invertColors;
bool? grayscale;
@ -513,6 +515,7 @@ class Settings {
this.keepScreenOnReader = true,
this.webtoonSidePadding = 0,
this.showPageGaps = true,
this.autoReadDuplicateChapters = false,
this.invertColors = false,
this.grayscale = false,
this.readerBrightness = 0.0,
@ -796,6 +799,7 @@ class Settings {
keepScreenOnReader = json['keepScreenOnReader'];
webtoonSidePadding = json['webtoonSidePadding'];
showPageGaps = json['showPageGaps'];
autoReadDuplicateChapters = json['autoReadDuplicateChapters'];
invertColors = json['invertColors'];
grayscale = json['grayscale'];
readerBrightness = json['readerBrightness']?.toDouble();
@ -985,6 +989,7 @@ class Settings {
'keepScreenOnReader': keepScreenOnReader,
'webtoonSidePadding': webtoonSidePadding,
'showPageGaps': showPageGaps,
'autoReadDuplicateChapters': autoReadDuplicateChapters,
'invertColors': invertColors,
'grayscale': grayscale,
'readerBrightness': readerBrightness,

File diff suppressed because it is too large Load diff

View file

@ -59,7 +59,7 @@ final class AnimeStreamControllerProvider
}
String _$animeStreamControllerHash() =>
r'0a85358dcd4e3d7823c253ffbeccce8cf2612175';
r'fd3e6030dccc06be7ccd4b9564476a3587de401f';
final class AnimeStreamControllerFamily extends $Family
with

View file

@ -64,7 +64,7 @@ final class UpdateMangaDetailProvider
}
}
String _$updateMangaDetailHash() => r'd29f8980f7545dc57a112fd57ea5c045b36b34fe';
String _$updateMangaDetailHash() => r'7a5595f307535c2fed5b174c98ce56c0fdedf5d6';
final class UpdateMangaDetailFamily extends $Family
with

View file

@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/misc.dart';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart';
import 'package:mangayomi/utils/chapter_recognition.dart';
import 'package:mangayomi/modules/manga/reader/mixins/chapter_reader_settings_mixin.dart';
import 'package:mangayomi/modules/manga/reader/mixins/chapter_controller_mixin.dart';
import 'package:mangayomi/models/settings.dart';
@ -204,17 +206,45 @@ class ReaderController extends _$ReaderController
..chapterId = chapter.id
..index = isRead ? 0 : newIndex,
);
final autoReadDuplChap = ref.read(autoReadDuplicateChaptersStateProvider);
final now = DateTime.now().millisecondsSinceEpoch;
// When the chapter is finished, mark every other scanlation of the same
// chapter number as read too, in the same transaction.
final List<Chapter> siblings = [];
if (isRead && autoReadDuplChap) {
final manga = chapter.manga.value;
if (manga != null) {
final chapterNumber = ChapterRecognition().parseChapterNumber(
manga.name!,
chapter.name!,
);
for (final c in manga.chapters) {
if (c.id == chapter.id || (c.isRead ?? false)) continue;
final n = ChapterRecognition().parseChapterNumber(
manga.name!,
c.name!,
);
if (n == chapterNumber) {
c.isRead = true;
c.lastPageRead = '1';
c.updatedAt = now;
siblings.add(c);
}
}
}
}
final chap = chapter;
isar.writeTxnSync(() {
isar.settings.putSync(
getIsarSetting()
..chapterPageIndexList = chapterPageIndexs
..updatedAt = DateTime.now().millisecondsSinceEpoch,
..updatedAt = now,
);
chap.isRead = isRead;
chap.lastPageRead = isRead ? '1' : (newIndex + 1).toString();
chap.updatedAt = DateTime.now().millisecondsSinceEpoch;
chap.updatedAt = now;
isar.chapters.putSync(chap);
if (siblings.isNotEmpty) isar.chapters.putAllSync(siblings);
});
onSettingsMutated();
if (isRead) {

View file

@ -147,7 +147,7 @@ final class ReaderControllerProvider
}
}
String _$readerControllerHash() => r'adab728fa21939302d0f928b11be204e8e8a0527';
String _$readerControllerHash() => r'f129a22656f06082d217f2364697f3fc68668b64';
final class ReaderControllerFamily extends $Family
with

View file

@ -63,7 +63,7 @@ final class CheckForUpdateProvider
}
}
String _$checkForUpdateHash() => r'644316334ac3e95d37f54d7197d744c9de1260b6';
String _$checkForUpdateHash() => r'8064f08768f4d98997201a2f07ba160c721a6a3f';
final class CheckForUpdateFamily extends $Family
with

View file

@ -301,7 +301,7 @@ final class RestoreTachiBkBackupProvider
}
String _$restoreTachiBkBackupHash() =>
r'54434eaa9dc875d03ffb1dcf62ad2d7971649d61';
r'0f121d94021eb9a56d0689240dbcefd49f5a2a79';
final class RestoreTachiBkBackupFamily extends $Family
with $FunctionalFamilyOverride<void, (String, BackupType)> {

View file

@ -526,6 +526,26 @@ class ShowPageGapsState extends _$ShowPageGapsState {
}
}
@riverpod
class AutoReadDuplicateChaptersState extends _$AutoReadDuplicateChaptersState {
@override
bool build() {
return isar.settings.getSync(227)!.autoReadDuplicateChapters ?? false;
}
void set(bool value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(
settings!
..autoReadDuplicateChapters = value
..updatedAt = DateTime.now().millisecondsSinceEpoch,
),
);
}
}
@riverpod
class InvertColorsState extends _$InvertColorsState {
@override

View file

@ -1338,6 +1338,60 @@ abstract class _$ShowPageGapsState extends $Notifier<bool> {
}
}
@ProviderFor(AutoReadDuplicateChaptersState)
final autoReadDuplicateChaptersStateProvider =
AutoReadDuplicateChaptersStateProvider._();
final class AutoReadDuplicateChaptersStateProvider
extends $NotifierProvider<AutoReadDuplicateChaptersState, bool> {
AutoReadDuplicateChaptersStateProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'autoReadDuplicateChaptersStateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$autoReadDuplicateChaptersStateHash();
@$internal
@override
AutoReadDuplicateChaptersState create() => AutoReadDuplicateChaptersState();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(bool value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<bool>(value),
);
}
}
String _$autoReadDuplicateChaptersStateHash() =>
r'288816e17fefa753c690cc43a40cfbcf5352b9df';
abstract class _$AutoReadDuplicateChaptersState extends $Notifier<bool> {
bool build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<bool, bool>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<bool, bool>,
bool,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@ProviderFor(InvertColorsState)
final invertColorsStateProvider = InvertColorsStateProvider._();

View file

@ -28,6 +28,7 @@ class ReaderScreen extends ConsumerWidget {
final cropBorders = ref.watch(cropBordersStateProvider);
final keepScreenOn = ref.watch(keepScreenOnReaderStateProvider);
final autoReadDuplChap = ref.watch(autoReadDuplicateChaptersStateProvider);
final showPageGaps = ref.watch(showPageGapsStateProvider);
final webtoonSidePadding = ref.watch(webtoonSidePaddingStateProvider);
final navigationLayout = ref.watch(readerNavigationLayoutStateProvider);
@ -411,6 +412,15 @@ class ReaderScreen extends ConsumerWidget {
ref.read(showPageGapsStateProvider.notifier).set(value);
},
),
SwitchListTile(
value: autoReadDuplChap,
title: Text(context.l10n.mark_duplicate_chapters_read),
onChanged: (value) {
ref
.read(autoReadDuplicateChaptersStateProvider.notifier)
.set(value);
},
),
ListTile(
title: Text(context.l10n.webtoon_side_padding),
subtitle: Slider(

View file

@ -59,7 +59,7 @@ final class NovelReaderControllerProvider
}
String _$novelReaderControllerHash() =>
r'9133fe86e1c4bbdd47d9fa38e75310ecb8ede325';
r'c9171314d6c24d3f7a88bd7a258ab409f769dbb5';
final class NovelReaderControllerFamily extends $Family
with