mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-07 13:38:13 -08:00
feature: Added update notification and download links per platform (#362)
Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
parent
5ef7936c33
commit
2c71dde228
16 changed files with 712 additions and 8 deletions
|
|
@ -1224,5 +1224,16 @@
|
|||
"playbackType": "Playback type",
|
||||
"playbackTypeDirect": "Direct",
|
||||
"playbackTypeTranscode": "Transcode",
|
||||
"playbackTypeOffline": "Offline"
|
||||
"playbackTypeOffline": "Offline",
|
||||
"latestReleases": "Latest releases",
|
||||
"autoCheckForUpdates": "Periodically check for updates",
|
||||
"newReleaseFoundTitle": "Update {newRelease} available!",
|
||||
"@newReleaseFoundTitle": {
|
||||
"placeholders": {
|
||||
"newRelease": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"newUpdateFoundOnGithub": "Found a new update on Github"
|
||||
}
|
||||
|
|
@ -34,6 +34,8 @@ class ClientSettingsModel with _$ClientSettingsModel {
|
|||
@Default(false) bool showAllCollectionTypes,
|
||||
@Default(2) int maxConcurrentDownloads,
|
||||
@Default(DynamicSchemeVariant.rainbow) DynamicSchemeVariant schemeVariant,
|
||||
@Default(true) bool checkForUpdates,
|
||||
String? lastViewedUpdate,
|
||||
int? libraryPageSize,
|
||||
}) = _ClientSettingsModel;
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ mixin _$ClientSettingsModel {
|
|||
bool get showAllCollectionTypes => throw _privateConstructorUsedError;
|
||||
int get maxConcurrentDownloads => throw _privateConstructorUsedError;
|
||||
DynamicSchemeVariant get schemeVariant => throw _privateConstructorUsedError;
|
||||
bool get checkForUpdates => throw _privateConstructorUsedError;
|
||||
String? get lastViewedUpdate => throw _privateConstructorUsedError;
|
||||
int? get libraryPageSize => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this ClientSettingsModel to a JSON map.
|
||||
|
|
@ -78,6 +80,8 @@ abstract class $ClientSettingsModelCopyWith<$Res> {
|
|||
bool showAllCollectionTypes,
|
||||
int maxConcurrentDownloads,
|
||||
DynamicSchemeVariant schemeVariant,
|
||||
bool checkForUpdates,
|
||||
String? lastViewedUpdate,
|
||||
int? libraryPageSize});
|
||||
}
|
||||
|
||||
|
|
@ -115,6 +119,8 @@ class _$ClientSettingsModelCopyWithImpl<$Res, $Val extends ClientSettingsModel>
|
|||
Object? showAllCollectionTypes = null,
|
||||
Object? maxConcurrentDownloads = null,
|
||||
Object? schemeVariant = null,
|
||||
Object? checkForUpdates = null,
|
||||
Object? lastViewedUpdate = freezed,
|
||||
Object? libraryPageSize = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
|
|
@ -194,6 +200,14 @@ class _$ClientSettingsModelCopyWithImpl<$Res, $Val extends ClientSettingsModel>
|
|||
? _value.schemeVariant
|
||||
: schemeVariant // ignore: cast_nullable_to_non_nullable
|
||||
as DynamicSchemeVariant,
|
||||
checkForUpdates: null == checkForUpdates
|
||||
? _value.checkForUpdates
|
||||
: checkForUpdates // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
lastViewedUpdate: freezed == lastViewedUpdate
|
||||
? _value.lastViewedUpdate
|
||||
: lastViewedUpdate // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
libraryPageSize: freezed == libraryPageSize
|
||||
? _value.libraryPageSize
|
||||
: libraryPageSize // ignore: cast_nullable_to_non_nullable
|
||||
|
|
@ -230,6 +244,8 @@ abstract class _$$ClientSettingsModelImplCopyWith<$Res>
|
|||
bool showAllCollectionTypes,
|
||||
int maxConcurrentDownloads,
|
||||
DynamicSchemeVariant schemeVariant,
|
||||
bool checkForUpdates,
|
||||
String? lastViewedUpdate,
|
||||
int? libraryPageSize});
|
||||
}
|
||||
|
||||
|
|
@ -265,6 +281,8 @@ class __$$ClientSettingsModelImplCopyWithImpl<$Res>
|
|||
Object? showAllCollectionTypes = null,
|
||||
Object? maxConcurrentDownloads = null,
|
||||
Object? schemeVariant = null,
|
||||
Object? checkForUpdates = null,
|
||||
Object? lastViewedUpdate = freezed,
|
||||
Object? libraryPageSize = freezed,
|
||||
}) {
|
||||
return _then(_$ClientSettingsModelImpl(
|
||||
|
|
@ -344,6 +362,14 @@ class __$$ClientSettingsModelImplCopyWithImpl<$Res>
|
|||
? _value.schemeVariant
|
||||
: schemeVariant // ignore: cast_nullable_to_non_nullable
|
||||
as DynamicSchemeVariant,
|
||||
checkForUpdates: null == checkForUpdates
|
||||
? _value.checkForUpdates
|
||||
: checkForUpdates // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
lastViewedUpdate: freezed == lastViewedUpdate
|
||||
? _value.lastViewedUpdate
|
||||
: lastViewedUpdate // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
libraryPageSize: freezed == libraryPageSize
|
||||
? _value.libraryPageSize
|
||||
: libraryPageSize // ignore: cast_nullable_to_non_nullable
|
||||
|
|
@ -376,6 +402,8 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
|
|||
this.showAllCollectionTypes = false,
|
||||
this.maxConcurrentDownloads = 2,
|
||||
this.schemeVariant = DynamicSchemeVariant.rainbow,
|
||||
this.checkForUpdates = true,
|
||||
this.lastViewedUpdate,
|
||||
this.libraryPageSize})
|
||||
: super._();
|
||||
|
||||
|
|
@ -437,11 +465,16 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
|
|||
@JsonKey()
|
||||
final DynamicSchemeVariant schemeVariant;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool checkForUpdates;
|
||||
@override
|
||||
final String? lastViewedUpdate;
|
||||
@override
|
||||
final int? libraryPageSize;
|
||||
|
||||
@override
|
||||
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
||||
return 'ClientSettingsModel(syncPath: $syncPath, position: $position, size: $size, timeOut: $timeOut, nextUpDateCutoff: $nextUpDateCutoff, themeMode: $themeMode, themeColor: $themeColor, amoledBlack: $amoledBlack, blurPlaceHolders: $blurPlaceHolders, blurUpcomingEpisodes: $blurUpcomingEpisodes, selectedLocale: $selectedLocale, enableMediaKeys: $enableMediaKeys, posterSize: $posterSize, pinchPosterZoom: $pinchPosterZoom, mouseDragSupport: $mouseDragSupport, requireWifi: $requireWifi, showAllCollectionTypes: $showAllCollectionTypes, maxConcurrentDownloads: $maxConcurrentDownloads, schemeVariant: $schemeVariant, libraryPageSize: $libraryPageSize)';
|
||||
return 'ClientSettingsModel(syncPath: $syncPath, position: $position, size: $size, timeOut: $timeOut, nextUpDateCutoff: $nextUpDateCutoff, themeMode: $themeMode, themeColor: $themeColor, amoledBlack: $amoledBlack, blurPlaceHolders: $blurPlaceHolders, blurUpcomingEpisodes: $blurUpcomingEpisodes, selectedLocale: $selectedLocale, enableMediaKeys: $enableMediaKeys, posterSize: $posterSize, pinchPosterZoom: $pinchPosterZoom, mouseDragSupport: $mouseDragSupport, requireWifi: $requireWifi, showAllCollectionTypes: $showAllCollectionTypes, maxConcurrentDownloads: $maxConcurrentDownloads, schemeVariant: $schemeVariant, checkForUpdates: $checkForUpdates, lastViewedUpdate: $lastViewedUpdate, libraryPageSize: $libraryPageSize)';
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -470,6 +503,8 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
|
|||
..add(
|
||||
DiagnosticsProperty('maxConcurrentDownloads', maxConcurrentDownloads))
|
||||
..add(DiagnosticsProperty('schemeVariant', schemeVariant))
|
||||
..add(DiagnosticsProperty('checkForUpdates', checkForUpdates))
|
||||
..add(DiagnosticsProperty('lastViewedUpdate', lastViewedUpdate))
|
||||
..add(DiagnosticsProperty('libraryPageSize', libraryPageSize));
|
||||
}
|
||||
|
||||
|
|
@ -514,6 +549,10 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
|
|||
other.maxConcurrentDownloads == maxConcurrentDownloads) &&
|
||||
(identical(other.schemeVariant, schemeVariant) ||
|
||||
other.schemeVariant == schemeVariant) &&
|
||||
(identical(other.checkForUpdates, checkForUpdates) ||
|
||||
other.checkForUpdates == checkForUpdates) &&
|
||||
(identical(other.lastViewedUpdate, lastViewedUpdate) ||
|
||||
other.lastViewedUpdate == lastViewedUpdate) &&
|
||||
(identical(other.libraryPageSize, libraryPageSize) ||
|
||||
other.libraryPageSize == libraryPageSize));
|
||||
}
|
||||
|
|
@ -541,6 +580,8 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
|
|||
showAllCollectionTypes,
|
||||
maxConcurrentDownloads,
|
||||
schemeVariant,
|
||||
checkForUpdates,
|
||||
lastViewedUpdate,
|
||||
libraryPageSize
|
||||
]);
|
||||
|
||||
|
|
@ -582,6 +623,8 @@ abstract class _ClientSettingsModel extends ClientSettingsModel {
|
|||
final bool showAllCollectionTypes,
|
||||
final int maxConcurrentDownloads,
|
||||
final DynamicSchemeVariant schemeVariant,
|
||||
final bool checkForUpdates,
|
||||
final String? lastViewedUpdate,
|
||||
final int? libraryPageSize}) = _$ClientSettingsModelImpl;
|
||||
_ClientSettingsModel._() : super._();
|
||||
|
||||
|
|
@ -628,6 +671,10 @@ abstract class _ClientSettingsModel extends ClientSettingsModel {
|
|||
@override
|
||||
DynamicSchemeVariant get schemeVariant;
|
||||
@override
|
||||
bool get checkForUpdates;
|
||||
@override
|
||||
String? get lastViewedUpdate;
|
||||
@override
|
||||
int? get libraryPageSize;
|
||||
|
||||
/// Create a copy of ClientSettingsModel
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ _$ClientSettingsModelImpl _$$ClientSettingsModelImplFromJson(
|
|||
schemeVariant: $enumDecodeNullable(
|
||||
_$DynamicSchemeVariantEnumMap, json['schemeVariant']) ??
|
||||
DynamicSchemeVariant.rainbow,
|
||||
checkForUpdates: json['checkForUpdates'] as bool? ?? true,
|
||||
lastViewedUpdate: json['lastViewedUpdate'] as String?,
|
||||
libraryPageSize: (json['libraryPageSize'] as num?)?.toInt(),
|
||||
);
|
||||
|
||||
|
|
@ -66,6 +68,8 @@ Map<String, dynamic> _$$ClientSettingsModelImplToJson(
|
|||
'showAllCollectionTypes': instance.showAllCollectionTypes,
|
||||
'maxConcurrentDownloads': instance.maxConcurrentDownloads,
|
||||
'schemeVariant': _$DynamicSchemeVariantEnumMap[instance.schemeVariant]!,
|
||||
'checkForUpdates': instance.checkForUpdates,
|
||||
'lastViewedUpdate': instance.lastViewedUpdate,
|
||||
'libraryPageSize': instance.libraryPageSize,
|
||||
};
|
||||
|
||||
|
|
|
|||
77
lib/providers/update_provider.dart
Normal file
77
lib/providers/update_provider.dart
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||
import 'package:fladder/util/update_checker.dart';
|
||||
|
||||
part 'update_provider.freezed.dart';
|
||||
part 'update_provider.g.dart';
|
||||
|
||||
Set<TargetPlatform> get _directUpdatePlatforms => {
|
||||
TargetPlatform.linux,
|
||||
TargetPlatform.macOS,
|
||||
TargetPlatform.windows,
|
||||
};
|
||||
|
||||
final hasNewUpdateProvider = Provider<bool>((ref) {
|
||||
//Disable update notification for platforms that are updated outside of Github.
|
||||
if (!_directUpdatePlatforms.contains(defaultTargetPlatform) || kIsWeb) {
|
||||
return false;
|
||||
}
|
||||
return ref.watch(clientSettingsProvider.select((value) => value.lastViewedUpdate)) !=
|
||||
ref.watch(updateProvider.select((value) => value.latestRelease?.version));
|
||||
});
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class Update extends _$Update {
|
||||
final updateChecker = UpdateChecker();
|
||||
|
||||
Timer? _timer;
|
||||
|
||||
@override
|
||||
UpdatesModel build() {
|
||||
final checkForUpdates = ref.watch(clientSettingsProvider.select((value) => value.checkForUpdates));
|
||||
|
||||
if (!checkForUpdates) {
|
||||
_timer?.cancel();
|
||||
return UpdatesModel();
|
||||
}
|
||||
ref.onDispose(() {
|
||||
_timer?.cancel();
|
||||
});
|
||||
|
||||
_timer?.cancel();
|
||||
|
||||
_timer = Timer.periodic(const Duration(minutes: 30), (timer) {
|
||||
_fetchLatest();
|
||||
});
|
||||
|
||||
_fetchLatest();
|
||||
|
||||
return UpdatesModel();
|
||||
}
|
||||
|
||||
Future<List<ReleaseInfo>> _fetchLatest() async {
|
||||
final latest = await updateChecker.fetchRecentReleases();
|
||||
state = state.copyWith(
|
||||
lastRelease: latest,
|
||||
);
|
||||
return latest;
|
||||
}
|
||||
}
|
||||
|
||||
@Freezed(toJson: false, fromJson: false)
|
||||
class UpdatesModel with _$UpdatesModel {
|
||||
const UpdatesModel._();
|
||||
|
||||
factory UpdatesModel({
|
||||
@Default([]) List<ReleaseInfo> lastRelease,
|
||||
}) = _UpdatesModel;
|
||||
|
||||
ReleaseInfo? get latestRelease => lastRelease.firstWhereOrNull((value) => value.isNewerThanCurrent);
|
||||
}
|
||||
163
lib/providers/update_provider.freezed.dart
Normal file
163
lib/providers/update_provider.freezed.dart
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'update_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
/// @nodoc
|
||||
mixin _$UpdatesModel {
|
||||
List<ReleaseInfo> get lastRelease => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of UpdatesModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$UpdatesModelCopyWith<UpdatesModel> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $UpdatesModelCopyWith<$Res> {
|
||||
factory $UpdatesModelCopyWith(
|
||||
UpdatesModel value, $Res Function(UpdatesModel) then) =
|
||||
_$UpdatesModelCopyWithImpl<$Res, UpdatesModel>;
|
||||
@useResult
|
||||
$Res call({List<ReleaseInfo> lastRelease});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$UpdatesModelCopyWithImpl<$Res, $Val extends UpdatesModel>
|
||||
implements $UpdatesModelCopyWith<$Res> {
|
||||
_$UpdatesModelCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of UpdatesModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? lastRelease = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
lastRelease: null == lastRelease
|
||||
? _value.lastRelease
|
||||
: lastRelease // ignore: cast_nullable_to_non_nullable
|
||||
as List<ReleaseInfo>,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$UpdatesModelImplCopyWith<$Res>
|
||||
implements $UpdatesModelCopyWith<$Res> {
|
||||
factory _$$UpdatesModelImplCopyWith(
|
||||
_$UpdatesModelImpl value, $Res Function(_$UpdatesModelImpl) then) =
|
||||
__$$UpdatesModelImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({List<ReleaseInfo> lastRelease});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$UpdatesModelImplCopyWithImpl<$Res>
|
||||
extends _$UpdatesModelCopyWithImpl<$Res, _$UpdatesModelImpl>
|
||||
implements _$$UpdatesModelImplCopyWith<$Res> {
|
||||
__$$UpdatesModelImplCopyWithImpl(
|
||||
_$UpdatesModelImpl _value, $Res Function(_$UpdatesModelImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of UpdatesModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? lastRelease = null,
|
||||
}) {
|
||||
return _then(_$UpdatesModelImpl(
|
||||
lastRelease: null == lastRelease
|
||||
? _value._lastRelease
|
||||
: lastRelease // ignore: cast_nullable_to_non_nullable
|
||||
as List<ReleaseInfo>,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _$UpdatesModelImpl extends _UpdatesModel with DiagnosticableTreeMixin {
|
||||
_$UpdatesModelImpl({final List<ReleaseInfo> lastRelease = const []})
|
||||
: _lastRelease = lastRelease,
|
||||
super._();
|
||||
|
||||
final List<ReleaseInfo> _lastRelease;
|
||||
@override
|
||||
@JsonKey()
|
||||
List<ReleaseInfo> get lastRelease {
|
||||
if (_lastRelease is EqualUnmodifiableListView) return _lastRelease;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_lastRelease);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
||||
return 'UpdatesModel(lastRelease: $lastRelease)';
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(DiagnosticsProperty('type', 'UpdatesModel'))
|
||||
..add(DiagnosticsProperty('lastRelease', lastRelease));
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$UpdatesModelImpl &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._lastRelease, _lastRelease));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType, const DeepCollectionEquality().hash(_lastRelease));
|
||||
|
||||
/// Create a copy of UpdatesModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$UpdatesModelImplCopyWith<_$UpdatesModelImpl> get copyWith =>
|
||||
__$$UpdatesModelImplCopyWithImpl<_$UpdatesModelImpl>(this, _$identity);
|
||||
}
|
||||
|
||||
abstract class _UpdatesModel extends UpdatesModel {
|
||||
factory _UpdatesModel({final List<ReleaseInfo> lastRelease}) =
|
||||
_$UpdatesModelImpl;
|
||||
_UpdatesModel._() : super._();
|
||||
|
||||
@override
|
||||
List<ReleaseInfo> get lastRelease;
|
||||
|
||||
/// Create a copy of UpdatesModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$UpdatesModelImplCopyWith<_$UpdatesModelImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
24
lib/providers/update_provider.g.dart
Normal file
24
lib/providers/update_provider.g.dart
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'update_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$updateHash() => r'97f7aca4e255d654a9295e1e9e019536faf6455e';
|
||||
|
||||
/// See also [Update].
|
||||
@ProviderFor(Update)
|
||||
final updateProvider = NotifierProvider<Update, UpdatesModel>.internal(
|
||||
Update.new,
|
||||
name: r'updateProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$updateHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$Update = Notifier<UpdatesModel>;
|
||||
// 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
|
||||
|
|
@ -7,6 +7,7 @@ import 'package:iconsax_plus/iconsax_plus.dart';
|
|||
|
||||
import 'package:fladder/screens/crash_screen/crash_screen.dart';
|
||||
import 'package:fladder/screens/settings/settings_scaffold.dart';
|
||||
import 'package:fladder/screens/settings/widgets/settings_update_information.dart';
|
||||
import 'package:fladder/screens/shared/fladder_icon.dart';
|
||||
import 'package:fladder/screens/shared/fladder_logo.dart';
|
||||
import 'package:fladder/screens/shared/media/external_urls.dart';
|
||||
|
|
@ -42,6 +43,7 @@ class AboutSettingsPage extends ConsumerWidget {
|
|||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final applicationInfo = ref.watch(applicationInfoProvider);
|
||||
|
||||
return SettingsScaffold(
|
||||
label: "",
|
||||
items: [
|
||||
|
|
@ -114,6 +116,7 @@ class AboutSettingsPage extends ConsumerWidget {
|
|||
)
|
||||
],
|
||||
),
|
||||
const SettingsUpdateInformation(),
|
||||
].addInBetween(const SizedBox(height: 16)),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:window_manager/window_manager.dart';
|
|||
|
||||
import 'package:fladder/providers/arguments_provider.dart';
|
||||
import 'package:fladder/providers/auth_provider.dart';
|
||||
import 'package:fladder/providers/update_provider.dart';
|
||||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:fladder/routes/auto_router.gr.dart';
|
||||
import 'package:fladder/screens/settings/quick_connect_window.dart';
|
||||
|
|
@ -99,6 +100,10 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
|||
final quickConnectAvailable =
|
||||
ref.watch(userProvider.select((value) => value?.serverConfiguration?.quickConnectAvailable ?? false));
|
||||
|
||||
final newRelease = ref.watch(updateProvider.select((value) => value.latestRelease));
|
||||
|
||||
final hasNewUpdate = ref.watch(hasNewUpdateProvider);
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(left: AdaptiveLayout.of(context).sideBarWidth),
|
||||
child: Container(
|
||||
|
|
@ -109,6 +114,18 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
|||
showBackButtonNested: true,
|
||||
showUserIcon: true,
|
||||
items: [
|
||||
if (hasNewUpdate && newRelease != null) ...[
|
||||
Card(
|
||||
color: context.colors.secondaryContainer,
|
||||
child: SettingsListTile(
|
||||
label: Text(context.localized.newReleaseFoundTitle(newRelease.version)),
|
||||
subLabel: Text(context.localized.newUpdateFoundOnGithub),
|
||||
icon: IconsaxPlusLinear.information,
|
||||
onTap: () => navigateTo(const AboutSettingsRoute()),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.settingsClientTitle),
|
||||
subLabel: Text(context.localized.settingsClientDesc),
|
||||
|
|
@ -138,7 +155,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
|||
),
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.about),
|
||||
subLabel: const Text("Fladder"),
|
||||
subLabel: Text("Fladder, ${context.localized.latestReleases}"),
|
||||
selected: containsRoute(const AboutSettingsRoute()),
|
||||
leading: Opacity(
|
||||
opacity: 1,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
|
||||
|
|
|
|||
134
lib/screens/settings/widgets/settings_update_information.dart
Normal file
134
lib/screens/settings/widgets/settings_update_information.dart
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:markdown_widget/widget/markdown.dart';
|
||||
|
||||
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||
import 'package:fladder/providers/update_provider.dart';
|
||||
import 'package:fladder/screens/settings/settings_list_tile.dart';
|
||||
import 'package:fladder/screens/shared/media/external_urls.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/theme_extensions.dart';
|
||||
import 'package:fladder/util/update_checker.dart';
|
||||
|
||||
class SettingsUpdateInformation extends ConsumerStatefulWidget {
|
||||
const SettingsUpdateInformation({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() => _SettingsUpdateInformationState();
|
||||
}
|
||||
|
||||
class _SettingsUpdateInformationState extends ConsumerState<SettingsUpdateInformation> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((value) {
|
||||
final latestRelease = ref.read(updateProvider.select((value) => value.latestRelease));
|
||||
if (latestRelease == null) return;
|
||||
final lastViewedUpdate = ref.read(clientSettingsProvider.select((value) => value.lastViewedUpdate));
|
||||
if (lastViewedUpdate != latestRelease.version) {
|
||||
ref
|
||||
.read(clientSettingsProvider.notifier)
|
||||
.update((value) => value.copyWith(lastViewedUpdate: latestRelease.version));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final updates = ref.watch(updateProvider);
|
||||
final latestRelease = updates.latestRelease;
|
||||
final otherReleases = updates.lastRelease;
|
||||
final checkForUpdate = ref.watch(clientSettingsProvider.select((value) => value.checkForUpdates));
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Column(
|
||||
spacing: 8,
|
||||
children: [
|
||||
const Divider(),
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.latestReleases),
|
||||
subLabel: Text(context.localized.autoCheckForUpdates),
|
||||
onTap: () => ref
|
||||
.read(clientSettingsProvider.notifier)
|
||||
.update((value) => value.copyWith(checkForUpdates: !checkForUpdate)),
|
||||
trailing: Switch(
|
||||
value: checkForUpdate,
|
||||
onChanged: (value) => ref
|
||||
.read(clientSettingsProvider.notifier)
|
||||
.update((value) => value.copyWith(checkForUpdates: !checkForUpdate)),
|
||||
),
|
||||
),
|
||||
if (checkForUpdate) ...[
|
||||
if (latestRelease != null)
|
||||
UpdateInformation(
|
||||
releaseInfo: latestRelease,
|
||||
expanded: true,
|
||||
),
|
||||
...otherReleases.where((element) => element != latestRelease).map(
|
||||
(value) => UpdateInformation(releaseInfo: value),
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UpdateInformation extends StatelessWidget {
|
||||
final ReleaseInfo releaseInfo;
|
||||
final bool expanded;
|
||||
const UpdateInformation({
|
||||
required this.releaseInfo,
|
||||
this.expanded = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ExpansionTile(
|
||||
backgroundColor:
|
||||
releaseInfo.isNewerThanCurrent ? context.colors.primaryContainer : context.colors.surfaceContainer,
|
||||
collapsedBackgroundColor: releaseInfo.isNewerThanCurrent ? context.colors.primaryContainer : null,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
collapsedShape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
title: Text(releaseInfo.version),
|
||||
initiallyExpanded: expanded,
|
||||
childrenPadding: const EdgeInsets.all(16),
|
||||
expandedCrossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: MarkdownWidget(
|
||||
data: releaseInfo.changelog,
|
||||
shrinkWrap: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
...releaseInfo.preferredDownloads.entries.map(
|
||||
(entry) {
|
||||
return FilledButton(
|
||||
onPressed: () => launchUrl(context, entry.value),
|
||||
child: Text(
|
||||
entry.key.prettifyKey(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
...releaseInfo.otherDownloads.entries.map(
|
||||
(entry) {
|
||||
return ElevatedButton(
|
||||
onPressed: () => launchUrl(context, entry.value),
|
||||
child: Text(
|
||||
entry.key.prettifyKey(),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
].addInBetween(const SizedBox(height: 12)),
|
||||
);
|
||||
}
|
||||
}
|
||||
150
lib/util/update_checker.dart
Normal file
150
lib/util/update_checker.dart
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
class ReleaseInfo {
|
||||
final String version;
|
||||
final String changelog;
|
||||
final String url;
|
||||
final bool isNewerThanCurrent;
|
||||
final Map<String, String> downloads;
|
||||
|
||||
ReleaseInfo({
|
||||
required this.version,
|
||||
required this.changelog,
|
||||
required this.url,
|
||||
required this.isNewerThanCurrent,
|
||||
required this.downloads,
|
||||
});
|
||||
|
||||
String? downloadUrlFor(String platform) => downloads[platform];
|
||||
|
||||
Map<String, String> get preferredDownloads {
|
||||
final group = _platformGroup();
|
||||
final entries = downloads.entries.where((e) => e.key.contains(group));
|
||||
return Map.fromEntries(entries);
|
||||
}
|
||||
|
||||
Map<String, String> get otherDownloads {
|
||||
final group = _platformGroup();
|
||||
final entries = downloads.entries.where((e) => !e.key.contains(group));
|
||||
return Map.fromEntries(entries);
|
||||
}
|
||||
|
||||
String _platformGroup() {
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
return 'android';
|
||||
case TargetPlatform.iOS:
|
||||
return 'ios';
|
||||
case TargetPlatform.windows:
|
||||
return 'windows';
|
||||
case TargetPlatform.macOS:
|
||||
return 'macos';
|
||||
case TargetPlatform.linux:
|
||||
return 'linux';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DownloadLabelFormatter on String {
|
||||
String prettifyKey() {
|
||||
final parts = split('_');
|
||||
if (parts.isEmpty) return this;
|
||||
|
||||
final base = parts.first.capitalize();
|
||||
if (parts.length == 1) return base;
|
||||
|
||||
final variant = parts.sublist(1).join(' ').capitalize();
|
||||
return '$base ($variant)';
|
||||
}
|
||||
|
||||
String capitalize() => isEmpty ? this : '${this[0].toUpperCase()}${substring(1)}';
|
||||
}
|
||||
|
||||
class UpdateChecker {
|
||||
final String owner = 'DonutWare';
|
||||
final String repo = 'Fladder';
|
||||
|
||||
Future<List<ReleaseInfo>> fetchRecentReleases({int count = 5}) async {
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
final currentVersion = info.version;
|
||||
|
||||
final url = Uri.parse('https://api.github.com/repos/$owner/$repo/releases?per_page=$count');
|
||||
final response = await http.get(url);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
print('Failed to fetch releases: ${response.statusCode}');
|
||||
return [];
|
||||
}
|
||||
|
||||
final List<dynamic> releases = jsonDecode(response.body);
|
||||
return releases.map((json) {
|
||||
final tag = (json['tag_name'] as String?)?.replaceFirst(RegExp(r'^v'), '');
|
||||
final changelog = json['body'] as String? ?? '';
|
||||
final htmlUrl = json['html_url'] as String? ?? '';
|
||||
final assets = json['assets'] as List<dynamic>? ?? [];
|
||||
|
||||
final Map<String, String> downloads = {};
|
||||
for (final asset in assets) {
|
||||
final name = asset['name'] as String? ?? '';
|
||||
final downloadUrl = asset['browser_download_url'] as String? ?? '';
|
||||
|
||||
if (name.contains('Android') && name.endsWith('.apk')) {
|
||||
downloads['android'] = downloadUrl;
|
||||
} else if (name.contains('iOS') && name.endsWith('.ipa')) {
|
||||
downloads['ios'] = downloadUrl;
|
||||
} else if (name.contains('Windows') && name.endsWith('Setup.exe')) {
|
||||
downloads['windows_installer'] = downloadUrl;
|
||||
} else if (name.contains('Windows') && name.endsWith('.zip')) {
|
||||
downloads['windows_portable'] = downloadUrl;
|
||||
} else if (name.contains('macOS') && name.endsWith('.dmg')) {
|
||||
downloads['macos'] = downloadUrl;
|
||||
} else if (name.contains('Linux') && name.endsWith('.AppImage')) {
|
||||
downloads['linux_appimage'] = downloadUrl;
|
||||
} else if (name.contains('Linux') && name.endsWith('.flatpak')) {
|
||||
downloads['linux_flatpak'] = downloadUrl;
|
||||
} else if (name.contains('Linux') && name.endsWith('.zip')) {
|
||||
downloads['linux_zip'] = downloadUrl;
|
||||
} else if (name.contains('Linux') && name.endsWith('.zsync')) {
|
||||
downloads['linux_zsync'] = downloadUrl;
|
||||
} else if (name.contains('Web') && name.endsWith('.zip')) {
|
||||
downloads['web'] = downloadUrl;
|
||||
}
|
||||
}
|
||||
|
||||
bool isNewer = tag != null && _compareVersions(tag, currentVersion) > 0;
|
||||
|
||||
return ReleaseInfo(
|
||||
version: tag ?? 'unknown',
|
||||
changelog: changelog.trim(),
|
||||
url: htmlUrl,
|
||||
isNewerThanCurrent: isNewer,
|
||||
downloads: downloads,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
Future<bool> isUpToDate() async {
|
||||
final releases = await fetchRecentReleases(count: 1);
|
||||
if (releases.isEmpty) return true;
|
||||
return !releases.first.isNewerThanCurrent;
|
||||
}
|
||||
|
||||
static int _compareVersions(String a, String b) {
|
||||
final aParts = a.split('.').map(int.tryParse).toList();
|
||||
final bParts = b.split('.').map(int.tryParse).toList();
|
||||
|
||||
for (var i = 0; i < aParts.length || i < bParts.length; i++) {
|
||||
final aVal = i < aParts.length ? (aParts[i] ?? 0) : 0;
|
||||
final bVal = i < bParts.length ? (bParts[i] ?? 0) : 0;
|
||||
if (aVal != bVal) return aVal.compareTo(bVal);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -2,20 +2,25 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
|
||||
import 'package:fladder/providers/update_provider.dart';
|
||||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:fladder/routes/auto_router.gr.dart';
|
||||
import 'package:fladder/screens/shared/flat_button.dart';
|
||||
import 'package:fladder/screens/shared/user_icon.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/theme_extensions.dart';
|
||||
|
||||
class SettingsUserIcon extends ConsumerWidget {
|
||||
const SettingsUserIcon({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final size = 24.0;
|
||||
final user = ref.watch(userProvider);
|
||||
final hasNewUpdate = ref.watch(hasNewUpdateProvider);
|
||||
return Tooltip(
|
||||
message: context.localized.settings,
|
||||
waitDuration: const Duration(seconds: 1),
|
||||
|
|
@ -28,9 +33,35 @@ class SettingsUserIcon extends ConsumerWidget {
|
|||
context.router.push(const ClientSettingsRoute());
|
||||
}
|
||||
},
|
||||
child: UserIcon(
|
||||
user: user,
|
||||
cornerRadius: 200,
|
||||
child: Stack(
|
||||
alignment: Alignment.bottomRight,
|
||||
children: [
|
||||
UserIcon(
|
||||
user: user,
|
||||
cornerRadius: 200,
|
||||
),
|
||||
if (hasNewUpdate)
|
||||
Transform.translate(
|
||||
offset: Offset(size / 4, size / 4),
|
||||
child: Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
color: context.colors.surface,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: FittedBox(
|
||||
child: Icon(
|
||||
IconsaxPlusBold.information,
|
||||
color: context.colors.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -221,7 +221,7 @@ class _SideNavigationBarState extends ConsumerState<SideNavigationBar> {
|
|||
selectedIcon: const Icon(IconsaxPlusBold.setting_3),
|
||||
horizontal: true,
|
||||
expanded: shouldExpand,
|
||||
icon: const SizedBox(height: 32, child: SettingsUserIcon()),
|
||||
icon: const SettingsUserIcon(),
|
||||
onPressed: () {
|
||||
if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.single) {
|
||||
context.router.push(const SettingsRoute());
|
||||
|
|
|
|||
40
pubspec.lock
40
pubspec.lock
|
|
@ -595,6 +595,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
flutter_highlight:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_highlight
|
||||
sha256: "7b96333867aa07e122e245c033b8ad622e4e3a42a1a2372cbb098a2541d8782c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.0"
|
||||
flutter_keyboard_visibility:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -842,6 +850,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
highlight:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: highlight
|
||||
sha256: "5353a83ffe3e3eca7df0abfb72dcf3fa66cc56b953728e7113ad4ad88497cf21"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.0"
|
||||
html:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1082,6 +1098,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.3.0"
|
||||
markdown_widget:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: markdown_widget
|
||||
sha256: b52c13d3ee4d0e60c812e15b0593f142a3b8a2003cde1babb271d001a1dbdc1c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2+8"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1570,6 +1594,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
scroll_to_index:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: scroll_to_index
|
||||
sha256: b707546e7500d9f070d63e5acf74fd437ec7eeeb68d3412ef7b0afada0b4f176
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
share_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -2055,6 +2087,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.4"
|
||||
visibility_detector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: visibility_detector
|
||||
sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.0+2"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ dependencies:
|
|||
reorderable_grid: ^1.0.10
|
||||
overflow_view: ^0.4.0
|
||||
flutter_sticky_header: ^0.7.0
|
||||
markdown_widget: ^2.3.2+8
|
||||
|
||||
# Navigation
|
||||
auto_route: ^9.3.0+1
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue