mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-03-11 17:25:32 +00:00
Merge pull request #574 from Schnitzel5/rework/local-source
reworked local source
This commit is contained in:
commit
64cb896ea5
42 changed files with 1641 additions and 386 deletions
|
|
@ -546,5 +546,11 @@
|
|||
"watch_order": "Watch order",
|
||||
"sequels": "Sequels",
|
||||
"recommendations": "Recommendations",
|
||||
"recommendations_similarity": "Similarity:"
|
||||
"recommendations_similarity": "Similarity:",
|
||||
"local_folder_structure": "Structure of a local folder",
|
||||
"local_folder": "Local folders",
|
||||
"add_local_folder": "Add local folder",
|
||||
"rescan_local_folder": "Rescan all local folders now",
|
||||
"export_metadata": "Export metadata",
|
||||
"exported": "Exported"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3350,6 +3350,42 @@ abstract class AppLocalizations {
|
|||
/// In en, this message translates to:
|
||||
/// **'Similarity:'**
|
||||
String get recommendations_similarity;
|
||||
|
||||
/// No description provided for @local_folder_structure.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Structure of a local folder'**
|
||||
String get local_folder_structure;
|
||||
|
||||
/// No description provided for @local_folder.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Local folders'**
|
||||
String get local_folder;
|
||||
|
||||
/// No description provided for @add_local_folder.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add local folder'**
|
||||
String get add_local_folder;
|
||||
|
||||
/// No description provided for @rescan_local_folder.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Rescan all local folders now'**
|
||||
String get rescan_local_folder;
|
||||
|
||||
/// No description provided for @export_metadata.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Export metadata'**
|
||||
String get export_metadata;
|
||||
|
||||
/// No description provided for @exported.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Exported'**
|
||||
String get exported;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
|
|
|||
|
|
@ -1733,4 +1733,22 @@ class AppLocalizationsAr extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get recommendations_similarity => 'Similarity:';
|
||||
|
||||
@override
|
||||
String get local_folder_structure => 'Structure of a local folder';
|
||||
|
||||
@override
|
||||
String get local_folder => 'Local folders';
|
||||
|
||||
@override
|
||||
String get add_local_folder => 'Add local folder';
|
||||
|
||||
@override
|
||||
String get rescan_local_folder => 'Rescan all local folders now';
|
||||
|
||||
@override
|
||||
String get export_metadata => 'Export metadata';
|
||||
|
||||
@override
|
||||
String get exported => 'Exported';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1735,4 +1735,22 @@ class AppLocalizationsAs extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get recommendations_similarity => 'Similarity:';
|
||||
|
||||
@override
|
||||
String get local_folder_structure => 'Structure of a local folder';
|
||||
|
||||
@override
|
||||
String get local_folder => 'Local folders';
|
||||
|
||||
@override
|
||||
String get add_local_folder => 'Add local folder';
|
||||
|
||||
@override
|
||||
String get rescan_local_folder => 'Rescan all local folders now';
|
||||
|
||||
@override
|
||||
String get export_metadata => 'Export metadata';
|
||||
|
||||
@override
|
||||
String get exported => 'Exported';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1746,4 +1746,22 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get recommendations_similarity => 'Similarity:';
|
||||
|
||||
@override
|
||||
String get local_folder_structure => 'Structure of a local folder';
|
||||
|
||||
@override
|
||||
String get local_folder => 'Local folders';
|
||||
|
||||
@override
|
||||
String get add_local_folder => 'Add local folder';
|
||||
|
||||
@override
|
||||
String get rescan_local_folder => 'Rescan all local folders now';
|
||||
|
||||
@override
|
||||
String get export_metadata => 'Export metadata';
|
||||
|
||||
@override
|
||||
String get exported => 'Exported';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1734,4 +1734,22 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get recommendations_similarity => 'Similarity:';
|
||||
|
||||
@override
|
||||
String get local_folder_structure => 'Structure of a local folder';
|
||||
|
||||
@override
|
||||
String get local_folder => 'Local folders';
|
||||
|
||||
@override
|
||||
String get add_local_folder => 'Add local folder';
|
||||
|
||||
@override
|
||||
String get rescan_local_folder => 'Rescan all local folders now';
|
||||
|
||||
@override
|
||||
String get export_metadata => 'Export metadata';
|
||||
|
||||
@override
|
||||
String get exported => 'Exported';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1751,6 +1751,24 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get recommendations_similarity => 'Similarity:';
|
||||
|
||||
@override
|
||||
String get local_folder_structure => 'Structure of a local folder';
|
||||
|
||||
@override
|
||||
String get local_folder => 'Local folders';
|
||||
|
||||
@override
|
||||
String get add_local_folder => 'Add local folder';
|
||||
|
||||
@override
|
||||
String get rescan_local_folder => 'Rescan all local folders now';
|
||||
|
||||
@override
|
||||
String get export_metadata => 'Export metadata';
|
||||
|
||||
@override
|
||||
String get exported => 'Exported';
|
||||
}
|
||||
|
||||
/// The translations for Spanish Castilian, as used in Latin America and the Caribbean (`es_419`).
|
||||
|
|
|
|||
|
|
@ -1752,4 +1752,22 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get recommendations_similarity => 'Similarity:';
|
||||
|
||||
@override
|
||||
String get local_folder_structure => 'Structure of a local folder';
|
||||
|
||||
@override
|
||||
String get local_folder => 'Local folders';
|
||||
|
||||
@override
|
||||
String get add_local_folder => 'Add local folder';
|
||||
|
||||
@override
|
||||
String get rescan_local_folder => 'Rescan all local folders now';
|
||||
|
||||
@override
|
||||
String get export_metadata => 'Export metadata';
|
||||
|
||||
@override
|
||||
String get exported => 'Exported';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1736,4 +1736,22 @@ class AppLocalizationsHi extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get recommendations_similarity => 'Similarity:';
|
||||
|
||||
@override
|
||||
String get local_folder_structure => 'Structure of a local folder';
|
||||
|
||||
@override
|
||||
String get local_folder => 'Local folders';
|
||||
|
||||
@override
|
||||
String get add_local_folder => 'Add local folder';
|
||||
|
||||
@override
|
||||
String get rescan_local_folder => 'Rescan all local folders now';
|
||||
|
||||
@override
|
||||
String get export_metadata => 'Export metadata';
|
||||
|
||||
@override
|
||||
String get exported => 'Exported';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1740,4 +1740,22 @@ class AppLocalizationsId extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get recommendations_similarity => 'Similarity:';
|
||||
|
||||
@override
|
||||
String get local_folder_structure => 'Structure of a local folder';
|
||||
|
||||
@override
|
||||
String get local_folder => 'Local folders';
|
||||
|
||||
@override
|
||||
String get add_local_folder => 'Add local folder';
|
||||
|
||||
@override
|
||||
String get rescan_local_folder => 'Rescan all local folders now';
|
||||
|
||||
@override
|
||||
String get export_metadata => 'Export metadata';
|
||||
|
||||
@override
|
||||
String get exported => 'Exported';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1749,4 +1749,22 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get recommendations_similarity => 'Similarity:';
|
||||
|
||||
@override
|
||||
String get local_folder_structure => 'Structure of a local folder';
|
||||
|
||||
@override
|
||||
String get local_folder => 'Local folders';
|
||||
|
||||
@override
|
||||
String get add_local_folder => 'Add local folder';
|
||||
|
||||
@override
|
||||
String get rescan_local_folder => 'Rescan all local folders now';
|
||||
|
||||
@override
|
||||
String get export_metadata => 'Export metadata';
|
||||
|
||||
@override
|
||||
String get exported => 'Exported';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1748,6 +1748,24 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get recommendations_similarity => 'Similarity:';
|
||||
|
||||
@override
|
||||
String get local_folder_structure => 'Structure of a local folder';
|
||||
|
||||
@override
|
||||
String get local_folder => 'Local folders';
|
||||
|
||||
@override
|
||||
String get add_local_folder => 'Add local folder';
|
||||
|
||||
@override
|
||||
String get rescan_local_folder => 'Rescan all local folders now';
|
||||
|
||||
@override
|
||||
String get export_metadata => 'Export metadata';
|
||||
|
||||
@override
|
||||
String get exported => 'Exported';
|
||||
}
|
||||
|
||||
/// The translations for Portuguese, as used in Brazil (`pt_BR`).
|
||||
|
|
|
|||
|
|
@ -1750,4 +1750,22 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get recommendations_similarity => 'Similarity:';
|
||||
|
||||
@override
|
||||
String get local_folder_structure => 'Structure of a local folder';
|
||||
|
||||
@override
|
||||
String get local_folder => 'Local folders';
|
||||
|
||||
@override
|
||||
String get add_local_folder => 'Add local folder';
|
||||
|
||||
@override
|
||||
String get rescan_local_folder => 'Rescan all local folders now';
|
||||
|
||||
@override
|
||||
String get export_metadata => 'Export metadata';
|
||||
|
||||
@override
|
||||
String get exported => 'Exported';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1734,4 +1734,22 @@ class AppLocalizationsTh extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get recommendations_similarity => 'Similarity:';
|
||||
|
||||
@override
|
||||
String get local_folder_structure => 'Structure of a local folder';
|
||||
|
||||
@override
|
||||
String get local_folder => 'Local folders';
|
||||
|
||||
@override
|
||||
String get add_local_folder => 'Add local folder';
|
||||
|
||||
@override
|
||||
String get rescan_local_folder => 'Rescan all local folders now';
|
||||
|
||||
@override
|
||||
String get export_metadata => 'Export metadata';
|
||||
|
||||
@override
|
||||
String get exported => 'Exported';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1740,4 +1740,22 @@ class AppLocalizationsTr extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get recommendations_similarity => 'Similarity:';
|
||||
|
||||
@override
|
||||
String get local_folder_structure => 'Structure of a local folder';
|
||||
|
||||
@override
|
||||
String get local_folder => 'Local folders';
|
||||
|
||||
@override
|
||||
String get add_local_folder => 'Add local folder';
|
||||
|
||||
@override
|
||||
String get rescan_local_folder => 'Rescan all local folders now';
|
||||
|
||||
@override
|
||||
String get export_metadata => 'Export metadata';
|
||||
|
||||
@override
|
||||
String get exported => 'Exported';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1705,4 +1705,22 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get recommendations_similarity => 'Similarity:';
|
||||
|
||||
@override
|
||||
String get local_folder_structure => 'Structure of a local folder';
|
||||
|
||||
@override
|
||||
String get local_folder => 'Local folders';
|
||||
|
||||
@override
|
||||
String get add_local_folder => 'Add local folder';
|
||||
|
||||
@override
|
||||
String get rescan_local_folder => 'Rescan all local folders now';
|
||||
|
||||
@override
|
||||
String get export_metadata => 'Export metadata';
|
||||
|
||||
@override
|
||||
String get exported => 'Exported';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -286,6 +286,8 @@ class Settings {
|
|||
|
||||
late AlgorithmWeights? algorithmWeights;
|
||||
|
||||
List<String>? localFolders;
|
||||
|
||||
Settings({
|
||||
this.id = 227,
|
||||
this.updatedAt = 0,
|
||||
|
|
@ -414,6 +416,7 @@ class Settings {
|
|||
this.volumeBoostCap,
|
||||
this.downloadedOnlyMode = false,
|
||||
this.algorithmWeights,
|
||||
this.localFolders,
|
||||
});
|
||||
|
||||
Settings.fromJson(Map<String, dynamic> json) {
|
||||
|
|
@ -652,6 +655,7 @@ class Settings {
|
|||
algorithmWeights = json['algorithmWeights'] != null
|
||||
? AlgorithmWeights.fromJson(json['algorithmWeights'])
|
||||
: null;
|
||||
localFolders = json['localFolders'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
|
|
@ -804,6 +808,7 @@ class Settings {
|
|||
'downloadedOnlyMode': downloadedOnlyMode,
|
||||
if (algorithmWeights != null)
|
||||
'algorithmWeights': algorithmWeights!.toJson(),
|
||||
'localFolders': localFolders,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -332,8 +332,14 @@ class _SubtitlesWidgetSearchState extends ConsumerState<SubtitlesWidgetSearch> {
|
|||
try {
|
||||
final subtitle = subtitles![index];
|
||||
final storageProvider = StorageProvider();
|
||||
final animeDir =
|
||||
widget.chapter.archivePath != null &&
|
||||
widget.chapter.manga.value?.source == "local"
|
||||
? Directory(path.dirname(widget.chapter.archivePath!))
|
||||
: null;
|
||||
final chapterDirectory = (await storageProvider.getMangaChapterDirectory(
|
||||
widget.chapter,
|
||||
mangaMainDirectory: animeDir,
|
||||
))!;
|
||||
final subtitleFile = File(
|
||||
path.join(
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ part of 'extensions_provider.dart';
|
|||
// **************************************************************************
|
||||
|
||||
String _$getExtensionsStreamHash() =>
|
||||
r'af34092ebf31c784010110af746e3ee2731297bd';
|
||||
r'18790d3d4a7f52e5e7239c8726dcd09bb51d803a';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
|
|
|||
|
|
@ -73,6 +73,28 @@ class _SourcesScreenState extends ConsumerState<SourcesScreen> {
|
|||
label: Text(context.l10n.show_extensions),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
l10n.other,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SourceListTile(
|
||||
source: Source(
|
||||
name: "local",
|
||||
lang: "",
|
||||
itemType: widget.itemType,
|
||||
),
|
||||
itemType: widget.itemType,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
@ -178,6 +200,34 @@ class _SourcesScreenState extends ConsumerState<SourcesScreen> {
|
|||
item1.name!.compareTo(item2.name!),
|
||||
order: GroupedListOrder.ASC,
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
l10n.other,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SourceListTile(
|
||||
source: Source(
|
||||
name: "local",
|
||||
lang: "",
|
||||
itemType: widget.itemType,
|
||||
),
|
||||
itemType: widget.itemType,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ import 'package:mangayomi/utils/language.dart';
|
|||
class SourceListTile extends StatelessWidget {
|
||||
final ItemType itemType;
|
||||
final Source source;
|
||||
|
||||
bool get isLocal => source.name == "local" && source.lang == "";
|
||||
|
||||
const SourceListTile({
|
||||
super.key,
|
||||
required this.source,
|
||||
|
|
@ -24,21 +27,23 @@ class SourceListTile extends StatelessWidget {
|
|||
return Consumer(
|
||||
builder: (context, ref, child) => ListTile(
|
||||
onTap: () {
|
||||
final sources = isar.sources
|
||||
.filter()
|
||||
.idIsNotNull()
|
||||
.and()
|
||||
.itemTypeEqualTo(itemType)
|
||||
.findAllSync();
|
||||
isar.writeTxnSync(() {
|
||||
for (var src in sources) {
|
||||
isar.sources.putSync(
|
||||
src
|
||||
..lastUsed = src.id == source.id ? true : false
|
||||
..updatedAt = DateTime.now().millisecondsSinceEpoch,
|
||||
);
|
||||
}
|
||||
});
|
||||
if (!isLocal) {
|
||||
final sources = isar.sources
|
||||
.filter()
|
||||
.idIsNotNull()
|
||||
.and()
|
||||
.itemTypeEqualTo(itemType)
|
||||
.findAllSync();
|
||||
isar.writeTxnSync(() {
|
||||
for (var src in sources) {
|
||||
isar.sources.putSync(
|
||||
src
|
||||
..lastUsed = src.id == source.id ? true : false
|
||||
..updatedAt = DateTime.now().millisecondsSinceEpoch,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
context.push('/mangaHome', extra: (source, false));
|
||||
},
|
||||
leading: Container(
|
||||
|
|
@ -73,7 +78,15 @@ class SourceListTile extends StatelessWidget {
|
|||
),
|
||||
],
|
||||
),
|
||||
title: Text(source.name!),
|
||||
title: Text(
|
||||
!isLocal
|
||||
? source.name!
|
||||
: "${context.l10n.local_source} ${source.itemType == ItemType.manga
|
||||
? context.l10n.manga
|
||||
: source.itemType == ItemType.anime
|
||||
? context.l10n.anime
|
||||
: context.l10n.novel}",
|
||||
),
|
||||
trailing: SizedBox(
|
||||
width: 150,
|
||||
child: Row(
|
||||
|
|
@ -96,22 +109,23 @@ class SourceListTile extends StatelessWidget {
|
|||
},
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
IconButton(
|
||||
padding: const EdgeInsets.all(0),
|
||||
onPressed: () {
|
||||
isar.writeTxnSync(
|
||||
() => isar.sources.putSync(
|
||||
source
|
||||
..isPinned = !source.isPinned!
|
||||
..updatedAt = DateTime.now().millisecondsSinceEpoch,
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.push_pin_outlined,
|
||||
color: source.isPinned! ? context.primaryColor : null,
|
||||
if (!isLocal)
|
||||
IconButton(
|
||||
padding: const EdgeInsets.all(0),
|
||||
onPressed: () {
|
||||
isar.writeTxnSync(
|
||||
() => isar.sources.putSync(
|
||||
source
|
||||
..isPinned = !source.isPinned!
|
||||
..updatedAt = DateTime.now().millisecondsSinceEpoch,
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.push_pin_outlined,
|
||||
color: source.isPinned! ? context.primaryColor : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io'; // For I/O-operations
|
||||
import 'package:epubx/epubx.dart';
|
||||
import 'package:isar/isar.dart'; // Isar database package for local storage
|
||||
import 'package:mangayomi/main.dart'; // Exposes the global `isar` instance
|
||||
import 'package:mangayomi/models/settings.dart';
|
||||
import 'package:mangayomi/utils/extensions/others.dart';
|
||||
import 'package:path/path.dart' as p; // For manipulating file system paths
|
||||
import 'package:bot_toast/bot_toast.dart'; // For Exceptions
|
||||
import 'package:mangayomi/models/manga.dart'; // Has Manga model and ItemType enum
|
||||
|
|
@ -10,6 +14,26 @@ import 'package:mangayomi/providers/storage_provider.dart'; // Provides storage
|
|||
import 'package:riverpod_annotation/riverpod_annotation.dart'; // Annotations for code generation
|
||||
part 'file_scanner.g.dart';
|
||||
|
||||
@riverpod
|
||||
class LocalFoldersState extends _$LocalFoldersState {
|
||||
@override
|
||||
List<String> build() {
|
||||
return isar.settings.getSync(227)!.localFolders ?? [];
|
||||
}
|
||||
|
||||
void set(List<String> value) {
|
||||
final settings = isar.settings.getSync(227)!;
|
||||
state = value;
|
||||
isar.writeTxnSync(
|
||||
() => isar.settings.putSync(
|
||||
settings
|
||||
..localFolders = state
|
||||
..updatedAt = DateTime.now().millisecondsSinceEpoch,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Scans `Mangayomi/local` folder (if exists) for Mangas/Animes and imports in library.
|
||||
///
|
||||
/// **Folder structure:**
|
||||
|
|
@ -18,25 +42,37 @@ part 'file_scanner.g.dart';
|
|||
/// Mangayomi/local/MangaName/Chapter1/Page1.jpg
|
||||
/// Mangayomi/local/MangaName/Chapter2.cbz
|
||||
/// Mangayomi/local/AnimeName/Episode1.mp4
|
||||
/// Mangayomi/local/NovelName/NovelName.epub
|
||||
/// ```
|
||||
/// **Supported filetypes:** (taken from lib/modules/library/providers/local_archive.dart, line 98)
|
||||
/// ```
|
||||
/// Videotypes: mp4, mov, avi, flv, wmv, mpeg, mkv
|
||||
/// Imagetypes: jpg, jpeg, png, webp
|
||||
/// Archivetypes: cbz, zip, cbt, tar
|
||||
/// Other types: epub
|
||||
/// ```
|
||||
@riverpod
|
||||
Future<void> scanLocalLibrary(Ref ref) async {
|
||||
// Get /local directory
|
||||
final localDir = await _getLocalLibrary();
|
||||
final localDir = await getLocalLibrary();
|
||||
await _scanDirectory(ref, localDir);
|
||||
final customDirs = ref.read(localFoldersStateProvider);
|
||||
for (final dir in customDirs) {
|
||||
await _scanDirectory(ref, Directory(dir));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _scanDirectory(Ref ref, Directory? dir) async {
|
||||
// Don't do anything if /local doesn't exist
|
||||
if (localDir == null || !await localDir.exists()) return;
|
||||
if (dir == null || !await dir.exists()) return;
|
||||
|
||||
final dateNow = DateTime.now().millisecondsSinceEpoch;
|
||||
|
||||
// Fetch all existing mangas in library that are in /local (or \local)
|
||||
final List<Manga> existingMangas = await isar.mangas
|
||||
.filter()
|
||||
.sourceEqualTo("local")
|
||||
.or()
|
||||
.linkContains("Mangayomi/local")
|
||||
.or()
|
||||
.linkContains("Mangayomi\\local")
|
||||
|
|
@ -84,7 +120,7 @@ Future<void> scanLocalLibrary(Ref ref) async {
|
|||
}
|
||||
|
||||
// Iterate over each sub-directory (each representing a title, Manga or Anime)
|
||||
await for (final folder in localDir.list()) {
|
||||
await for (final folder in dir.list()) {
|
||||
if (folder is! Directory) continue;
|
||||
final title = p.basename(folder.path); // Anime/Manga title
|
||||
String relativePath = _getRelativePath(folder.path);
|
||||
|
|
@ -95,14 +131,19 @@ Future<void> scanLocalLibrary(Ref ref) async {
|
|||
final files = children.whereType<File>().toList();
|
||||
|
||||
// Determine itemtype
|
||||
final hasImagesFolders = subDirs.isNotEmpty;
|
||||
final hasImagesFolders = subDirs
|
||||
.where((e) => !e.path.endsWith("_subtitles"))
|
||||
.isNotEmpty;
|
||||
final hasArchives = files.any((f) => _isArchive(f.path));
|
||||
final hasVideos = files.any((f) => _isVideo(f.path));
|
||||
final hasEpubs = files.any((f) => _isEpub(f.path));
|
||||
late ItemType itemType;
|
||||
if (hasImagesFolders || hasArchives) {
|
||||
itemType = ItemType.manga;
|
||||
} else if (hasVideos) {
|
||||
itemType = ItemType.anime;
|
||||
} else if (hasEpubs) {
|
||||
itemType = ItemType.novel;
|
||||
} else {
|
||||
continue; // nothing to import from this folder
|
||||
}
|
||||
|
|
@ -115,7 +156,7 @@ Future<void> scanLocalLibrary(Ref ref) async {
|
|||
manga = mangaMap[relativePath]!;
|
||||
} else {
|
||||
manga = Manga(
|
||||
favorite: true,
|
||||
favorite: false,
|
||||
source: 'local',
|
||||
author: '',
|
||||
artist: '',
|
||||
|
|
@ -151,6 +192,26 @@ Future<void> scanLocalLibrary(Ref ref) async {
|
|||
} else if (imageFiles.isEmpty && manga.customCoverImage != null) {
|
||||
manga.customCoverImage = null;
|
||||
}
|
||||
|
||||
final jsonFiles = files.where((f) => _isJson(f.path)).toList();
|
||||
if (jsonFiles.isNotEmpty) {
|
||||
try {
|
||||
final str = await File(jsonFiles.first.path).readAsString();
|
||||
final data = jsonDecode(str) as Map<String, dynamic>?;
|
||||
manga.name = data?["name"];
|
||||
manga.description = data?["description"];
|
||||
manga.artist = data?["artist"];
|
||||
manga.author = data?["author"];
|
||||
manga.genre = data?["genre"]?.cast<String>();
|
||||
manga.status = data?["status"] != null
|
||||
? Status.values[data!["status"]]
|
||||
: Status.unknown;
|
||||
manga.lastUpdate = dateNow;
|
||||
} catch (e) {
|
||||
BotToast.showText(text: "Error reading metadata: $e");
|
||||
}
|
||||
}
|
||||
|
||||
processedMangas.add(manga);
|
||||
|
||||
// Scan chapters/episodes
|
||||
|
|
@ -168,6 +229,11 @@ Future<void> scanLocalLibrary(Ref ref) async {
|
|||
final videos = files.where((f) => _isVideo(f.path)).toList();
|
||||
addNewChapters(videos, false);
|
||||
}
|
||||
if (hasEpubs) {
|
||||
// Each .epub
|
||||
final epubs = files.where((f) => _isEpub(f.path)).toList();
|
||||
addNewChapters(epubs, false);
|
||||
}
|
||||
}
|
||||
|
||||
final changedMangas = <Manga>[];
|
||||
|
|
@ -193,6 +259,8 @@ Future<void> scanLocalLibrary(Ref ref) async {
|
|||
// Fetch all existing mangas in library that are in /local (or \local)
|
||||
final savedMangas = await isar.mangas
|
||||
.filter()
|
||||
.sourceEqualTo("local")
|
||||
.or()
|
||||
.linkContains("Mangayomi/local")
|
||||
.or()
|
||||
.linkContains("Mangayomi\\local")
|
||||
|
|
@ -223,20 +291,51 @@ Future<void> scanLocalLibrary(Ref ref) async {
|
|||
final itemName = p.basename(p.dirname(chapterPath));
|
||||
final manga = mangaByName[itemName];
|
||||
if (manga != null) {
|
||||
final chap = Chapter(
|
||||
mangaId: manga.id,
|
||||
name:
|
||||
pathBool[1] // If Chapter is an image folder or archive/video
|
||||
? p.basename(chapterPath)
|
||||
: p.basenameWithoutExtension(chapterPath),
|
||||
dateUpload: dateNow.toString(),
|
||||
archivePath: chapterPath,
|
||||
);
|
||||
final chapterFile = File(chapterPath);
|
||||
if (manga.itemType == ItemType.novel) {
|
||||
final bytes = await chapterFile.readAsBytes();
|
||||
final book = await EpubReader.readBook(bytes);
|
||||
if (book.Content != null && book.Content!.Images != null) {
|
||||
final coverImage =
|
||||
book.Content!.Images!.containsKey("media/file0.png")
|
||||
? book.Content!.Images!["media/file0.png"]!.Content
|
||||
: book.Content!.Images!.values.first.Content;
|
||||
manga.customCoverImage = coverImage;
|
||||
saveManga++;
|
||||
}
|
||||
for (var chapter in book.Chapters ?? []) {
|
||||
chaptersToSave.add(
|
||||
Chapter(
|
||||
mangaId: manga.id,
|
||||
name: chapter.Title is String && chapter.Title.isEmpty
|
||||
? "Book"
|
||||
: chapter.Title,
|
||||
archivePath: chapterPath,
|
||||
downloadSize: chapterFile.existsSync()
|
||||
? chapterFile.lengthSync().formattedFileSize()
|
||||
: null,
|
||||
)..manga.value = manga,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
final chap = Chapter(
|
||||
mangaId: manga.id,
|
||||
name:
|
||||
pathBool[1] // If Chapter is an image folder or archive/video
|
||||
? p.basename(chapterPath)
|
||||
: p.basenameWithoutExtension(chapterPath),
|
||||
dateUpload: dateNow.toString(),
|
||||
archivePath: chapterPath,
|
||||
downloadSize: chapterFile.existsSync()
|
||||
? chapterFile.lengthSync().formattedFileSize()
|
||||
: null,
|
||||
);
|
||||
chaptersToSave.add(chap);
|
||||
}
|
||||
if (manga.lastUpdate != dateNow) {
|
||||
manga.lastUpdate = dateNow;
|
||||
saveManga++;
|
||||
}
|
||||
chaptersToSave.add(chap);
|
||||
}
|
||||
}
|
||||
try {
|
||||
|
|
@ -272,7 +371,7 @@ Future<void> scanLocalLibrary(Ref ref) async {
|
|||
}
|
||||
|
||||
/// Returns the `/local` directory inside the app's default storage.
|
||||
Future<Directory?> _getLocalLibrary() async {
|
||||
Future<Directory?> getLocalLibrary() async {
|
||||
try {
|
||||
final dir = await StorageProvider().getDefaultDirectory();
|
||||
return dir == null ? null : Directory(p.join(dir.path, 'local'));
|
||||
|
|
@ -309,6 +408,12 @@ String _getRelativePath(dir) {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns if file is a json
|
||||
bool _isJson(String path) {
|
||||
final ext = p.extension(path).toLowerCase();
|
||||
return ext == '.json';
|
||||
}
|
||||
|
||||
/// Returns if file is an image
|
||||
bool _isImage(String path) {
|
||||
final ext = p.extension(path).toLowerCase();
|
||||
|
|
@ -335,3 +440,9 @@ bool _isVideo(String path) {
|
|||
};
|
||||
return videoExtensions.contains(ext);
|
||||
}
|
||||
|
||||
/// Returns if file is an epub or html
|
||||
bool _isEpub(String path) {
|
||||
final ext = p.extension(path).toLowerCase();
|
||||
return ext == '.epub';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ part of 'file_scanner.dart';
|
|||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$scanLocalLibraryHash() => r'efbad9aa5fa4233e260a2e132389c23b40ef515a';
|
||||
String _$scanLocalLibraryHash() => r'80267fbc1da18bbd7ef6c8c4ef87bcea9ad99869';
|
||||
|
||||
/// Scans `Mangayomi/local` folder (if exists) for Mangas/Animes and imports in library.
|
||||
///
|
||||
|
|
@ -16,12 +16,15 @@ String _$scanLocalLibraryHash() => r'efbad9aa5fa4233e260a2e132389c23b40ef515a';
|
|||
/// Mangayomi/local/MangaName/Chapter1/Page1.jpg
|
||||
/// Mangayomi/local/MangaName/Chapter2.cbz
|
||||
/// Mangayomi/local/AnimeName/Episode1.mp4
|
||||
/// Mangayomi/local/NovelName/Chapter1.epub
|
||||
/// Mangayomi/local/NovelName/Chapter2.html
|
||||
/// ```
|
||||
/// **Supported filetypes:** (taken from lib/modules/library/providers/local_archive.dart, line 98)
|
||||
/// ```
|
||||
/// Videotypes: mp4, mov, avi, flv, wmv, mpeg, mkv
|
||||
/// Imagetypes: jpg, jpeg, png, webp
|
||||
/// Archivetypes: cbz, zip, cbt, tar
|
||||
/// Other types: epub, html
|
||||
/// ```
|
||||
///
|
||||
/// Copied from [scanLocalLibrary].
|
||||
|
|
@ -39,5 +42,21 @@ final scanLocalLibraryProvider = AutoDisposeFutureProvider<void>.internal(
|
|||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef ScanLocalLibraryRef = AutoDisposeFutureProviderRef<void>;
|
||||
String _$localFoldersStateHash() => r'3bea18b0e5e6d9d1950e12293825fc85b1a0de6c';
|
||||
|
||||
/// See also [LocalFoldersState].
|
||||
@ProviderFor(LocalFoldersState)
|
||||
final localFoldersStateProvider =
|
||||
AutoDisposeNotifierProvider<LocalFoldersState, List<String>>.internal(
|
||||
LocalFoldersState.new,
|
||||
name: r'localFoldersStateProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$localFoldersStateHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$LocalFoldersState = AutoDisposeNotifier<List<String>>;
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:draggable_menu/draggable_menu.dart';
|
||||
|
|
@ -32,6 +33,7 @@ import 'package:mangayomi/modules/widgets/custom_draggable_tabbar.dart';
|
|||
import 'package:mangayomi/modules/widgets/custom_extended_image_provider.dart';
|
||||
import 'package:mangayomi/providers/l10n_providers.dart';
|
||||
import 'package:mangayomi/providers/storage_provider.dart';
|
||||
import 'package:mangayomi/services/http/m_client.dart';
|
||||
import 'package:mangayomi/utils/extensions/string_extensions.dart';
|
||||
import 'package:mangayomi/utils/utils.dart';
|
||||
import 'package:mangayomi/utils/cached_network.dart';
|
||||
|
|
@ -633,9 +635,13 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
|
|||
value: 4,
|
||||
child: Text(l10n.extension_settings),
|
||||
),
|
||||
PopupMenuItem<int>(
|
||||
value: 5,
|
||||
child: Text(l10n.export_metadata),
|
||||
),
|
||||
];
|
||||
},
|
||||
onSelected: (value) {
|
||||
onSelected: (value) async {
|
||||
switch (value) {
|
||||
case 0:
|
||||
widget.checkForUpdate(true);
|
||||
|
|
@ -679,6 +685,58 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
|
|||
extra: source,
|
||||
);
|
||||
break;
|
||||
case 5:
|
||||
try {
|
||||
final result = await FilePicker.platform
|
||||
.getDirectoryPath();
|
||||
if (result != null) {
|
||||
final client = MClient.init();
|
||||
final coverFile = File(
|
||||
p.join(result, "cover.jpg"),
|
||||
);
|
||||
final metadataFile = File(
|
||||
p.join(result, "metadata.json"),
|
||||
);
|
||||
final headers =
|
||||
widget.manga!.isLocalArchive!
|
||||
? null
|
||||
: ref.read(
|
||||
headersProvider(
|
||||
source: widget.manga!.source!,
|
||||
lang: widget.manga!.lang!,
|
||||
sourceId:
|
||||
widget.manga!.sourceId,
|
||||
),
|
||||
);
|
||||
final imageUrl = toImgUrl(
|
||||
widget.manga!.customCoverFromTracker ??
|
||||
widget.manga!.imageUrl ??
|
||||
"",
|
||||
);
|
||||
final res = await client.get(
|
||||
Uri.parse(imageUrl),
|
||||
headers: headers,
|
||||
);
|
||||
await coverFile.writeAsBytes(
|
||||
res.bodyBytes,
|
||||
);
|
||||
await metadataFile.writeAsString(
|
||||
jsonEncode({
|
||||
"name": widget.manga!.name,
|
||||
"description":
|
||||
widget.manga!.description,
|
||||
"artist": widget.manga!.artist,
|
||||
"author": widget.manga!.author,
|
||||
"genre": widget.manga!.genre,
|
||||
"status": widget.manga!.status.index,
|
||||
}),
|
||||
);
|
||||
botToast(l10n.exported);
|
||||
}
|
||||
} catch (e) {
|
||||
botToast("Failed to export metadata: $e");
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
),
|
||||
|
|
@ -1454,7 +1512,7 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
|
|||
),
|
||||
],
|
||||
),
|
||||
isLocalArchive ? _action() : _actionFavouriteAndWebview(),
|
||||
_actionFavouriteAndWebview(),
|
||||
Container(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
child: Column(
|
||||
|
|
@ -1875,53 +1933,58 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
|
|||
child: Row(
|
||||
children: [
|
||||
Expanded(child: widget.action!),
|
||||
Expanded(child: _smartUpdateDays()),
|
||||
if (!isLocalArchive) Expanded(child: _smartUpdateDays()),
|
||||
Expanded(
|
||||
child: widget.itemType == ItemType.novel
|
||||
? SizedBox.shrink()
|
||||
: _action(),
|
||||
),
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
elevation: 0,
|
||||
),
|
||||
onPressed: () async {
|
||||
final manga = widget.manga!;
|
||||
if (!isLocalArchive)
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
elevation: 0,
|
||||
),
|
||||
onPressed: () async {
|
||||
final manga = widget.manga!;
|
||||
|
||||
final source = getSource(
|
||||
widget.manga!.lang!,
|
||||
widget.manga!.source!,
|
||||
widget.manga!.sourceId,
|
||||
);
|
||||
final url =
|
||||
"${source!.baseUrl}${widget.manga!.link!.getUrlWithoutDomain}";
|
||||
final source = getSource(
|
||||
widget.manga!.lang!,
|
||||
widget.manga!.source!,
|
||||
widget.manga!.sourceId,
|
||||
);
|
||||
final url =
|
||||
"${source!.baseUrl}${widget.manga!.link!.getUrlWithoutDomain}";
|
||||
|
||||
Map<String, dynamic> data = {
|
||||
'url': url,
|
||||
'sourceId': source.id.toString(),
|
||||
'title': manga.name!,
|
||||
};
|
||||
context.push("/mangawebview", extra: data);
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(Icons.public, size: 20, color: context.secondaryColor),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'WebView',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
Map<String, dynamic> data = {
|
||||
'url': url,
|
||||
'sourceId': source.id.toString(),
|
||||
'title': manga.name!,
|
||||
};
|
||||
context.push("/mangawebview", extra: data);
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.public,
|
||||
size: 20,
|
||||
color: context.secondaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'WebView',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: context.secondaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -160,37 +160,36 @@ class _MangaDetailsViewState extends ConsumerState<MangaDetailsView> {
|
|||
},
|
||||
),
|
||||
body: MangaDetailView(
|
||||
titleDescription: isLocalArchive
|
||||
? Container()
|
||||
: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.manga.author!,
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
titleDescription: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.manga.author ?? "Unknown",
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
Icon(getMangaStatusIcon(widget.manga.status), size: 14),
|
||||
const SizedBox(width: 4),
|
||||
Text(getMangaStatusName(widget.manga.status, context)),
|
||||
if (!isLocalArchive) const Text(' • '),
|
||||
if (!isLocalArchive) Text(widget.manga.source!),
|
||||
if (!isLocalArchive)
|
||||
Text(' (${widget.manga.lang!.toUpperCase()})'),
|
||||
if (!isLocalArchive && !widget.sourceExist)
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(3),
|
||||
child: Icon(
|
||||
Icons.warning_amber,
|
||||
color: Colors.deepOrangeAccent,
|
||||
size: 14,
|
||||
),
|
||||
),
|
||||
Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
Icon(getMangaStatusIcon(widget.manga.status), size: 14),
|
||||
const SizedBox(width: 4),
|
||||
Text(getMangaStatusName(widget.manga.status, context)),
|
||||
const Text(' • '),
|
||||
Text(widget.manga.source!),
|
||||
Text(' (${widget.manga.lang!.toUpperCase()})'),
|
||||
if (!widget.sourceExist)
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(3),
|
||||
child: Icon(
|
||||
Icons.warning_amber,
|
||||
color: Colors.deepOrangeAccent,
|
||||
size: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
action: widget.manga.favorite!
|
||||
? SizedBox(
|
||||
child: ElevatedButton(
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ Future<dynamic> updateMangaDetail(
|
|||
}) async {
|
||||
try {
|
||||
final manga = isar.mangas.getSync(mangaId!);
|
||||
if (manga!.chapters.isNotEmpty && isInit) {
|
||||
if ((manga!.isLocalArchive ?? false) ||
|
||||
(manga.chapters.isNotEmpty && isInit)) {
|
||||
return;
|
||||
}
|
||||
final source = getSource(manga.lang!, manga.source!, manga.sourceId);
|
||||
|
|
|
|||
|
|
@ -146,6 +146,21 @@ class ChapterListTileWidget extends ConsumerWidget {
|
|||
),
|
||||
],
|
||||
),
|
||||
if (chapter.downloadSize != null)
|
||||
Row(
|
||||
children: [
|
||||
const Text(' • '),
|
||||
Text(
|
||||
chapter.downloadSize!,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: context.isLight
|
||||
? Colors.black.withValues(alpha: 0.4)
|
||||
: Colors.white.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing:
|
||||
|
|
|
|||
|
|
@ -63,7 +63,8 @@ class _MangaHomeScreenState extends ConsumerState<MangaHomeScreen> {
|
|||
? 2
|
||||
: 0;
|
||||
late Source source = widget.source;
|
||||
late List<dynamic> filters = getFilterList(source: source);
|
||||
late bool isLocal = source.name == "local" && source.lang == "";
|
||||
late List<dynamic> filters = isLocal ? [] : getFilterList(source: source);
|
||||
final List<MManga> _mangaList = [];
|
||||
List<TypeMangaSelector> _types(BuildContext context) {
|
||||
final l10n = l10nLocalizations(context)!;
|
||||
|
|
@ -127,8 +128,10 @@ class _MangaHomeScreenState extends ConsumerState<MangaHomeScreen> {
|
|||
AsyncValue<MPages?>? _getManga;
|
||||
int _length = 0;
|
||||
bool _isFiltering = false;
|
||||
late final supportsLatest = ref.watch(supportsLatestProvider(source: source));
|
||||
late final filterList = getFilterList(source: source);
|
||||
late final supportsLatest = isLocal
|
||||
? true
|
||||
: ref.watch(supportsLatestProvider(source: source));
|
||||
late final filterList = isLocal ? [] : getFilterList(source: source);
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_selectedIndex == 2 && (_isSearch && _query.isNotEmpty) ||
|
||||
|
|
@ -161,7 +164,15 @@ class _MangaHomeScreenState extends ConsumerState<MangaHomeScreen> {
|
|||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("${source.name}"),
|
||||
Text(
|
||||
!isLocal
|
||||
? "${source.name}"
|
||||
: "${context.l10n.local_source} ${source.itemType == ItemType.manga
|
||||
? context.l10n.manga
|
||||
: source.itemType == ItemType.anime
|
||||
? context.l10n.anime
|
||||
: context.l10n.novel}",
|
||||
),
|
||||
source.notes != null && source.notes!.isNotEmpty
|
||||
? SizedBox(
|
||||
height: 20,
|
||||
|
|
@ -267,44 +278,45 @@ class _MangaHomeScreenState extends ConsumerState<MangaHomeScreen> {
|
|||
},
|
||||
onSelected: (value) {},
|
||||
),
|
||||
PopupMenuButton(
|
||||
popUpAnimationStyle: popupAnimationStyle,
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem<int>(
|
||||
value: 0,
|
||||
child: Text(context.l10n.open_in_browser),
|
||||
),
|
||||
PopupMenuItem<int>(
|
||||
value: 1,
|
||||
child: Text(context.l10n.settings),
|
||||
),
|
||||
];
|
||||
},
|
||||
onSelected: (value) async {
|
||||
if (value == 0) {
|
||||
final baseUrl = ref.watch(
|
||||
sourceBaseUrlProvider(source: source),
|
||||
);
|
||||
Map<String, dynamic> data = {
|
||||
'url': baseUrl,
|
||||
'sourceId': source.id.toString(),
|
||||
'title': '',
|
||||
};
|
||||
context.push("/mangawebview", extra: data);
|
||||
} else {
|
||||
final res = await context.push(
|
||||
'/extension_detail',
|
||||
extra: source,
|
||||
);
|
||||
if (res != null && mounted) {
|
||||
setState(() {
|
||||
source = res as Source;
|
||||
});
|
||||
if (!isLocal)
|
||||
PopupMenuButton(
|
||||
popUpAnimationStyle: popupAnimationStyle,
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem<int>(
|
||||
value: 0,
|
||||
child: Text(context.l10n.open_in_browser),
|
||||
),
|
||||
PopupMenuItem<int>(
|
||||
value: 1,
|
||||
child: Text(context.l10n.settings),
|
||||
),
|
||||
];
|
||||
},
|
||||
onSelected: (value) async {
|
||||
if (value == 0) {
|
||||
final baseUrl = ref.watch(
|
||||
sourceBaseUrlProvider(source: source),
|
||||
);
|
||||
Map<String, dynamic> data = {
|
||||
'url': baseUrl,
|
||||
'sourceId': source.id.toString(),
|
||||
'title': '',
|
||||
};
|
||||
context.push("/mangawebview", extra: data);
|
||||
} else {
|
||||
final res = await context.push(
|
||||
'/extension_detail',
|
||||
extra: source,
|
||||
);
|
||||
if (res != null && mounted) {
|
||||
setState(() {
|
||||
source = res as Source;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
bottom: PreferredSize(
|
||||
preferredSize: Size.fromHeight(AppBar().preferredSize.height * 0.8),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mangayomi/l10n/generated/app_localizations.dart';
|
||||
import 'package:mangayomi/modules/library/providers/file_scanner.dart';
|
||||
import 'package:mangayomi/modules/more/settings/downloads/providers/downloads_state_provider.dart';
|
||||
import 'package:mangayomi/providers/l10n_providers.dart';
|
||||
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
|
||||
|
|
@ -18,6 +21,7 @@ class _DownloadsScreenState extends ConsumerState<DownloadsScreen> {
|
|||
final saveAsCBZArchiveState = ref.watch(saveAsCBZArchiveStateProvider);
|
||||
final onlyOnWifiState = ref.watch(onlyOnWifiStateProvider);
|
||||
final concurrentDownloads = ref.watch(concurrentDownloadsStateProvider);
|
||||
final localFolders = ref.watch(localFoldersStateProvider);
|
||||
final l10n = l10nLocalizations(context);
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(l10n!.downloads)),
|
||||
|
|
@ -106,6 +110,291 @@ class _DownloadsScreenState extends ConsumerState<DownloadsScreen> {
|
|||
style: TextStyle(fontSize: 11, color: context.secondaryColor),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
onTap: () async => ref.read(scanLocalLibraryProvider.future),
|
||||
title: Text(context.l10n.rescan_local_folder),
|
||||
),
|
||||
ListTile(
|
||||
onTap: () async {
|
||||
final result = await FilePicker.platform.getDirectoryPath();
|
||||
if (result != null) {
|
||||
final temp = localFolders.toList();
|
||||
temp.add(result);
|
||||
ref.read(localFoldersStateProvider.notifier).set(temp);
|
||||
}
|
||||
},
|
||||
title: Text(context.l10n.add_local_folder),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.local_folder,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => _showHelpDialog(context),
|
||||
label: const Icon(Icons.question_mark),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
FutureBuilder(
|
||||
future: getLocalLibrary(),
|
||||
builder: (context, snapshot) => snapshot.data?.path != null
|
||||
? _buildLocalFolder(
|
||||
l10n,
|
||||
localFolders,
|
||||
snapshot.data!.path,
|
||||
isDefault: true,
|
||||
)
|
||||
: Container(),
|
||||
),
|
||||
...localFolders.map(
|
||||
(e) => _buildLocalFolder(l10n, localFolders, e),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showHelpDialog(BuildContext context) {
|
||||
final data = (
|
||||
"LocalFolder",
|
||||
[
|
||||
(
|
||||
"MangaName",
|
||||
[
|
||||
("cover.jpg", Icons.image_outlined),
|
||||
(
|
||||
"Chapter1",
|
||||
[
|
||||
("Page1.jpg", Icons.image_outlined),
|
||||
("Page2.jpeg", Icons.image_outlined),
|
||||
("Page3.png", Icons.image_outlined),
|
||||
("Page4.webp", Icons.image_outlined),
|
||||
],
|
||||
),
|
||||
("Chapter2.cbz", Icons.folder_zip_outlined),
|
||||
("Chapter3.zip", Icons.folder_zip_outlined),
|
||||
("Chapter4.cbt", Icons.folder_zip_outlined),
|
||||
("Chapter5.tar", Icons.folder_zip_outlined),
|
||||
],
|
||||
),
|
||||
(
|
||||
"AnimeName",
|
||||
[
|
||||
("cover.jpg", Icons.image_outlined),
|
||||
("Episode1.mp4", Icons.video_file_outlined),
|
||||
(
|
||||
"Episode1_subtitles",
|
||||
[
|
||||
("en.srt", Icons.subtitles_outlined),
|
||||
("de.srt", Icons.subtitles_outlined),
|
||||
],
|
||||
),
|
||||
("Episode2.mov", Icons.video_file_outlined),
|
||||
("Episode3.avi", Icons.video_file_outlined),
|
||||
("Episode4.flv", Icons.video_file_outlined),
|
||||
("Episode5.wmv", Icons.video_file_outlined),
|
||||
("Episode6.mpeg", Icons.video_file_outlined),
|
||||
("Episode7.mkv", Icons.video_file_outlined),
|
||||
],
|
||||
),
|
||||
(
|
||||
"NovelName",
|
||||
[
|
||||
("cover.jpg", Icons.image_outlined),
|
||||
("NovelName.epub", Icons.book_outlined),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Widget buildSubFolder((String, dynamic) data, int level) {
|
||||
if (data.$2 is List) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
for (int i = 1; i < level; i++)
|
||||
const WidgetSpan(child: SizedBox(width: 20)),
|
||||
if (level > 0)
|
||||
WidgetSpan(child: Icon(Icons.subdirectory_arrow_right)),
|
||||
WidgetSpan(child: Icon(Icons.folder)),
|
||||
const WidgetSpan(child: SizedBox(width: 5)),
|
||||
TextSpan(text: data.$1),
|
||||
],
|
||||
),
|
||||
),
|
||||
...(data.$2 as List<(String, dynamic)>).map(
|
||||
(e) => buildSubFolder(e, level + 1),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
for (int i = 1; i < level; i++)
|
||||
const WidgetSpan(child: SizedBox(width: 20)),
|
||||
if (level > 0)
|
||||
WidgetSpan(child: Icon(Icons.subdirectory_arrow_right)),
|
||||
WidgetSpan(child: Icon(data.$2 as IconData)),
|
||||
const WidgetSpan(child: SizedBox(width: 5)),
|
||||
TextSpan(text: data.$1),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
return AlertDialog(
|
||||
title: Text(context.l10n.local_folder_structure),
|
||||
content: SizedBox(
|
||||
width: context.width(0.6),
|
||||
height: context.height(0.8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: SingleChildScrollView(child: buildSubFolder(data, 0)),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(context.l10n.cancel),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLocalFolder(
|
||||
AppLocalizations l10n,
|
||||
List<String> localFolders,
|
||||
String folder, {
|
||||
bool isDefault = false,
|
||||
}) {
|
||||
return Padding(
|
||||
key: Key('folder_${folder.hashCode}'),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Card(
|
||||
child: Column(
|
||||
children: [
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
shadowColor: Colors.transparent,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(0),
|
||||
bottomRight: Radius.circular(0),
|
||||
topRight: Radius.circular(10),
|
||||
topLeft: Radius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
onPressed: null,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
const Icon(Icons.label_outline_rounded),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(child: Text(folder)),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!isDefault)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
return AlertDialog(
|
||||
title: Text(l10n.delete),
|
||||
content: Text("${l10n.delete} $folder"),
|
||||
actions: [
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(l10n.cancel),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
final temp = localFolders
|
||||
.toList();
|
||||
temp.removeAt(
|
||||
temp.indexOf(folder),
|
||||
);
|
||||
ref
|
||||
.read(
|
||||
localFoldersStateProvider
|
||||
.notifier,
|
||||
)
|
||||
.set(temp);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(l10n.ok),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.delete_outlined),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -328,58 +328,69 @@ Future<void> pushToMangaReaderDetail({
|
|||
bool addToFavourite = false,
|
||||
}) async {
|
||||
int? mangaId;
|
||||
if (archiveId == null) {
|
||||
final manga =
|
||||
mangaM ??
|
||||
Manga(
|
||||
imageUrl: getManga!.imageUrl,
|
||||
name: getManga.name!.trim().trimLeft().trimRight(),
|
||||
genre: getManga.genre?.map((e) => e.toString()).toList() ?? [],
|
||||
author: getManga.author ?? "",
|
||||
status: getManga.status ?? Status.unknown,
|
||||
description: getManga.description ?? "",
|
||||
link: getManga.link,
|
||||
source: source,
|
||||
lang: lang,
|
||||
lastUpdate: 0,
|
||||
itemType: itemType ?? ItemType.manga,
|
||||
artist: getManga.artist ?? '',
|
||||
sourceId: sourceId,
|
||||
);
|
||||
final empty = isar.mangas
|
||||
.filter()
|
||||
.langEqualTo(lang)
|
||||
.nameEqualTo(manga.name)
|
||||
.sourceEqualTo(manga.source)
|
||||
.isEmptySync();
|
||||
if (empty) {
|
||||
isar.writeTxnSync(() {
|
||||
isar.mangas.putSync(
|
||||
manga..updatedAt = DateTime.now().millisecondsSinceEpoch,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
isar.writeTxnSync(() {
|
||||
isar.mangas.putSync(manga);
|
||||
});
|
||||
}
|
||||
mangaId = isar.mangas
|
||||
.filter()
|
||||
.isLocalArchiveEqualTo(true)
|
||||
.sourceEqualTo("local")
|
||||
.nameEqualTo(getManga?.name)
|
||||
.findFirstSync()
|
||||
?.id;
|
||||
|
||||
mangaId = isar.mangas
|
||||
.filter()
|
||||
.langEqualTo(lang)
|
||||
.nameEqualTo(manga.name)
|
||||
.sourceEqualTo(manga.source)
|
||||
.findAllSync()
|
||||
.firstWhere(
|
||||
(element) =>
|
||||
element.sourceId == null ? true : element.sourceId == sourceId,
|
||||
)
|
||||
.id!;
|
||||
} else {
|
||||
mangaId = archiveId;
|
||||
if (mangaId == null) {
|
||||
if (archiveId == null) {
|
||||
final manga =
|
||||
mangaM ??
|
||||
Manga(
|
||||
imageUrl: getManga!.imageUrl,
|
||||
name: getManga.name!.trim().trimLeft().trimRight(),
|
||||
genre: getManga.genre?.map((e) => e.toString()).toList() ?? [],
|
||||
author: getManga.author ?? "",
|
||||
status: getManga.status ?? Status.unknown,
|
||||
description: getManga.description ?? "",
|
||||
link: getManga.link,
|
||||
source: source,
|
||||
lang: lang,
|
||||
lastUpdate: 0,
|
||||
itemType: itemType ?? ItemType.manga,
|
||||
artist: getManga.artist ?? '',
|
||||
sourceId: sourceId,
|
||||
);
|
||||
final empty = isar.mangas
|
||||
.filter()
|
||||
.langEqualTo(lang)
|
||||
.nameEqualTo(manga.name)
|
||||
.sourceEqualTo(manga.source)
|
||||
.isEmptySync();
|
||||
if (empty) {
|
||||
isar.writeTxnSync(() {
|
||||
isar.mangas.putSync(
|
||||
manga..updatedAt = DateTime.now().millisecondsSinceEpoch,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
isar.writeTxnSync(() {
|
||||
isar.mangas.putSync(manga);
|
||||
});
|
||||
}
|
||||
|
||||
mangaId = isar.mangas
|
||||
.filter()
|
||||
.langEqualTo(lang)
|
||||
.nameEqualTo(manga.name)
|
||||
.sourceEqualTo(manga.source)
|
||||
.findAllSync()
|
||||
.firstWhere(
|
||||
(element) =>
|
||||
element.sourceId == null ? true : element.sourceId == sourceId,
|
||||
)
|
||||
.id!;
|
||||
} else {
|
||||
mangaId = archiveId;
|
||||
}
|
||||
}
|
||||
|
||||
final mang = isar.mangas.getSync(mangaId);
|
||||
if (mang!.sourceId == null) {
|
||||
if (mang!.sourceId == null && !(mang.isLocalArchive ?? false)) {
|
||||
isar.writeTxnSync(() {
|
||||
isar.mangas.putSync(mang..sourceId = sourceId);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:mangayomi/eval/lib.dart';
|
||||
import 'package:mangayomi/eval/model/m_manga.dart';
|
||||
import 'package:mangayomi/eval/model/m_pages.dart';
|
||||
import 'package:mangayomi/main.dart';
|
||||
import 'package:mangayomi/models/manga.dart';
|
||||
import 'package:mangayomi/models/source.dart';
|
||||
import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
|
@ -12,6 +18,27 @@ Future<MPages?> getLatestUpdates(
|
|||
required Source source,
|
||||
required int page,
|
||||
}) async {
|
||||
if (source.name == "local" && source.lang == "") {
|
||||
final result =
|
||||
(await isar.mangas
|
||||
.filter()
|
||||
.itemTypeEqualTo(source.itemType)
|
||||
.group(
|
||||
(q) => q
|
||||
.sourceEqualTo("local")
|
||||
.or()
|
||||
.linkContains("Mangayomi/local")
|
||||
.or()
|
||||
.linkContains("Mangayomi\\local"),
|
||||
)
|
||||
.sortByDateAddedDesc()
|
||||
.offset(max(0, page - 1) * 50)
|
||||
.limit(50)
|
||||
.findAll())
|
||||
.map((e) => MManga(name: e.name))
|
||||
.toList();
|
||||
return MPages(list: result, hasNextPage: true);
|
||||
}
|
||||
return getExtensionService(
|
||||
source,
|
||||
ref.read(androidProxyServerStateProvider),
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ part of 'get_latest_updates.dart';
|
|||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$getLatestUpdatesHash() => r'fd4ece1d796e079a469e5f80f456ee821ff0bc03';
|
||||
String _$getLatestUpdatesHash() => r'b6a138d2e2202fc8fec29c1e4d31c4a4ddb8d848';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:mangayomi/eval/lib.dart';
|
||||
import 'package:mangayomi/eval/model/m_manga.dart';
|
||||
import 'package:mangayomi/eval/model/m_pages.dart';
|
||||
import 'package:mangayomi/main.dart';
|
||||
import 'package:mangayomi/models/manga.dart';
|
||||
import 'package:mangayomi/models/source.dart';
|
||||
import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
|
@ -12,6 +18,27 @@ Future<MPages?> getPopular(
|
|||
required Source source,
|
||||
required int page,
|
||||
}) async {
|
||||
if (source.name == "local" && source.lang == "") {
|
||||
final result =
|
||||
(await isar.mangas
|
||||
.filter()
|
||||
.itemTypeEqualTo(source.itemType)
|
||||
.group(
|
||||
(q) => q
|
||||
.sourceEqualTo("local")
|
||||
.or()
|
||||
.linkContains("Mangayomi/local")
|
||||
.or()
|
||||
.linkContains("Mangayomi\\local"),
|
||||
)
|
||||
.sortByName()
|
||||
.offset(max(0, page - 1) * 50)
|
||||
.limit(50)
|
||||
.findAll())
|
||||
.map((e) => MManga(name: e.name))
|
||||
.toList();
|
||||
return MPages(list: result, hasNextPage: true);
|
||||
}
|
||||
return getExtensionService(
|
||||
source,
|
||||
ref.read(androidProxyServerStateProvider),
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ part of 'get_popular.dart';
|
|||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$getPopularHash() => r'5fd933ce7e2b9c2dd113b7642ed54c1a1196f638';
|
||||
String _$getPopularHash() => r'85152c70d6873dbf9c7f1b8a7f9d3846c53b9863';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
|
|
|||
|
|
@ -32,9 +32,13 @@ Future<(List<Video>, bool, List<String>, Directory?)> getVideoList(
|
|||
);
|
||||
List<String> infoHashes = [];
|
||||
if (await File(mp4animePath).exists() || isLocalArchive) {
|
||||
final animeDir =
|
||||
episode.archivePath != null && episode.manga.value?.source == "local"
|
||||
? Directory(p.dirname(episode.archivePath!))
|
||||
: null;
|
||||
final chapterDirectory = (await storageProvider.getMangaChapterDirectory(
|
||||
episode,
|
||||
mangaMainDirectory: mangaDirectory,
|
||||
mangaMainDirectory: animeDir ?? mangaDirectory,
|
||||
))!;
|
||||
final path = isLocalArchive ? episode.archivePath : mp4animePath;
|
||||
final subtitlesDir = Directory(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:mangayomi/eval/lib.dart';
|
||||
import 'package:mangayomi/eval/model/m_manga.dart';
|
||||
import 'package:mangayomi/eval/model/m_pages.dart';
|
||||
import 'package:mangayomi/main.dart';
|
||||
import 'package:mangayomi/models/manga.dart';
|
||||
import 'package:mangayomi/models/source.dart';
|
||||
import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
|
@ -14,6 +20,27 @@ Future<MPages?> search(
|
|||
required int page,
|
||||
required List<dynamic> filterList,
|
||||
}) async {
|
||||
if (source.name == "local" && source.lang == "") {
|
||||
final result =
|
||||
(await isar.mangas
|
||||
.filter()
|
||||
.itemTypeEqualTo(source.itemType)
|
||||
.group(
|
||||
(q) => q
|
||||
.sourceEqualTo("local")
|
||||
.or()
|
||||
.linkContains("Mangayomi/local")
|
||||
.or()
|
||||
.linkContains("Mangayomi\\local"),
|
||||
)
|
||||
.nameContains(query, caseSensitive: false)
|
||||
.offset(max(0, page - 1) * 50)
|
||||
.limit(50)
|
||||
.findAll())
|
||||
.map((e) => MManga(name: e.name))
|
||||
.toList();
|
||||
return MPages(list: result, hasNextPage: true);
|
||||
}
|
||||
return getExtensionService(
|
||||
source,
|
||||
ref.read(androidProxyServerStateProvider),
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ part of 'search.dart';
|
|||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$searchHash() => r'b08d5a4b6e7d285830af7e5388b06fa61f175ede';
|
||||
String _$searchHash() => r'e23c6dd56549ffdfc89b5fcfa43719d90ca1760e';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:mangayomi/eval/lib.dart';
|
||||
import 'package:mangayomi/eval/model/m_manga.dart';
|
||||
import 'package:mangayomi/eval/model/m_pages.dart';
|
||||
import 'package:mangayomi/main.dart';
|
||||
import 'package:mangayomi/models/manga.dart';
|
||||
import 'package:mangayomi/models/source.dart';
|
||||
import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
|
@ -14,6 +18,26 @@ Future<MPages?> search(
|
|||
required int page,
|
||||
required List<dynamic> filterList,
|
||||
}) async {
|
||||
if (source.name == "local" && source.lang == "") {
|
||||
final result =
|
||||
(await isar.mangas
|
||||
.filter()
|
||||
.group(
|
||||
(q) => q
|
||||
.sourceEqualTo("local")
|
||||
.or()
|
||||
.linkContains("Mangayomi/local")
|
||||
.or()
|
||||
.linkContains("Mangayomi\\local"),
|
||||
)
|
||||
.nameContains(query, caseSensitive: false)
|
||||
.offset(page * 50)
|
||||
.limit(50)
|
||||
.findAll())
|
||||
.map((e) => MManga(name: e.name))
|
||||
.toList();
|
||||
return MPages(list: result, hasNextPage: true);
|
||||
}
|
||||
return getExtensionService(
|
||||
source,
|
||||
ref.read(androidProxyServerStateProvider),
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ part of 'search_.dart';
|
|||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$searchHash() => r'b08d5a4b6e7d285830af7e5388b06fa61f175ede';
|
||||
String _$searchHash() => r'e23c6dd56549ffdfc89b5fcfa43719d90ca1760e';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ part of 'trakt_tv.dart';
|
|||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$traktTvHash() => r'd852a7d96511637bf565cbcf6e958397740158fd';
|
||||
String _$traktTvHash() => r'269f0b865c39188f083dbc7dcad9652ee9e31efa';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui';
|
||||
import 'package:extended_image/extended_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:mangayomi/modules/manga/reader/u_chap_data_preload.dart';
|
||||
import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart';
|
||||
import 'package:mangayomi/modules/widgets/custom_extended_image_provider.dart';
|
||||
|
|
@ -14,6 +16,16 @@ import 'package:path/path.dart';
|
|||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
extension FileFormatter on num {
|
||||
String formattedFileSize({bool base1024 = true}) {
|
||||
final base = base1024 ? 1024 : 1000;
|
||||
if (this <= 0) return "0";
|
||||
final units = ["B", "kB", "MB", "GB", "TB"];
|
||||
int digitGroups = (log(this) / log(base)).round();
|
||||
return "${NumberFormat("#,##0.#").format(this / pow(base, digitGroups))} ${units[digitGroups]}";
|
||||
}
|
||||
}
|
||||
|
||||
extension LetExtension<T> on T {
|
||||
R let<R>(R Function(T) block) {
|
||||
return block(this);
|
||||
|
|
|
|||
Loading…
Reference in a new issue