add option to download online subtitles

- button appears for downloaded and local entries
- added app logging class
This commit is contained in:
Schnitzel5 2025-08-27 00:01:18 +02:00
parent f6c2a24af2
commit e2ddcf1ffe
18 changed files with 131 additions and 2 deletions

View file

@ -145,6 +145,7 @@
"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

@ -945,6 +945,12 @@ abstract class AppLocalizations {
/// **'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

@ -440,6 +440,9 @@ class AppLocalizationsAr extends AppLocalizations {
@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

@ -442,6 +442,9 @@ class AppLocalizationsAs extends AppLocalizations {
@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

@ -444,6 +444,9 @@ class AppLocalizationsDe extends AppLocalizations {
@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

@ -442,6 +442,9 @@ class AppLocalizationsEn extends AppLocalizations {
@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

@ -446,6 +446,9 @@ class AppLocalizationsEs extends AppLocalizations {
@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

@ -448,6 +448,9 @@ class AppLocalizationsFr extends AppLocalizations {
@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

@ -442,6 +442,9 @@ class AppLocalizationsHi extends AppLocalizations {
@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

@ -446,6 +446,9 @@ class AppLocalizationsId extends AppLocalizations {
@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

@ -446,6 +446,9 @@ class AppLocalizationsIt extends AppLocalizations {
@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

@ -446,6 +446,9 @@ class AppLocalizationsPt extends AppLocalizations {
@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

@ -447,6 +447,9 @@ class AppLocalizationsRu extends AppLocalizations {
@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

@ -442,6 +442,9 @@ class AppLocalizationsTh extends AppLocalizations {
@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

@ -442,6 +442,9 @@ class AppLocalizationsTr extends AppLocalizations {
@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

@ -434,6 +434,9 @@ class AppLocalizationsZh extends AppLocalizations {
@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

@ -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) {

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,71 @@ 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);
}
rethrow;
}
}
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 +431,7 @@ subtitlesSearchraggableMenu(
],
),
),
SubtitlesWidgetSearch(chapter: chapter),
SubtitlesWidgetSearch(chapter: chapter, isLocal: isLocal),
],
),
),