Merge pull request #721 from NBA2K1/improve-updates

Improve updates
This commit is contained in:
Moustapha Kodjo Amadou 2026-05-09 16:07:59 +01:00 committed by GitHub
commit 25c1d72c8b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 145 additions and 167 deletions

View file

@ -11,6 +11,7 @@ import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/update.dart';
import 'package:mangayomi/models/source.dart';
import 'package:mangayomi/modules/more/about/providers/download_file_screen.dart';
import 'package:mangayomi/modules/more/providers/downloaded_only_state_provider.dart';
import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart';
import 'package:mangayomi/modules/more/settings/sync/providers/sync_providers.dart';
@ -118,7 +119,6 @@ class _MainScreenState extends ConsumerState<MainScreen> {
void _initializeProviders() {
Future.microtask(() {
if (mounted) {
ref.read(checkForUpdateProvider(context: context));
for (var type in ItemType.values) {
ref.read(
fetchItemSourcesListProvider(
@ -169,6 +169,17 @@ class _MainScreenState extends ConsumerState<MainScreen> {
bool isLibSwitch = false;
@override
Widget build(BuildContext context) {
ref.listen<AsyncValue<UpdateInfo?>>(checkForUpdateProvider, (_, next) {
next.whenData((updateInfo) {
if (updateInfo != null && context.mounted) {
showDialog(
context: context,
builder: (_) => DownloadFileScreen(updateAvailable: updateInfo),
);
}
});
});
ref.listen<Locale>(l10nLocaleStateProvider, (previous, next) {
_clearCache();
setState(() {});

View file

@ -1,5 +1,6 @@
import 'dart:io';
import 'package:bot_toast/bot_toast.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -8,6 +9,7 @@ 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/download_file_screen.dart';
import 'package:mangayomi/modules/more/about/providers/get_package_info.dart';
import 'package:mangayomi/modules/more/about/providers/logs_state.dart';
import 'package:mangayomi/modules/widgets/progress_center.dart';
@ -73,13 +75,33 @@ class AboutScreen extends ConsumerWidget {
},
),
ListTile(
onTap: () {
ref.read(
checkForUpdateProvider(
context: context,
manualUpdate: true,
),
);
onTap: () async {
BotToast.showText(text: l10n.searching_for_updates);
try {
final updateInfo = await performManualUpdateCheck();
if (updateInfo != null) {
BotToast.showText(
text: l10n.new_update_available,
);
await Future.delayed(const Duration(seconds: 1));
if (context.mounted) {
showDialog(
context: context,
builder: (_) => DownloadFileScreen(
updateAvailable: updateInfo,
),
);
}
} else {
BotToast.showText(
text: l10n.no_new_updates_available,
);
}
} catch (_) {
BotToast.showText(
text: l10n.no_new_updates_available,
);
}
},
title: Text(l10n.check_for_update),
),

View file

@ -1,12 +1,8 @@
import 'dart:convert';
import 'dart:developer';
import 'package:bot_toast/bot_toast.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/modules/more/about/providers/download_file_screen.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
import 'package:mangayomi/services/fetch_sources_list.dart';
import 'package:mangayomi/services/http/m_client.dart';
import 'package:mangayomi/utils/extensions/string_extensions.dart';
@ -14,43 +10,29 @@ import 'package:package_info_plus/package_info_plus.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'check_for_update.g.dart';
@riverpod
Future<void> checkForUpdate(
Ref ref, {
BuildContext? context,
bool? manualUpdate,
}) async {
manualUpdate = manualUpdate ?? false;
final checkForUpdates = ref.read(checkForAppUpdatesProvider);
if (!checkForUpdates && !manualUpdate) return;
final l10n = l10nLocalizations(context!)!;
/// Convenience alias: (version, body, htmlUrl, assets).
typedef UpdateInfo = (String, String, String, List<dynamic>);
if (manualUpdate) {
BotToast.showText(text: l10n.searching_for_updates);
}
/// Automatic update-check provider.
///
/// Respects the user's [checkForAppUpdatesProvider] preference. Returns
/// [UpdateInfo] when a newer version exists, `null` otherwise.
@riverpod
Future<UpdateInfo?> checkForUpdate(Ref ref) async {
if (!ref.read(checkForAppUpdatesProvider)) return null;
return _getUpdateIfAvailable();
}
/// Compares the running version against the latest release.
/// Returns [UpdateInfo] when an update is available, or `null` when already
/// up-to-date. Throws if the network request fails.
Future<UpdateInfo?> _getUpdateIfAvailable() async {
final info = await PackageInfo.fromPlatform();
if (kDebugMode) {
log(info.data.toString());
}
final updateAvailable = await _checkUpdate();
if (compareVersions(info.version, updateAvailable.$1) < 0) {
if (manualUpdate) {
BotToast.showText(text: l10n.new_update_available);
await Future.delayed(const Duration(seconds: 1));
}
if (context.mounted) {
showDialog(
context: context,
builder: (context) {
return DownloadFileScreen(updateAvailable: updateAvailable);
},
);
}
} else if (compareVersions(info.version, updateAvailable.$1) == 0) {
if (manualUpdate) {
BotToast.showText(text: l10n.no_new_updates_available);
}
}
final latest = await _fetchLatestRelease();
return compareVersions(info.version, latest.$1) < 0 ? latest : null;
}
@riverpod
@ -58,27 +40,23 @@ bool checkForAppUpdates(Ref ref) {
return isar.settings.getSync(227)?.checkForAppUpdates ?? true;
}
Future<(String, String, String, List<dynamic>)> _checkUpdate() async {
/// Performs an update check unconditionally, ignoring the auto-update setting.
Future<UpdateInfo?> performManualUpdateCheck() => _getUpdateIfAvailable();
Future<UpdateInfo> _fetchLatestRelease() async {
final http = MClient.init(reqcopyWith: {'useDartHttpClient': true});
try {
final res = await http.get(
Uri.parse(
"https://api.github.com/repos/kodjodevf/Mangayomi/releases?page=1&per_page=10",
),
);
List resListJson = jsonDecode(res.body) as List;
return (
resListJson.first["name"]
.toString()
.substringAfter('v')
.substringBefore('-'),
resListJson.first["body"].toString(),
resListJson.first["html_url"].toString(),
(resListJson.first["assets"] as List)
.map((asset) => asset["browser_download_url"])
.toList(),
);
} catch (e) {
rethrow;
}
final res = await http.get(
Uri.parse(
'https://api.github.com/repos/kodjodevf/Mangayomi/releases/latest',
),
);
final release = jsonDecode(res.body) as Map<String, dynamic>;
return (
release['name'].toString().substringAfter('v').substringBefore('-'),
release['body'].toString(),
release['html_url'].toString(),
(release['assets'] as List)
.map((asset) => asset['browser_download_url'])
.toList(),
);
}

View file

@ -8,87 +8,58 @@ part of 'check_for_update.dart';
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Automatic update-check provider.
///
/// Respects the user's [checkForAppUpdatesProvider] preference. Returns
/// [UpdateInfo] when a newer version exists, `null` otherwise.
@ProviderFor(checkForUpdate)
final checkForUpdateProvider = CheckForUpdateFamily._();
final checkForUpdateProvider = CheckForUpdateProvider._();
/// Automatic update-check provider.
///
/// Respects the user's [checkForAppUpdatesProvider] preference. Returns
/// [UpdateInfo] when a newer version exists, `null` otherwise.
final class CheckForUpdateProvider
extends $FunctionalProvider<AsyncValue<void>, void, FutureOr<void>>
with $FutureModifier<void>, $FutureProvider<void> {
CheckForUpdateProvider._({
required CheckForUpdateFamily super.from,
required ({BuildContext? context, bool? manualUpdate}) super.argument,
}) : super(
retry: null,
name: r'checkForUpdateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
extends
$FunctionalProvider<
AsyncValue<UpdateInfo?>,
UpdateInfo?,
FutureOr<UpdateInfo?>
>
with $FutureModifier<UpdateInfo?>, $FutureProvider<UpdateInfo?> {
/// Automatic update-check provider.
///
/// Respects the user's [checkForAppUpdatesProvider] preference. Returns
/// [UpdateInfo] when a newer version exists, `null` otherwise.
CheckForUpdateProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'checkForUpdateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$checkForUpdateHash();
@override
String toString() {
return r'checkForUpdateProvider'
''
'$argument';
}
@$internal
@override
$FutureProviderElement<void> $createElement($ProviderPointer pointer) =>
$FutureProviderElement(pointer);
$FutureProviderElement<UpdateInfo?> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<void> create(Ref ref) {
final argument =
this.argument as ({BuildContext? context, bool? manualUpdate});
return checkForUpdate(
ref,
context: argument.context,
manualUpdate: argument.manualUpdate,
);
}
@override
bool operator ==(Object other) {
return other is CheckForUpdateProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
FutureOr<UpdateInfo?> create(Ref ref) {
return checkForUpdate(ref);
}
}
String _$checkForUpdateHash() => r'8064f08768f4d98997201a2f07ba160c721a6a3f';
final class CheckForUpdateFamily extends $Family
with
$FunctionalFamilyOverride<
FutureOr<void>,
({BuildContext? context, bool? manualUpdate})
> {
CheckForUpdateFamily._()
: super(
retry: null,
name: r'checkForUpdateProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
CheckForUpdateProvider call({BuildContext? context, bool? manualUpdate}) =>
CheckForUpdateProvider._(
argument: (context: context, manualUpdate: manualUpdate),
from: this,
);
@override
String toString() => r'checkForUpdateProvider';
}
String _$checkForUpdateHash() => r'7134bb3c6ac01bc16fecf975195a5231d57c6148';
@ProviderFor(checkForAppUpdates)
final checkForAppUpdatesProvider = CheckForAppUpdatesProvider._();

View file

@ -98,24 +98,26 @@ class _UpdatesScreenState extends BaseLibraryTabScreenState<UpdatesScreen> {
}
Future<void> _updateLibrary() async {
setState(() => _isLoading = true);
final itemType = getCurrentItemType();
final mangaList = isar.mangas
.filter()
.idIsNotNull()
.favoriteEqualTo(true)
.and()
.itemTypeEqualTo(itemType)
.and()
.isLocalArchiveEqualTo(false)
.findAllSync();
await updateLibrary(
ref: ref,
context: context,
mangaList: mangaList,
itemType: itemType,
);
setState(() => _isLoading = false);
try {
setState(() => _isLoading = true);
final itemType = getCurrentItemType();
final mangaList = await isar.mangas
.filter()
.idIsNotNull()
.favoriteEqualTo(true)
.itemTypeEqualTo(itemType)
.isLocalArchiveEqualTo(false)
.findAll();
if (!mounted) return;
await updateLibrary(
ref: ref,
context: context,
mangaList: mangaList,
itemType: itemType,
);
} finally {
if (mounted) setState(() => _isLoading = false);
}
}
Future<void> _clearUpdates() async {
@ -124,6 +126,7 @@ class _UpdatesScreenState extends BaseLibraryTabScreenState<UpdatesScreen> {
.idIsNotNull()
.chapter((q) => q.manga((q) => q.itemTypeEqualTo(getCurrentItemType())))
.findAll();
if (updates.isEmpty) return;
final idsToDelete = <Id>[];
isar.writeTxnSync(() {
for (var update in updates) {
@ -133,7 +136,7 @@ class _UpdatesScreenState extends BaseLibraryTabScreenState<UpdatesScreen> {
.addChangedPart(ActionType.removeUpdate, update.id, "{}", false);
}
});
await isar.writeTxn(() => isar.updates.deleteAll(idsToDelete));
await isar.writeTxn(() async => await isar.updates.deleteAll(idsToDelete));
}
}
@ -273,25 +276,18 @@ Widget _updateNumbers(WidgetRef ref, ItemType itemType) {
stream: isar.updates
.filter()
.idIsNotNull()
.and()
.chapter((q) => q.manga((q) => q.itemTypeEqualTo(itemType)))
.watch(fireImmediately: true),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data!.isNotEmpty) {
final entries = snapshot.data!.toList();
return entries.isEmpty
? SizedBox.shrink()
: Badge(
backgroundColor: Theme.of(context).focusColor,
label: Text(
entries.length.toString(),
style: TextStyle(
color: Theme.of(context).textTheme.bodySmall!.color,
),
),
);
}
return Container();
final count = snapshot.data?.length ?? 0;
if (count == 0) return const SizedBox.shrink();
return Badge(
backgroundColor: Theme.of(context).focusColor,
label: Text(
count.toString(),
style: TextStyle(color: Theme.of(context).textTheme.bodySmall!.color),
),
);
},
);
}