feat #682 option to split chapter whe importing local epub

This commit is contained in:
Moustapha Kodjo Amadou 2026-04-07 11:55:42 +01:00
parent f7ec660543
commit 0df04bcfad
26 changed files with 367 additions and 66 deletions

View file

@ -196,6 +196,8 @@
"total_episodes": "Gesamtepisoden",
"import_local_file": "Lokale Datei importieren",
"import_files": "Dateien",
"split_epub_chapters": "In Kapitel aufteilen",
"split_epub_chapters_description": "Jedes EPUB-Kapitel als separaten Eintrag importieren",
"nothing_read_recently": "Kürzlich nichts gelesen",
"status": "Status",
"not_started": "Nicht begonnen",

View file

@ -388,6 +388,8 @@
"total_episodes": "Total episodes",
"import_local_file": "Import Local file",
"import_files": "Files",
"split_epub_chapters": "Split into chapters",
"split_epub_chapters_description": "Import each EPUB chapter as a separate entry",
"nothing_read_recently": "Nothing read recently",
"status": "Status",
"not_started": "Not started",
@ -830,5 +832,6 @@
"extension_server_jar_imported": "Extension server JAR was imported.",
"could_not_launch_apk_bridge_page": "Could not launch the ApkBridge page.",
"proxy_server_ip_hint": "Server IP (e.g., 10.0.0.5 or https://example.com)",
"not_configured": "Not configured"
"not_configured": "Not configured",
"webview": "Webview"
}

View file

@ -1619,6 +1619,18 @@ abstract class AppLocalizations {
/// **'Files'**
String get import_files;
/// No description provided for @split_epub_chapters.
///
/// In en, this message translates to:
/// **'Split into chapters'**
String get split_epub_chapters;
/// No description provided for @split_epub_chapters_description.
///
/// In en, this message translates to:
/// **'Import each EPUB chapter as a separate entry'**
String get split_epub_chapters_description;
/// No description provided for @nothing_read_recently.
///
/// In en, this message translates to:
@ -4240,6 +4252,12 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Not configured'**
String get not_configured;
/// No description provided for @webview.
///
/// In en, this message translates to:
/// **'Webview'**
String get webview;
}
class _AppLocalizationsDelegate

View file

@ -869,6 +869,13 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get import_files => 'ملفات';
@override
String get split_epub_chapters => 'Split into chapters';
@override
String get split_epub_chapters_description =>
'Import each EPUB chapter as a separate entry';
@override
String get nothing_read_recently => 'لم يتم قراءة شيء مؤخراً';
@ -2286,4 +2293,7 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get not_configured => 'Not configured';
@override
String get webview => 'Webview';
}

View file

@ -871,6 +871,13 @@ class AppLocalizationsAs extends AppLocalizations {
@override
String get import_files => 'ফাইল';
@override
String get split_epub_chapters => 'Split into chapters';
@override
String get split_epub_chapters_description =>
'Import each EPUB chapter as a separate entry';
@override
String get nothing_read_recently => 'শেহতীয়াকৈ একো পঢ়া নাই';
@ -2292,4 +2299,7 @@ class AppLocalizationsAs extends AppLocalizations {
@override
String get not_configured => 'Not configured';
@override
String get webview => 'Webview';
}

View file

@ -873,6 +873,13 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get import_files => 'Dateien';
@override
String get split_epub_chapters => 'In Kapitel aufteilen';
@override
String get split_epub_chapters_description =>
'Jedes EPUB-Kapitel als separaten Eintrag importieren';
@override
String get nothing_read_recently => 'Kürzlich nichts gelesen';
@ -2308,4 +2315,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get not_configured => 'Not configured';
@override
String get webview => 'Webview';
}

View file

@ -871,6 +871,13 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get import_files => 'Files';
@override
String get split_epub_chapters => 'Split into chapters';
@override
String get split_epub_chapters_description =>
'Import each EPUB chapter as a separate entry';
@override
String get nothing_read_recently => 'Nothing read recently';
@ -2286,4 +2293,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get not_configured => 'Not configured';
@override
String get webview => 'Webview';
}

View file

@ -875,6 +875,13 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get import_files => 'Archivos';
@override
String get split_epub_chapters => 'Split into chapters';
@override
String get split_epub_chapters_description =>
'Import each EPUB chapter as a separate entry';
@override
String get nothing_read_recently => 'Nada leído recientemente';
@ -2315,6 +2322,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get not_configured => 'Not configured';
@override
String get webview => 'Webview';
}
/// The translations for Spanish Castilian, as used in Latin America and the Caribbean (`es_419`).

View file

@ -877,6 +877,13 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get import_files => 'Fichiers';
@override
String get split_epub_chapters => 'Split into chapters';
@override
String get split_epub_chapters_description =>
'Import each EPUB chapter as a separate entry';
@override
String get nothing_read_recently => 'Rien de lu recemment';
@ -2316,4 +2323,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get not_configured => 'Not configured';
@override
String get webview => 'Webview';
}

