Merge pull request #567 from Schnitzel5/logger

add option to download online subtitles
This commit is contained in:
Moustapha Kodjo Amadou 2025-08-27 09:09:54 +01:00 committed by GitHub
commit 40a0e080f9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 413 additions and 26 deletions

View file

@ -143,6 +143,9 @@
"nsfw_sources_info": "This does not prevent unofficial or potentially incorrectly flagged extensions from surfacing NSFW (18+) content within the app",
"version": "Version",
"check_for_update": "Check for update",
"share_app_logs": "Share app logs",
"no_app_logs": "No log.txt available!",
"failed": "Failed!",
"n_days_ago": "{days} days ago",
"today": "Today",
"yesterday": "Yesterday",

View file

@ -933,6 +933,24 @@ abstract class AppLocalizations {
/// **'Check for update'**
String get check_for_update;
/// No description provided for @share_app_logs.
///
/// In en, this message translates to:
/// **'Share app logs'**
String get share_app_logs;
/// No description provided for @no_app_logs.
///
/// In en, this message translates to:
/// **'No log.txt available!'**
String get no_app_logs;
/// No description provided for @failed.
///
/// In en, this message translates to:
/// **'Failed!'**
String get failed;
/// No description provided for @n_days_ago.
///
/// In en, this message translates to:

View file

@ -434,6 +434,15 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get check_for_update => 'التحقق من التحديثات';
@override
String get share_app_logs => 'Share app logs';
@override
String get no_app_logs => 'No log.txt available!';
@override
String get failed => 'Failed!';
@override
String n_days_ago(Object days) {
return 'منذ $days أيام';

View file

@ -436,6 +436,15 @@ class AppLocalizationsAs extends AppLocalizations {
@override
String get check_for_update => 'আপডেটৰ বাবে পৰীক্ষা কৰক';
@override
String get share_app_logs => 'Share app logs';
@override
String get no_app_logs => 'No log.txt available!';
@override
String get failed => 'Failed!';
@override
String n_days_ago(Object days) {
return '$days দিনৰ আগতে';

View file

@ -438,6 +438,15 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get check_for_update => 'Auf Aktualisierung prüfen';
@override
String get share_app_logs => 'Share app logs';
@override
String get no_app_logs => 'No log.txt available!';
@override
String get failed => 'Failed!';
@override
String n_days_ago(Object days) {
return 'Vor $days Tagen';

View file

@ -436,6 +436,15 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get check_for_update => 'Check for update';
@override
String get share_app_logs => 'Share app logs';
@override
String get no_app_logs => 'No log.txt available!';
@override
String get failed => 'Failed!';
@override
String n_days_ago(Object days) {
return '$days days ago';

View file

@ -440,6 +440,15 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get check_for_update => 'Buscar actualizaciones';
@override
String get share_app_logs => 'Share app logs';
@override
String get no_app_logs => 'No log.txt available!';
@override
String get failed => 'Failed!';
@override
String n_days_ago(Object days) {
return 'hace $days días';

View file

@ -442,6 +442,15 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get check_for_update => 'Rechercher des mises à jour';
@override
String get share_app_logs => 'Share app logs';
@override
String get no_app_logs => 'No log.txt available!';
@override
String get failed => 'Failed!';
@override
String n_days_ago(Object days) {
return 'Il y a $days jours';

View file

@ -436,6 +436,15 @@ class AppLocalizationsHi extends AppLocalizations {
@override
String get check_for_update => 'अपडेट के लिए जांचें';
@override
String get share_app_logs => 'Share app logs';
@override
String get no_app_logs => 'No log.txt available!';
@override
String get failed => 'Failed!';
@override
String n_days_ago(Object days) {
return '$days दिन पहले';

View file

@ -440,6 +440,15 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get check_for_update => 'Periksa Pembaruan';
@override
String get share_app_logs => 'Share app logs';
@override
String get no_app_logs => 'No log.txt available!';
@override
String get failed => 'Failed!';
@override
String n_days_ago(Object days) {
return '$days Hari yang Lalu';

View file

@ -440,6 +440,15 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get check_for_update => 'Controlla aggiornamenti';
@override
String get share_app_logs => 'Share app logs';
@override
String get no_app_logs => 'No log.txt available!';
@override
String get failed => 'Failed!';
@override
String n_days_ago(Object days) {
return '$days giorni fa';

View file

@ -440,6 +440,15 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get check_for_update => 'Verificar atualização';
@override
String get share_app_logs => 'Share app logs';
@override
String get no_app_logs => 'No log.txt available!';
@override
String get failed => 'Failed!';
@override
String n_days_ago(Object days) {
return '$days dias atrás';

View file

@ -441,6 +441,15 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get check_for_update => 'Проверить обновления';
@override
String get share_app_logs => 'Share app logs';
@override
String get no_app_logs => 'No log.txt available!';
@override
String get failed => 'Failed!';
@override
String n_days_ago(Object days) {
return '$days дней назад';

View file

@ -436,6 +436,15 @@ class AppLocalizationsTh extends AppLocalizations {
@override
String get check_for_update => 'ตรวจสอบการอัพเดท';
@override
String get share_app_logs => 'Share app logs';
@override
String get no_app_logs => 'No log.txt available!';
@override
String get failed => 'Failed!';
@override
String n_days_ago(Object days) {
return '$days วันที่แล้ว';

View file

@ -436,6 +436,15 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get check_for_update => 'Güncelleme Kontrol Et';
@override
String get share_app_logs => 'Share app logs';
@override
String get no_app_logs => 'No log.txt available!';
@override
String get failed => 'Failed!';
@override
String n_days_ago(Object days) {
return '$days gün önce';

View file

@ -428,6 +428,15 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get check_for_update => '检查更新';
@override
String get share_app_logs => 'Share app logs';
@override
String get no_app_logs => 'No log.txt available!';
@override
String get failed => 'Failed!';
@override
String n_days_ago(Object days) {
return '$days天前';

View file

@ -30,6 +30,7 @@ import 'package:mangayomi/l10n/generated/app_localizations.dart';
import 'package:mangayomi/services/http/m_client.dart';
import 'package:mangayomi/src/rust/frb_generated.dart';
import 'package:mangayomi/utils/discord_rpc.dart';
import 'package:mangayomi/utils/log/logger.dart';
import 'package:mangayomi/utils/url_protocol/api.dart';
import 'package:mangayomi/modules/more/settings/appearance/providers/theme_provider.dart';
import 'package:mangayomi/modules/library/providers/file_scanner.dart';
@ -66,6 +67,7 @@ void main(List<String> args) async {
);
}
}
await AppLogger.init();
isar = await StorageProvider().initDB(null, inspector: kDebugMode);
await Hive.initFlutter();
Hive.registerAdapter(TrackSearchAdapter());
@ -144,6 +146,7 @@ class _MyAppState extends ConsumerState<MyApp> {
void dispose() {
_linkSubscription?.cancel();
discordRpc?.destroy();
AppLogger.dispose();
super.dispose();
}

View file

@ -1355,6 +1355,7 @@ mp.register_script_message('call_button_${button.id}_long', button${button.id}lo
await subtitlesSearchraggableMenu(
context,
chapter: widget.episode,
isLocal: widget.isLocal,
)
as ImdbSubtitle?;
if (subtitle != null && context.mounted) {
@ -2254,13 +2255,22 @@ mp.register_script_message('call_button_${button.id}_long', button${button.id}lo
format: "image/png",
includeLibassSubtitles: _includeSubtitles,
);
await Share.shareXFiles([
XFile.fromData(
imageBytes!,
name: name,
mimeType: 'image/png',
),
]);
if (context.mounted) {
final box =
context.findRenderObject() as RenderBox?;
await Share.shareXFiles(
[
XFile.fromData(
imageBytes!,
name: name,
mimeType: 'image/png',
),
],
sharePositionOrigin:
box!.localToGlobal(Offset.zero) &
box.size,
);
}
},
),
button(

View file

@ -2,17 +2,30 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mangayomi/eval/model/m_bridge.dart';
import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/modules/widgets/custom_extended_image_provider.dart';
import 'package:mangayomi/modules/widgets/error_text.dart';
import 'package:mangayomi/modules/widgets/progress_center.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
import 'package:mangayomi/providers/storage_provider.dart';
import 'package:mangayomi/services/fetch_subtitles.dart';
import 'package:mangayomi/services/http/m_client.dart';
import 'package:mangayomi/services/http/rhttp/src/model/settings.dart';
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
import 'package:mangayomi/utils/extensions/string_extensions.dart';
import 'package:mangayomi/utils/log/logger.dart';
import 'package:path/path.dart' as path;
import 'package:super_sliver_list/super_sliver_list.dart';
class SubtitlesWidgetSearch extends ConsumerStatefulWidget {
final Chapter chapter;
const SubtitlesWidgetSearch({required this.chapter, super.key});
final bool isLocal;
const SubtitlesWidgetSearch({
required this.chapter,
required this.isLocal,
super.key,
});
@override
ConsumerState<SubtitlesWidgetSearch> createState() =>
@ -296,6 +309,12 @@ class _SubtitlesWidgetSearchState extends ConsumerState<SubtitlesWidgetSearch> {
),
],
),
if (isSubtitles && widget.isLocal)
OutlinedButton.icon(
onPressed: () async => _downloadSubtitle(index),
label: Text(context.l10n.download),
icon: Icon(Icons.download_outlined),
),
],
),
],
@ -308,11 +327,70 @@ class _SubtitlesWidgetSearchState extends ConsumerState<SubtitlesWidgetSearch> {
},
);
}
Future<void> _downloadSubtitle(int index) async {
botToast(context.l10n.started);
try {
final subtitle = subtitles![index];
final storageProvider = StorageProvider();
final chapterDirectory = (await storageProvider.getMangaChapterDirectory(
widget.chapter,
))!;
final subtitleFile = File(
path.join(
'${chapterDirectory.path}_subtitles',
'${subtitle.language}.srt',
),
);
final client = MClient.httpClient(
settings: const ClientSettings(
throwOnStatusCode: false,
tlsSettings: TlsSettings(verifyCertificates: false),
),
);
await subtitleFile.create(recursive: true);
final response = await _withRetry(
() => client.get(Uri.parse(subtitle.url ?? '')),
);
if (response.statusCode != 200) {
AppLogger.log(
'Warning: Failed to download subtitle file: ${subtitle.language}',
);
return;
}
AppLogger.log('Subtitle file downloaded: ${subtitle.language}');
await subtitleFile.writeAsBytes(response.bodyBytes);
if (context.mounted) {
botToast(context.l10n.finished(""));
}
} catch (e) {
AppLogger.log("Failed to download subtitle:", logLevel: LogLevel.error);
AppLogger.log(e.toString(), logLevel: LogLevel.error);
if (context.mounted) {
botToast(context.l10n.failed);
}
}
}
Future<T> _withRetry<T>(Future<T> Function() operation) async {
int attempts = 0;
while (true) {
try {
attempts++;
return await operation();
} catch (e) {
if (attempts >= 3) {
AppLogger.log("Request retries failed", logLevel: LogLevel.error);
}
}
}
}
}
subtitlesSearchraggableMenu(
BuildContext context, {
required Chapter chapter,
required bool isLocal,
}) async {
var padding = MediaQuery.of(context).padding;
return await showDialog(
@ -352,7 +430,7 @@ subtitlesSearchraggableMenu(
],
),
),
SubtitlesWidgetSearch(chapter: chapter),
SubtitlesWidgetSearch(chapter: chapter, isLocal: isLocal),
],
),
),

View file

@ -654,7 +654,14 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
);
final url =
"${source!.baseUrl}${widget.manga!.link!.getUrlWithoutDomain}";
Share.share(url);
final box =
context.findRenderObject() as RenderBox?;
Share.share(
url,
sharePositionOrigin:
box!.localToGlobal(Offset.zero) &
box.size,
);
break;
case 3:
context.push("/migrate", extra: widget.manga);

View file

@ -25,7 +25,7 @@ class ChapterPageDownload extends ConsumerWidget {
ref.read(downloadChapterProvider(chapter: chapter, useWifi: useWifi));
}
void _sendFile() async {
void _sendFile(BuildContext context) async {
final storageProvider = StorageProvider();
final mangaDir = await storageProvider.getMangaMainDirectory(chapter);
final path = await storageProvider.getMangaChapterDirectory(
@ -52,8 +52,13 @@ class ChapterPageDownload extends ConsumerWidget {
} else {
files = path!.listSync().map((e) => XFile(e.path)).toList();
}
if (files.isNotEmpty) {
Share.shareXFiles(files, text: chapter.name);
if (files.isNotEmpty && context.mounted) {
final box = context.findRenderObject() as RenderBox?;
Share.shareXFiles(
files,
text: chapter.name,
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
);
}
}
@ -123,7 +128,7 @@ class ChapterPageDownload extends ConsumerWidget {
),
onSelected: (value) {
if (value == 0) {
_sendFile();
_sendFile(context);
} else if (value == 1) {
_deleteFile(download.id!);
}

View file

@ -401,13 +401,21 @@ class _MangaChapterPageGalleryState
context.l10n.share,
Icons.share_outlined,
() async {
await Share.shareXFiles([
XFile.fromData(
imageBytes,
name: name,
mimeType: 'image/png',
),
]);
if (context.mounted) {
final box =
context.findRenderObject() as RenderBox?;
await Share.shareXFiles(
[
XFile.fromData(
imageBytes,
name: name,
mimeType: 'image/png',
),
],
sharePositionOrigin:
box!.localToGlobal(Offset.zero) & box.size,
);
}
},
),
button(

View file

@ -1,12 +1,19 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:mangayomi/eval/model/m_bridge.dart';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/modules/more/about/providers/check_for_update.dart';
import 'package:mangayomi/modules/more/about/providers/get_package_info.dart';
import 'package:mangayomi/modules/widgets/progress_center.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
import 'package:mangayomi/providers/storage_provider.dart';
import 'package:path/path.dart' as path;
import 'package:share_plus/share_plus.dart';
import 'package:url_launcher/url_launcher.dart';
class AboutScreen extends ConsumerWidget {
@ -73,6 +80,35 @@ class AboutScreen extends ConsumerWidget {
},
title: Text(l10n.check_for_update),
),
ListTile(
onTap: () async {
final storage = StorageProvider();
final directory = await storage.getDefaultDirectory();
final file = File(
path.join(directory!.path, 'logs.txt'),
);
if (await file.exists()) {
if (Platform.isLinux) {
await Clipboard.setData(
ClipboardData(text: file.path),
);
}
if (context.mounted) {
final box =
context.findRenderObject() as RenderBox?;
Share.shareXFiles(
[XFile(file.path)],
text: "log.txt",
sharePositionOrigin:
box!.localToGlobal(Offset.zero) & box.size,
);
}
} else {
botToast(l10n.no_app_logs);
}
},
title: Text(l10n.share_app_logs),
),
// ListTile(
// onTap: () {},
// title: const Text("What's news"),

View file

@ -176,9 +176,13 @@ Future<void> doBackUp(
alignment: Alignment.topLeft,
child: ElevatedButton(
onPressed: () {
Share.shareXFiles([
XFile(p.join(path, "$name.backup")),
], text: "$name.backup");
final box = context.findRenderObject() as RenderBox?;
Share.shareXFiles(
[XFile(p.join(path, "$name.backup"))],
text: "$name.backup",
sharePositionOrigin:
box!.localToGlobal(Offset.zero) & box.size,
);
},
child: Text(context.l10n.share),
),

View file

@ -239,7 +239,14 @@ class _MangaWebViewState extends ConsumerState<MangaWebView> {
if (value == 0) {
_webViewController?.reload();
} else if (value == 1) {
Share.share(_url);
final box =
context.findRenderObject() as RenderBox?;
Share.share(
_url,
sharePositionOrigin:
box!.localToGlobal(Offset.zero) &
box.size,
);
} else if (value == 2) {
await InAppBrowser.openWithSystemBrowser(
url: WebUri(_url),

View file

@ -13,6 +13,7 @@ import 'package:mangayomi/services/download_manager/m3u8/models/download.dart';
import 'package:mangayomi/services/download_manager/m3u8/models/ts_info.dart';
import 'package:mangayomi/src/rust/frb_generated.dart';
import 'package:mangayomi/utils/extensions/string_extensions.dart';
import 'package:mangayomi/utils/log/logger.dart';
import 'package:path/path.dart' as path;
import 'package:encrypt/encrypt.dart' as encrypt;
import 'package:convert/convert.dart';
@ -49,6 +50,7 @@ class M3u8Downloader {
if (kDebugMode) {
log('[M3u8Downloader] $message');
}
AppLogger.log(message);
}
void close() {
@ -151,11 +153,11 @@ class M3u8Downloader {
continue;
}
_log('Downloading subtitle file: ${element.label}');
subtitleFile.createSync(recursive: true);
if (element.file == null || element.file!.trim().isEmpty) {
_log('Warning: No subtitle file: ${element.label}');
continue;
}
subtitleFile.createSync(recursive: true);
if (element.file!.startsWith("http")) {
final response = await _withRetry(
() =>
@ -168,10 +170,13 @@ class M3u8Downloader {
_log('Subtitle file downloaded: ${element.label}');
await subtitleFile.writeAsBytes(response.bodyBytes);
} else {
_log('Subtitle file written: ${element.label}');
await subtitleFile.writeAsString(element.file!);
}
}
} catch (e) {
AppLogger.log("Download failed", logLevel: LogLevel.error);
AppLogger.log(e.toString(), logLevel: LogLevel.error);
throw M3u8DownloaderException('Download failed', e);
} finally {
close();

77
lib/utils/log/logger.dart Normal file
View file

@ -0,0 +1,77 @@
import 'dart:async';
import 'dart:io';
import 'package:mangayomi/providers/storage_provider.dart';
import 'package:path/path.dart' as path;
class AppLogger {
static final _logQueue = StreamController<String>();
static late File _logFile;
static late IOSink _sink;
static bool _initialized = false;
/// Initialize the logger
static Future<void> init() async {
final storage = StorageProvider();
final directory = await storage.getDefaultDirectory();
_logFile = File(path.join(directory!.path, 'logs.txt'));
if (await _logFile.exists() && await _logFile.length() > 100 * 1024) {
await _logFile.delete();
}
if (!await _logFile.exists()) {
await _logFile.create(recursive: true);
}
_sink = _logFile.openWrite(mode: FileMode.append);
_initialized = true;
_logQueue.stream.listen((log) {
_sink.writeln(log);
});
log('\n\nLogger initialized\n\n');
}
static void log(String message, {LogLevel logLevel = LogLevel.info}) {
if (!_initialized) return;
final now = DateTime.now();
final timestamp =
'${now.day.toString().padLeft(2, '0')}/${now.month.toString().padLeft(2, '0')}/${now.year.toString().padLeft(4, '0')} '
'${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}';
final logMessage = '[$timestamp][${logLevel.toString()}] $message';
_logQueue.add(logMessage);
}
static Future<void> dispose() async {
if (!_initialized) return;
await _logQueue.close();
await _sink.flush();
await _sink.close();
_initialized = false;
}
}
enum LogLevel {
debug,
info,
warning,
error;
@override
String toString() {
switch (this) {
case LogLevel.debug:
return 'DEBUG';
case LogLevel.info:
return 'INFO';
case LogLevel.warning:
return 'WARNING';
case LogLevel.error:
return 'ERROR';
}
}
}