Add Gitea update notifications
This commit is contained in:
+94
-1
@@ -1,7 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import '../backend/backend.dart';
|
import '../backend/backend.dart';
|
||||||
|
import '../core/utils/result.dart';
|
||||||
import 'router.dart';
|
import 'router.dart';
|
||||||
import 'theme.dart';
|
import 'theme.dart';
|
||||||
|
|
||||||
@@ -36,12 +38,103 @@ class GelRuleApp extends ConsumerWidget {
|
|||||||
orElse: () => ThemeMode.dark,
|
orElse: () => ThemeMode.dark,
|
||||||
),
|
),
|
||||||
routerConfig: router,
|
routerConfig: router,
|
||||||
builder: (context, child) => _DownloadOverlay(child: child),
|
builder: (context, child) => _AppOverlay(child: child),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _AppOverlay extends ConsumerStatefulWidget {
|
||||||
|
const _AppOverlay({required this.child});
|
||||||
|
|
||||||
|
final Widget? child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<_AppOverlay> createState() => _AppOverlayState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppOverlayState extends ConsumerState<_AppOverlay> {
|
||||||
|
bool _checkedUpdates = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
if (_checkedUpdates) return;
|
||||||
|
_checkedUpdates = true;
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => _checkUpdates());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _checkUpdates() async {
|
||||||
|
final result = await ref.read(updateServiceProvider).checkForUpdates();
|
||||||
|
if (!mounted || result is! Success<AppUpdateInfo?> || result.data == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await _showUpdateDialog(result.data!);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showUpdateDialog(AppUpdateInfo info) async {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Text('RuleGel ${info.version} is available'),
|
||||||
|
content: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 520),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(info.name),
|
||||||
|
if (info.body.trim().isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
info.body.trim(),
|
||||||
|
maxLines: 12,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await ref.read(updateServiceProvider).skipVersion(info);
|
||||||
|
if (context.mounted) Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: const Text('Skip this version'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await ref.read(updateServiceProvider).remindLater();
|
||||||
|
if (context.mounted) Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: const Text('Later'),
|
||||||
|
),
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: () async {
|
||||||
|
final url = Uri.tryParse(info.htmlUrl);
|
||||||
|
if (url != null) {
|
||||||
|
await launchUrl(url, mode: LaunchMode.externalApplication);
|
||||||
|
}
|
||||||
|
if (context.mounted) Navigator.pop(context);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.open_in_new_rounded),
|
||||||
|
label: const Text('Update'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
ref.invalidate(appSettingsProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return _DownloadOverlay(child: widget.child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _DownloadOverlay extends ConsumerWidget {
|
class _DownloadOverlay extends ConsumerWidget {
|
||||||
const _DownloadOverlay({required this.child});
|
const _DownloadOverlay({required this.child});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
const appDisplayVersion = '0.4.0';
|
||||||
|
const appBuildNumber = 4;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
export 'di/backend_providers.dart';
|
export 'di/backend_providers.dart';
|
||||||
export 'models/collection.dart';
|
export 'models/collection.dart';
|
||||||
export 'models/content_provider_config.dart';
|
export 'models/content_provider_config.dart';
|
||||||
|
export 'models/app_update_info.dart';
|
||||||
export 'models/download_task.dart';
|
export 'models/download_task.dart';
|
||||||
export 'models/favorite.dart';
|
export 'models/favorite.dart';
|
||||||
export 'models/post.dart';
|
export 'models/post.dart';
|
||||||
@@ -21,6 +22,7 @@ export 'services/feed_service.dart';
|
|||||||
export 'services/provider_check_service.dart';
|
export 'services/provider_check_service.dart';
|
||||||
export 'services/search_service.dart';
|
export 'services/search_service.dart';
|
||||||
export 'services/settings_service.dart';
|
export 'services/settings_service.dart';
|
||||||
|
export 'services/update_service.dart';
|
||||||
export 'services/viewed_history_service.dart';
|
export 'services/viewed_history_service.dart';
|
||||||
export 'utils/smart_blacklist.dart';
|
export 'utils/smart_blacklist.dart';
|
||||||
export 'utils/media_quality.dart';
|
export 'utils/media_quality.dart';
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../../core/cache/cache_service.dart';
|
import '../../core/cache/cache_service.dart';
|
||||||
@@ -24,6 +25,7 @@ import '../services/feed_service.dart';
|
|||||||
import '../services/provider_check_service.dart';
|
import '../services/provider_check_service.dart';
|
||||||
import '../services/search_service.dart';
|
import '../services/search_service.dart';
|
||||||
import '../services/settings_service.dart';
|
import '../services/settings_service.dart';
|
||||||
|
import '../services/update_service.dart';
|
||||||
import '../services/viewed_history_service.dart';
|
import '../services/viewed_history_service.dart';
|
||||||
|
|
||||||
final appDatabaseProvider = FutureProvider<AppDatabase>((ref) async {
|
final appDatabaseProvider = FutureProvider<AppDatabase>((ref) async {
|
||||||
@@ -158,3 +160,19 @@ final downloadTasksProvider = StreamProvider<List<DownloadTask>>((ref) {
|
|||||||
final manager = ref.watch(downloadManagerServiceProvider);
|
final manager = ref.watch(downloadManagerServiceProvider);
|
||||||
return manager.stream;
|
return manager.stream;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final updateServiceProvider = Provider<UpdateService>((ref) {
|
||||||
|
return UpdateService(
|
||||||
|
Dio(
|
||||||
|
BaseOptions(
|
||||||
|
connectTimeout: const Duration(seconds: 12),
|
||||||
|
receiveTimeout: const Duration(seconds: 12),
|
||||||
|
headers: const {
|
||||||
|
'User-Agent': 'RuleGel/0.4 Flutter local booru browser',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ref.watch(settingsServiceProvider),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
class AppUpdateInfo {
|
||||||
|
const AppUpdateInfo({
|
||||||
|
required this.version,
|
||||||
|
required this.tagName,
|
||||||
|
required this.name,
|
||||||
|
required this.body,
|
||||||
|
required this.htmlUrl,
|
||||||
|
required this.publishedAt,
|
||||||
|
this.apkUrl,
|
||||||
|
this.windowsInstallerUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String version;
|
||||||
|
final String tagName;
|
||||||
|
final String name;
|
||||||
|
final String body;
|
||||||
|
final String htmlUrl;
|
||||||
|
final DateTime? publishedAt;
|
||||||
|
final String? apkUrl;
|
||||||
|
final String? windowsInstallerUrl;
|
||||||
|
|
||||||
|
bool get hasAssets => apkUrl != null || windowsInstallerUrl != null;
|
||||||
|
}
|
||||||
@@ -33,6 +33,8 @@ class AppSettings {
|
|||||||
required this.lastFeedProviderIds,
|
required this.lastFeedProviderIds,
|
||||||
required this.lastFeedTopPeriod,
|
required this.lastFeedTopPeriod,
|
||||||
required this.lastFeedScrollOffset,
|
required this.lastFeedScrollOffset,
|
||||||
|
this.skippedUpdateVersion,
|
||||||
|
this.lastUpdateCheckAt,
|
||||||
this.defaultRatingFilter,
|
this.defaultRatingFilter,
|
||||||
this.lastFeedRating,
|
this.lastFeedRating,
|
||||||
});
|
});
|
||||||
@@ -59,6 +61,8 @@ class AppSettings {
|
|||||||
final List<String> lastFeedProviderIds;
|
final List<String> lastFeedProviderIds;
|
||||||
final String lastFeedTopPeriod;
|
final String lastFeedTopPeriod;
|
||||||
final double lastFeedScrollOffset;
|
final double lastFeedScrollOffset;
|
||||||
|
final String? skippedUpdateVersion;
|
||||||
|
final String? lastUpdateCheckAt;
|
||||||
final String? defaultRatingFilter;
|
final String? defaultRatingFilter;
|
||||||
final String? lastFeedRating;
|
final String? lastFeedRating;
|
||||||
|
|
||||||
@@ -85,6 +89,8 @@ class AppSettings {
|
|||||||
lastFeedProviderIds: [],
|
lastFeedProviderIds: [],
|
||||||
lastFeedTopPeriod: 'none',
|
lastFeedTopPeriod: 'none',
|
||||||
lastFeedScrollOffset: 0,
|
lastFeedScrollOffset: 0,
|
||||||
|
skippedUpdateVersion: null,
|
||||||
|
lastUpdateCheckAt: null,
|
||||||
defaultRatingFilter: null,
|
defaultRatingFilter: null,
|
||||||
lastFeedRating: null,
|
lastFeedRating: null,
|
||||||
);
|
);
|
||||||
@@ -112,6 +118,8 @@ class AppSettings {
|
|||||||
List<String>? lastFeedProviderIds,
|
List<String>? lastFeedProviderIds,
|
||||||
String? lastFeedTopPeriod,
|
String? lastFeedTopPeriod,
|
||||||
double? lastFeedScrollOffset,
|
double? lastFeedScrollOffset,
|
||||||
|
String? skippedUpdateVersion,
|
||||||
|
String? lastUpdateCheckAt,
|
||||||
String? defaultRatingFilter,
|
String? defaultRatingFilter,
|
||||||
String? lastFeedRating,
|
String? lastFeedRating,
|
||||||
bool clearLastFeedRating = false,
|
bool clearLastFeedRating = false,
|
||||||
@@ -141,6 +149,8 @@ class AppSettings {
|
|||||||
lastFeedProviderIds: lastFeedProviderIds ?? this.lastFeedProviderIds,
|
lastFeedProviderIds: lastFeedProviderIds ?? this.lastFeedProviderIds,
|
||||||
lastFeedTopPeriod: lastFeedTopPeriod ?? this.lastFeedTopPeriod,
|
lastFeedTopPeriod: lastFeedTopPeriod ?? this.lastFeedTopPeriod,
|
||||||
lastFeedScrollOffset: lastFeedScrollOffset ?? this.lastFeedScrollOffset,
|
lastFeedScrollOffset: lastFeedScrollOffset ?? this.lastFeedScrollOffset,
|
||||||
|
skippedUpdateVersion: skippedUpdateVersion ?? this.skippedUpdateVersion,
|
||||||
|
lastUpdateCheckAt: lastUpdateCheckAt ?? this.lastUpdateCheckAt,
|
||||||
defaultRatingFilter: defaultRatingFilter ?? this.defaultRatingFilter,
|
defaultRatingFilter: defaultRatingFilter ?? this.defaultRatingFilter,
|
||||||
lastFeedRating:
|
lastFeedRating:
|
||||||
clearLastFeedRating ? null : lastFeedRating ?? this.lastFeedRating,
|
clearLastFeedRating ? null : lastFeedRating ?? this.lastFeedRating,
|
||||||
@@ -170,6 +180,8 @@ class AppSettings {
|
|||||||
'lastFeedProviderIds': lastFeedProviderIds,
|
'lastFeedProviderIds': lastFeedProviderIds,
|
||||||
'lastFeedTopPeriod': lastFeedTopPeriod,
|
'lastFeedTopPeriod': lastFeedTopPeriod,
|
||||||
'lastFeedScrollOffset': lastFeedScrollOffset,
|
'lastFeedScrollOffset': lastFeedScrollOffset,
|
||||||
|
'skippedUpdateVersion': skippedUpdateVersion,
|
||||||
|
'lastUpdateCheckAt': lastUpdateCheckAt,
|
||||||
'defaultRatingFilter': defaultRatingFilter,
|
'defaultRatingFilter': defaultRatingFilter,
|
||||||
'lastFeedRating': lastFeedRating,
|
'lastFeedRating': lastFeedRating,
|
||||||
};
|
};
|
||||||
@@ -232,6 +244,8 @@ class AppSettings {
|
|||||||
lastFeedScrollOffset:
|
lastFeedScrollOffset:
|
||||||
(json['lastFeedScrollOffset'] as num?)?.toDouble() ??
|
(json['lastFeedScrollOffset'] as num?)?.toDouble() ??
|
||||||
defaults.lastFeedScrollOffset,
|
defaults.lastFeedScrollOffset,
|
||||||
|
skippedUpdateVersion: json['skippedUpdateVersion'] as String?,
|
||||||
|
lastUpdateCheckAt: json['lastUpdateCheckAt'] as String?,
|
||||||
defaultRatingFilter: json['defaultRatingFilter'] as String?,
|
defaultRatingFilter: json['defaultRatingFilter'] as String?,
|
||||||
lastFeedRating: json['lastFeedRating'] as String?,
|
lastFeedRating: json['lastFeedRating'] as String?,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
|
||||||
|
import '../../app/app_version.dart';
|
||||||
|
import '../../core/errors/failure.dart';
|
||||||
|
import '../../core/utils/result.dart';
|
||||||
|
import '../models/app_update_info.dart';
|
||||||
|
import 'settings_service.dart';
|
||||||
|
|
||||||
|
class UpdateService {
|
||||||
|
UpdateService(this._dio, this._settingsService);
|
||||||
|
|
||||||
|
static const latestReleaseUrl =
|
||||||
|
'https://gitea.rardogsynapse.online/api/v1/repos/RarDog/RuleGelApp/releases/latest';
|
||||||
|
|
||||||
|
final Dio _dio;
|
||||||
|
final SettingsService _settingsService;
|
||||||
|
|
||||||
|
Future<Result<AppUpdateInfo?>> checkForUpdates({bool force = false}) async {
|
||||||
|
final settingsResult = await _settingsService.getSettings();
|
||||||
|
final settings = settingsResult is Success<AppSettings>
|
||||||
|
? settingsResult.data
|
||||||
|
: AppSettings.defaults;
|
||||||
|
|
||||||
|
if (!force && !_shouldCheck(settings)) return const Success(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await _dio.get<dynamic>(latestReleaseUrl);
|
||||||
|
final data = Map<String, dynamic>.from(response.data as Map);
|
||||||
|
final info = _releaseFromJson(data);
|
||||||
|
final nextSettings = settings.copyWith(
|
||||||
|
lastUpdateCheckAt: DateTime.now().toIso8601String(),
|
||||||
|
);
|
||||||
|
await _settingsService.updateSettings(nextSettings);
|
||||||
|
|
||||||
|
final skipped = settings.skippedUpdateVersion == info.version ||
|
||||||
|
settings.skippedUpdateVersion == info.tagName;
|
||||||
|
if (!force && skipped) return const Success(null);
|
||||||
|
if (!_isNewerVersion(info.version, appDisplayVersion)) {
|
||||||
|
return const Success(null);
|
||||||
|
}
|
||||||
|
return Success(info);
|
||||||
|
} catch (error) {
|
||||||
|
return Error(
|
||||||
|
Failure(
|
||||||
|
code: 'update_check_failed',
|
||||||
|
message: 'Could not check for updates',
|
||||||
|
details: error,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<void>> remindLater() async {
|
||||||
|
final result = await _settingsService.getSettings();
|
||||||
|
if (result is Error<AppSettings>) return Error(result.failure);
|
||||||
|
final settings = (result as Success<AppSettings>).data;
|
||||||
|
return _settingsService.updateSettings(
|
||||||
|
settings.copyWith(lastUpdateCheckAt: DateTime.now().toIso8601String()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<void>> skipVersion(AppUpdateInfo info) async {
|
||||||
|
final result = await _settingsService.getSettings();
|
||||||
|
if (result is Error<AppSettings>) return Error(result.failure);
|
||||||
|
final settings = (result as Success<AppSettings>).data;
|
||||||
|
return _settingsService.updateSettings(
|
||||||
|
settings.copyWith(skippedUpdateVersion: info.version),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _shouldCheck(AppSettings settings) {
|
||||||
|
final last = DateTime.tryParse(settings.lastUpdateCheckAt ?? '');
|
||||||
|
if (last == null) return true;
|
||||||
|
return DateTime.now().difference(last) >= const Duration(hours: 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
AppUpdateInfo _releaseFromJson(Map<String, dynamic> json) {
|
||||||
|
final tag = (json['tag_name'] ?? '').toString();
|
||||||
|
final assets = (json['assets'] as List?) ?? const [];
|
||||||
|
String? apkUrl;
|
||||||
|
String? installerUrl;
|
||||||
|
for (final item in assets.whereType<Map>()) {
|
||||||
|
final name = (item['name'] ?? '').toString().toLowerCase();
|
||||||
|
final url = (item['browser_download_url'] ?? '').toString();
|
||||||
|
if (url.isEmpty) continue;
|
||||||
|
if (name.endsWith('.apk')) apkUrl = url;
|
||||||
|
if (name.endsWith('.exe')) installerUrl = url;
|
||||||
|
}
|
||||||
|
return AppUpdateInfo(
|
||||||
|
version: _versionFromTag(tag),
|
||||||
|
tagName: tag,
|
||||||
|
name: (json['name'] ?? tag).toString(),
|
||||||
|
body: (json['body'] ?? '').toString(),
|
||||||
|
htmlUrl: (json['html_url'] ?? '').toString(),
|
||||||
|
publishedAt: DateTime.tryParse((json['published_at'] ?? '').toString()),
|
||||||
|
apkUrl: apkUrl,
|
||||||
|
windowsInstallerUrl: installerUrl,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _versionFromTag(String tag) {
|
||||||
|
final match = RegExp(r'v?(\d+\.\d+\.\d+)').firstMatch(tag);
|
||||||
|
return match?.group(1) ?? tag.replaceFirst(RegExp(r'^v'), '');
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool _isNewerVersion(String latest, String current) {
|
||||||
|
final left = _parts(latest);
|
||||||
|
final right = _parts(current);
|
||||||
|
for (var i = 0; i < 3; i++) {
|
||||||
|
if (left[i] > right[i]) return true;
|
||||||
|
if (left[i] < right[i]) return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<int> _parts(String value) {
|
||||||
|
final match = RegExp(r'(\d+)\.(\d+)\.(\d+)').firstMatch(value);
|
||||||
|
if (match == null) return const [0, 0, 0];
|
||||||
|
return [
|
||||||
|
int.tryParse(match.group(1) ?? '') ?? 0,
|
||||||
|
int.tryParse(match.group(2) ?? '') ?? 0,
|
||||||
|
int.tryParse(match.group(3) ?? '') ?? 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,8 +2,11 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
import '../../../app/app_version.dart';
|
||||||
import '../../../backend/backend.dart';
|
import '../../../backend/backend.dart';
|
||||||
|
import '../../../core/utils/result.dart';
|
||||||
import '../../../shared/widgets/adaptive_scaffold.dart';
|
import '../../../shared/widgets/adaptive_scaffold.dart';
|
||||||
import '../../../shared/widgets/error_view.dart';
|
import '../../../shared/widgets/error_view.dart';
|
||||||
import 'settings_controller.dart';
|
import 'settings_controller.dart';
|
||||||
@@ -177,8 +180,14 @@ class SettingsScreen extends ConsumerWidget {
|
|||||||
icon: const Icon(Icons.download_for_offline_rounded),
|
icon: const Icon(Icons.download_for_offline_rounded),
|
||||||
label: const Text('Import settings and providers JSON'),
|
label: const Text('Import settings and providers JSON'),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
FilledButton.tonalIcon(
|
||||||
|
onPressed: () => _checkUpdates(context, ref),
|
||||||
|
icon: const Icon(Icons.system_update_alt_rounded),
|
||||||
|
label: const Text('Check for updates'),
|
||||||
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
const Text('Version 0.1.0'),
|
const Text('Version $appDisplayVersion'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -220,6 +229,82 @@ class SettingsScreen extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
controller.dispose();
|
controller.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _checkUpdates(BuildContext context, WidgetRef ref) async {
|
||||||
|
final messenger = ScaffoldMessenger.of(context);
|
||||||
|
messenger.showSnackBar(
|
||||||
|
const SnackBar(content: Text('Checking for updates...')),
|
||||||
|
);
|
||||||
|
final result =
|
||||||
|
await ref.read(updateServiceProvider).checkForUpdates(force: true);
|
||||||
|
if (!context.mounted) return;
|
||||||
|
messenger.hideCurrentSnackBar();
|
||||||
|
if (result is Error<AppUpdateInfo?>) {
|
||||||
|
messenger.showSnackBar(
|
||||||
|
SnackBar(content: Text(result.failure.message)),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final update = (result as Success<AppUpdateInfo?>).data;
|
||||||
|
if (update == null) {
|
||||||
|
messenger.showSnackBar(
|
||||||
|
const SnackBar(content: Text('RuleGel is up to date')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await _updateDialog(context, ref, update);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _updateDialog(
|
||||||
|
BuildContext context,
|
||||||
|
WidgetRef ref,
|
||||||
|
AppUpdateInfo info,
|
||||||
|
) async {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Text('RuleGel ${info.version} is available'),
|
||||||
|
content: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 520),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Text(
|
||||||
|
info.body.trim().isEmpty ? info.name : info.body.trim(),
|
||||||
|
maxLines: 14,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await ref.read(updateServiceProvider).skipVersion(info);
|
||||||
|
if (context.mounted) Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: const Text('Skip this version'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await ref.read(updateServiceProvider).remindLater();
|
||||||
|
if (context.mounted) Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: const Text('Later'),
|
||||||
|
),
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: () async {
|
||||||
|
final url = Uri.tryParse(info.htmlUrl);
|
||||||
|
if (url != null) {
|
||||||
|
await launchUrl(url, mode: LaunchMode.externalApplication);
|
||||||
|
}
|
||||||
|
if (context.mounted) Navigator.pop(context);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.open_in_new_rounded),
|
||||||
|
label: const Text('Update'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
ref.invalidate(settingsControllerProvider);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _TagListEditor extends StatefulWidget {
|
class _TagListEditor extends StatefulWidget {
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
name: gel_rule_app
|
name: gel_rule_app
|
||||||
description: Local backend/core layer for a Flutter booru browser app.
|
description: Local backend/core layer for a Flutter booru browser app.
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
version: 0.1.0+1
|
version: 0.4.0+4
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.3.0 <4.0.0"
|
sdk: ">=3.3.0 <4.0.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user