View file

@ -871,6 +871,13 @@ class AppLocalizationsHi extends AppLocalizations {
@override
String get import_files => 'फ़ाइलें';
@override
String get split_epub_chapters => 'Split into chapters';
@override
String get split_epub_chapters_description =>
'Import each EPUB chapter as a separate entry';
@override
String get nothing_read_recently => 'हाल ही में कुछ भी नहीं पढ़ा';
@ -2292,4 +2299,7 @@ class AppLocalizationsHi extends AppLocalizations {
@override
String get not_configured => 'Not configured';
@override
String get webview => 'Webview';
}

View file

@ -875,6 +875,13 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get import_files => 'File';
@override
String get split_epub_chapters => 'Split into chapters';
@override
String get split_epub_chapters_description =>
'Import each EPUB chapter as a separate entry';
@override
String get nothing_read_recently => 'Tidak ada yang dibaca baru-baru ini';
@ -2298,4 +2305,7 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get not_configured => 'Not configured';
@override
String get webview => 'Webview';
}

View file

@ -875,6 +875,13 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get import_files => 'File';
@override
String get split_epub_chapters => 'Split into chapters';
@override
String get split_epub_chapters_description =>
'Import each EPUB chapter as a separate entry';
@override
String get nothing_read_recently => 'Niente letto di recente';
@ -2312,4 +2319,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get not_configured => 'Not configured';
@override
String get webview => 'Webview';
}

View file

@ -864,6 +864,13 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get import_files => 'ファイル';
@override
String get split_epub_chapters => 'Split into chapters';
@override
String get split_epub_chapters_description =>
'Import each EPUB chapter as a separate entry';
@override
String get nothing_read_recently => '最近読んだものがありません';
@ -2258,4 +2265,7 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get not_configured => 'Not configured';
@override
String get webview => 'Webview';
}

View file

@ -875,6 +875,13 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get import_files => 'Arquivos';
@override
String get split_epub_chapters => 'Split into chapters';
@override
String get split_epub_chapters_description =>
'Import each EPUB chapter as a separate entry';
@override
String get nothing_read_recently => 'Nada lido recentemente';
@ -2310,6 +2317,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get not_configured => 'Not configured';
@override
String get webview => 'Webview';
}
/// The translations for Portuguese, as used in Brazil (`pt_BR`).

View file

@ -876,6 +876,13 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get import_files => 'Файлы';
@override
String get split_epub_chapters => 'Split into chapters';
@override
String get split_epub_chapters_description =>
'Import each EPUB chapter as a separate entry';
@override
String get nothing_read_recently => 'Недавно ничего не читалось';
@ -2316,4 +2323,7 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get not_configured => 'Not configured';
@override
String get webview => 'Webview';
}

View file

@ -871,6 +871,13 @@ class AppLocalizationsTh extends AppLocalizations {
@override
String get import_files => 'ไฟล์';
@override
String get split_epub_chapters => 'Split into chapters';
@override
String get split_epub_chapters_description =>
'Import each EPUB chapter as a separate entry';
@override
String get nothing_read_recently => 'ยังไม่ได้อ่านอะไรเลย';
@ -2286,4 +2293,7 @@ class AppLocalizationsTh extends AppLocalizations {
@override
String get not_configured => 'Not configured';
@override
String get webview => 'Webview';
}

View file

@ -871,6 +871,13 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get import_files => 'Dosyalar';
@override
String get split_epub_chapters => 'Split into chapters';
@override
String get split_epub_chapters_description =>
'Import each EPUB chapter as a separate entry';
@override
String get nothing_read_recently => 'Son Zamanlarda Okunan Bir Şey Yok';
@ -2299,4 +2306,7 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get not_configured => 'Not configured';
@override
String get webview => 'Webview';
}

View file

@ -862,6 +862,13 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get import_files => '文件';
@override
String get split_epub_chapters => 'Split into chapters';
@override
String get split_epub_chapters_description =>
'Import each EPUB chapter as a separate entry';
@override
String get nothing_read_recently => '最近未阅读';
@ -2239,4 +2246,7 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get not_configured => 'Not configured';
@override
String get webview => 'Webview';
}

View file

@ -295,21 +295,37 @@ Future<void> _scanDirectory(Ref ref, Directory? dir) async {
if (manga.itemType == ItemType.novel) {
final book = await parseEpubFromPath(
epubPath: chapterPath,
fullData: false,
fullData: true,
);
if (book.cover != null) {
manga.customCoverImage = book.cover!.getCoverImage;
saveManga++;
}
chaptersToSave.add(
Chapter(
mangaId: manga.id,
name: book.name,
archivePath: chapterPath,
downloadSize: null,
)..manga.value = manga,
);
final chaps = book.chapters;
if (chaps.isNotEmpty) {
for (int i = 0; i < chaps.length; i++) {
final epubChapter = chaps[i];
chaptersToSave.add(
Chapter(
mangaId: manga.id,
name: epubChapter.name,
archivePath: chapterPath,
url: epubChapter.path,
downloadSize: null,
)..manga.value = manga,
);
}
} else {
chaptersToSave.add(
Chapter(
mangaId: manga.id,
name: book.name,
archivePath: chapterPath,
downloadSize: null,
)..manga.value = manga,
);
}
} else {
final chapterFile = File(chapterPath);
final chap = Chapter(

View file

@ -16,6 +16,7 @@ Future importArchivesFromFile(
Manga? mManga, {
required ItemType itemType,
required bool init,
bool splitChapters = true,
}) async {
final keepAlile = ref.keepAlive();
try {
@ -72,7 +73,7 @@ Future importArchivesFromFile(
if (itemType == ItemType.novel) {
final book = await parseEpubFromPath(
epubPath: file.path!,
fullData: false,
fullData: splitChapters,
);
if (book.cover != null) {
@ -80,14 +81,32 @@ Future importArchivesFromFile(
manga..customCoverImage = book.cover!.getCoverImage,
);
}
chapters.add(
Chapter(
mangaId: mangaId,
name: book.name,
archivePath: file.path,
updatedAt: DateTime.now().millisecondsSinceEpoch,
)..manga.value = manga,
);
final chaps = book.chapters;
if (splitChapters && chaps.isNotEmpty) {
for (int i = 0; i < chaps.length; i++) {
final epubChapter = chaps[i];
chapters.add(
Chapter(
mangaId: mangaId,
name: epubChapter.name,
archivePath: file.path,
url: epubChapter.path,
updatedAt: DateTime.now().millisecondsSinceEpoch,
)..manga.value = manga,
);
}
} else {
// Fallback: single chapter if no spine chapters found
chapters.add(
Chapter(
mangaId: mangaId,
name: book.name,
archivePath: file.path,
updatedAt: DateTime.now().millisecondsSinceEpoch,
)..manga.value = manga,
);
}
} else {
chapters.add(
Chapter(

View file

@ -17,7 +17,8 @@ final class ImportArchivesFromFileProvider
with $FutureModifier<dynamic>, $FutureProvider<dynamic> {
ImportArchivesFromFileProvider._({
required ImportArchivesFromFileFamily super.from,
required (Manga?, {ItemType itemType, bool init}) super.argument,
required (Manga?, {ItemType itemType, bool init, bool splitChapters})
super.argument,
}) : super(
retry: null,
name: r'importArchivesFromFileProvider',
@ -43,12 +44,15 @@ final class ImportArchivesFromFileProvider
@override
FutureOr<dynamic> create(Ref ref) {
final argument = this.argument as (Manga?, {ItemType itemType, bool init});
final argument =
this.argument
as (Manga?, {ItemType itemType, bool init, bool splitChapters});
return importArchivesFromFile(
ref,
argument.$1,
itemType: argument.itemType,
init: argument.init,
splitChapters: argument.splitChapters,
);
}
@ -65,13 +69,13 @@ final class ImportArchivesFromFileProvider
}
String _$importArchivesFromFileHash() =>
r'52d80a07200627e1bc650b06d1fd8aed66c03e4c';
r'f7b2e8cb611bfc32c83e1dcffffb2ad7122f1067';
final class ImportArchivesFromFileFamily extends $Family
with
$FunctionalFamilyOverride<
FutureOr<dynamic>,
(Manga?, {ItemType itemType, bool init})
(Manga?, {ItemType itemType, bool init, bool splitChapters})
> {
ImportArchivesFromFileFamily._()
: super(
@ -86,8 +90,14 @@ final class ImportArchivesFromFileFamily extends $Family
Manga? mManga, {
required ItemType itemType,
required bool init,
bool splitChapters = true,
}) => ImportArchivesFromFileProvider._(
argument: (mManga, itemType: itemType, init: init),
argument: (
mManga,
itemType: itemType,
init: init,
splitChapters: splitChapters,
),
from: this,
);

View file

@ -263,6 +263,7 @@ void showImportLocalDialog(BuildContext context, ItemType itemType) {
ItemType.novel => ".epub",
};
bool isLoading = false;
bool splitChapters = true;
showDialog(
context: context,
barrierDismissible: !isLoading,
@ -274,50 +275,75 @@ void showImportLocalDialog(BuildContext context, ItemType itemType) {
return Consumer(
builder: (context, ref, child) {
return SizedBox(
height: 100,
height: itemType == ItemType.novel ? 150 : 100,
child: Stack(
children: [
Row(
Column(
children: [
if (itemType == ItemType.novel)
SwitchListTile(
dense: true,
contentPadding: EdgeInsets.zero,
title: Text(
l10n.split_epub_chapters,
style: const TextStyle(fontSize: 13),
),
subtitle: Text(
l10n.split_epub_chapters_description,
style: const TextStyle(fontSize: 10),
),
value: splitChapters,
onChanged: (v) =>
setState(() => splitChapters = v),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(3),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
onPressed: () async {
setState(() => isLoading = true);
await ref.watch(
importArchivesFromFileProvider(
itemType: itemType,
null,
init: true,
).future,
);
setState(() => isLoading = false);
if (!context.mounted) return;
Navigator.pop(context);
},
child: Column(
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
const Icon(Icons.archive_outlined),
Text(
"${l10n.import_files} ( $filesText )",
style: TextStyle(
color: Theme.of(
context,
).textTheme.bodySmall!.color,
fontSize: 10,
child: Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(3),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
10,
),
),
),
onPressed: () async {
setState(() => isLoading = true);
await ref.watch(
importArchivesFromFileProvider(
itemType: itemType,
null,
init: true,
splitChapters: splitChapters,
).future,
);
setState(() => isLoading = false);
if (!context.mounted) return;
Navigator.pop(context);
},
child: Column(
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
const Icon(Icons.archive_outlined),
Text(
"${l10n.import_files} ( $filesText )",
style: TextStyle(
color: Theme.of(
context,
).textTheme.bodySmall!.color,
fontSize: 10,
),
),
],
),
),
],
),
),
),
],
),
),
],

View file

@ -868,12 +868,24 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
manga: manga,
);
} else {
final splitChapters =
manga.itemType ==
ItemType.novel
? await _showSplitChaptersDialog(
context,
)
: true;
if (!context.mounted) {
return;
}
await ref.watch(
importArchivesFromFileProvider(
itemType:
manga.itemType,
manga,
init: false,
splitChapters:
splitChapters,
).future,
);
}
@ -1859,11 +1871,19 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
if (manga!.source == "torrent") {
addTorrent(context, manga: manga);
} else {
final splitChapters =
manga.itemType == ItemType.novel
? await _showSplitChaptersDialog(
context,
)
: true;
if (!context.mounted) return;
await ref.watch(
importArchivesFromFileProvider(
itemType: manga.itemType,
manga,
init: false,
splitChapters: splitChapters,
).future,
);
}
@ -1988,7 +2008,7 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
),
const SizedBox(height: 4),
Text(
'WebView',
context.l10n.webview,
style: TextStyle(
fontSize: 11,
color: context.secondaryColor,
@ -2564,3 +2584,25 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
);
}
}
Future<bool> _showSplitChaptersDialog(BuildContext context) async {
final l10n = l10nLocalizations(context)!;
return await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(l10n.split_epub_chapters),
content: Text(l10n.split_epub_chapters_description),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text(l10n.cancel),
),
FilledButton(
onPressed: () => Navigator.pop(context, true),
child: Text(l10n.split_epub_chapters),
),
],
),
) ??
true;
}

View file

@ -28,8 +28,23 @@ Future<(String, EpubNovel?)> getHtmlContent(
fullData: true,
);
String htmlContent = "";
for (var subChapter in book.chapters) {
htmlContent += "\n<hr/>\n${subChapter.content}";
if (chapter.url != null && chapter.url!.isNotEmpty) {
// Load specific chapter by its spine idref
final matches = book.chapters.where((c) => c.path == chapter.url);
if (matches.isNotEmpty) {
htmlContent = matches.first.content;
} else {
// Fallback: try via Rust direct access
htmlContent = await getChapterContent(
epubPath: chapter.archivePath!,
chapterPath: chapter.url!,
);
}
} else {
// Legacy: no chapter url, concatenate all (old single-chapter imports)
for (var subChapter in book.chapters) {
htmlContent += "\n<hr/>\n${subChapter.content}";
}
}
result = (_buildHtml(htmlContent), book);
} catch (_) {}

View file

@ -66,7 +66,7 @@ final class GetHtmlContentProvider
}
}
String _$getHtmlContentHash() => r'03e421b7f7e821526c47f3b460fc9d866f56c9f6';
String _$getHtmlContentHash() => r'b3f9bf922e2aad2d4e34a64cf08a6a755b743921';
final class GetHtmlContentFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<(String, EpubNovel?)>, Chapter> {

View file

@ -91,7 +91,7 @@ final class HeadersProvider
}
}
String _$headersHash() => r'b73ec60e965527495b5ac8ee96c8e23cc1392a56';
String _$headersHash() => r'35df3bf8876e8be44fa4a8fbe57097f1329601b7';
final class HeadersFamily extends $Family
with