mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-14 17:55:58 -07: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",
|
"playbackType": "Playback type",
|
||||||
"playbackTypeDirect": "Direct",
|
"playbackTypeDirect": "Direct",
|
||||||
"playbackTypeTranscode": "Transcode",
|
"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(false) bool showAllCollectionTypes,
|
||||||
@Default(2) int maxConcurrentDownloads,
|
@Default(2) int maxConcurrentDownloads,
|
||||||
@Default(DynamicSchemeVariant.rainbow) DynamicSchemeVariant schemeVariant,
|
@Default(DynamicSchemeVariant.rainbow) DynamicSchemeVariant schemeVariant,
|
||||||
|
@Default(true) bool checkForUpdates,
|
||||||
|
String? lastViewedUpdate,
|
||||||
int? libraryPageSize,
|
int? libraryPageSize,
|
||||||
}) = _ClientSettingsModel;
|
}) = _ClientSettingsModel;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,8 @@ mixin _$ClientSettingsModel {
|
||||||
bool get showAllCollectionTypes => throw _privateConstructorUsedError;
|
bool get showAllCollectionTypes => throw _privateConstructorUsedError;
|
||||||
int get maxConcurrentDownloads => throw _privateConstructorUsedError;
|
int get maxConcurrentDownloads => throw _privateConstructorUsedError;
|
||||||
DynamicSchemeVariant get schemeVariant => throw _privateConstructorUsedError;
|
DynamicSchemeVariant get schemeVariant => throw _privateConstructorUsedError;
|
||||||
|
bool get checkForUpdates => throw _privateConstructorUsedError;
|
||||||
|
String? get lastViewedUpdate => throw _privateConstructorUsedError;
|
||||||
int? get libraryPageSize => throw _privateConstructorUsedError;
|
int? get libraryPageSize => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
/// Serializes this ClientSettingsModel to a JSON map.
|
/// Serializes this ClientSettingsModel to a JSON map.
|
||||||
|
|
@ -78,6 +80,8 @@ abstract class $ClientSettingsModelCopyWith<$Res> {
|
||||||
bool showAllCollectionTypes,
|
bool showAllCollectionTypes,
|
||||||
int maxConcurrentDownloads,
|
int maxConcurrentDownloads,
|
||||||
DynamicSchemeVariant schemeVariant,
|
DynamicSchemeVariant schemeVariant,
|
||||||
|
bool checkForUpdates,
|
||||||
|
String? lastViewedUpdate,
|
||||||
int? libraryPageSize});
|
int? libraryPageSize});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -115,6 +119,8 @@ class _$ClientSettingsModelCopyWithImpl<$Res, $Val extends ClientSettingsModel>
|
||||||
Object? showAllCollectionTypes = null,
|
Object? showAllCollectionTypes = null,
|
||||||
Object? maxConcurrentDownloads = null,
|
Object? maxConcurrentDownloads = null,
|
||||||
Object? schemeVariant = null,
|
Object? schemeVariant = null,
|
||||||
|
Object? checkForUpdates = null,
|
||||||
|
Object? lastViewedUpdate = freezed,
|
||||||
Object? libraryPageSize = freezed,
|
Object? libraryPageSize = freezed,
|
||||||
}) {
|
}) {
|
||||||
return _then(_value.copyWith(
|
return _then(_value.copyWith(
|
||||||
|
|
@ -194,6 +200,14 @@ class _$ClientSettingsModelCopyWithImpl<$Res, $Val extends ClientSettingsModel>
|
||||||
? _value.schemeVariant
|
? _value.schemeVariant
|
||||||
: schemeVariant // ignore: cast_nullable_to_non_nullable
|
: schemeVariant // ignore: cast_nullable_to_non_nullable
|
||||||
as DynamicSchemeVariant,
|
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
|
libraryPageSize: freezed == libraryPageSize
|
||||||
? _value.libraryPageSize
|
? _value.libraryPageSize
|
||||||
: libraryPageSize // ignore: cast_nullable_to_non_nullable
|
: libraryPageSize // ignore: cast_nullable_to_non_nullable
|
||||||
|
|
@ -230,6 +244,8 @@ abstract class _$$ClientSettingsModelImplCopyWith<$Res>
|
||||||
bool showAllCollectionTypes,
|
bool showAllCollectionTypes,
|
||||||
int maxConcurrentDownloads,
|
int maxConcurrentDownloads,
|
||||||
DynamicSchemeVariant schemeVariant,
|
DynamicSchemeVariant schemeVariant,
|
||||||
|
bool checkForUpdates,
|
||||||
|
String? lastViewedUpdate,
|
||||||
int? libraryPageSize});
|
int? libraryPageSize});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -265,6 +281,8 @@ class __$$ClientSettingsModelImplCopyWithImpl<$Res>
|
||||||
Object? showAllCollectionTypes = null,
|
Object? showAllCollectionTypes = null,
|
||||||
Object? maxConcurrentDownloads = null,
|
Object? maxConcurrentDownloads = null,
|
||||||
Object? schemeVariant = null,
|
Object? schemeVariant = null,
|
||||||
|
Object? checkForUpdates = null,
|
||||||
|
Object? lastViewedUpdate = freezed,
|
||||||
Object? libraryPageSize = freezed,
|
Object? libraryPageSize = freezed,
|
||||||
}) {
|
}) {
|
||||||
return _then(_$ClientSettingsModelImpl(
|
return _then(_$ClientSettingsModelImpl(
|
||||||
|
|
@ -344,6 +362,14 @@ class __$$ClientSettingsModelImplCopyWithImpl<$Res>
|
||||||
? _value.schemeVariant
|
? _value.schemeVariant
|
||||||
: schemeVariant // ignore: cast_nullable_to_non_nullable
|
: schemeVariant // ignore: cast_nullable_to_non_nullable
|
||||||
as DynamicSchemeVariant,
|
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
|
libraryPageSize: freezed == libraryPageSize
|
||||||
? _value.libraryPageSize
|
? _value.libraryPageSize
|
||||||
: libraryPageSize // ignore: cast_nullable_to_non_nullable
|
: libraryPageSize // ignore: cast_nullable_to_non_nullable
|
||||||
|
|
@ -376,6 +402,8 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
|
||||||
this.showAllCollectionTypes = false,
|
this.showAllCollectionTypes = false,
|
||||||
this.maxConcurrentDownloads = 2,
|
this.maxConcurrentDownloads = 2,
|
||||||
this.schemeVariant = DynamicSchemeVariant.rainbow,
|
this.schemeVariant = DynamicSchemeVariant.rainbow,
|
||||||
|
this.checkForUpdates = true,
|
||||||
|
this.lastViewedUpdate,
|
||||||
this.libraryPageSize})
|
this.libraryPageSize})
|
||||||
: super._();
|
: super._();
|
||||||
|
|
||||||
|
|
@ -437,11 +465,16 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
|
||||||
@JsonKey()
|
@JsonKey()
|
||||||
final DynamicSchemeVariant schemeVariant;
|
final DynamicSchemeVariant schemeVariant;
|
||||||
@override
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final bool checkForUpdates;
|
||||||
|
@override
|
||||||
|
final String? lastViewedUpdate;
|
||||||
|
@override
|
||||||
final int? libraryPageSize;
|
final int? libraryPageSize;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
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
|
@override
|
||||||
|
|
@ -470,6 +503,8 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
|
||||||
..add(
|
..add(
|
||||||
DiagnosticsProperty('maxConcurrentDownloads', maxConcurrentDownloads))
|
DiagnosticsProperty('maxConcurrentDownloads', maxConcurrentDownloads))
|
||||||
..add(DiagnosticsProperty('schemeVariant', schemeVariant))
|
..add(DiagnosticsProperty('schemeVariant', schemeVariant))
|
||||||
|
..add(DiagnosticsProperty('checkForUpdates', checkForUpdates))
|
||||||
|
..add(DiagnosticsProperty('lastViewedUpdate', lastViewedUpdate))
|
||||||
..add(DiagnosticsProperty('libraryPageSize', libraryPageSize));
|
..add(DiagnosticsProperty('libraryPageSize', libraryPageSize));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -514,6 +549,10 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
|
||||||
other.maxConcurrentDownloads == maxConcurrentDownloads) &&
|
other.maxConcurrentDownloads == maxConcurrentDownloads) &&
|
||||||
(identical(other.schemeVariant, schemeVariant) ||
|
(identical(other.schemeVariant, schemeVariant) ||
|
||||||
other.schemeVariant == schemeVariant) &&
|
other.schemeVariant == schemeVariant) &&
|
||||||
|
(identical(other.checkForUpdates, checkForUpdates) ||
|
||||||
|
other.checkForUpdates == checkForUpdates) &&
|
||||||
|
(identical(other.lastViewedUpdate, lastViewedUpdate) ||
|
||||||
|
other.lastViewedUpdate == lastViewedUpdate) &&
|
||||||
(identical(other.libraryPageSize, libraryPageSize) ||
|
(identical(other.libraryPageSize, libraryPageSize) ||
|
||||||
other.libraryPageSize == libraryPageSize));
|
other.libraryPageSize == libraryPageSize));
|
||||||
}
|
}
|
||||||
|
|
@ -541,6 +580,8 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
|
||||||
showAllCollectionTypes,
|
showAllCollectionTypes,
|
||||||
maxConcurrentDownloads,
|
maxConcurrentDownloads,
|
||||||
schemeVariant,
|
schemeVariant,
|
||||||
|
checkForUpdates,
|
||||||
|
lastViewedUpdate,
|
||||||
libraryPageSize
|
libraryPageSize
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -582,6 +623,8 @@ abstract class _ClientSettingsModel extends ClientSettingsModel {
|
||||||
final bool showAllCollectionTypes,
|
final bool showAllCollectionTypes,
|
||||||
final int maxConcurrentDownloads,
|
final int maxConcurrentDownloads,
|
||||||
final DynamicSchemeVariant schemeVariant,
|
final DynamicSchemeVariant schemeVariant,
|
||||||
|
final bool checkForUpdates,
|
||||||
|
final String? lastViewedUpdate,
|
||||||
final int? libraryPageSize}) = _$ClientSettingsModelImpl;
|
final int? libraryPageSize}) = _$ClientSettingsModelImpl;
|
||||||
_ClientSettingsModel._() : super._();
|
_ClientSettingsModel._() : super._();
|
||||||
|
|
||||||
|
|
@ -628,6 +671,10 @@ abstract class _ClientSettingsModel extends ClientSettingsModel {
|
||||||
@override
|
@override
|
||||||
DynamicSchemeVariant get schemeVariant;
|
DynamicSchemeVariant get schemeVariant;
|
||||||
@override
|
@override
|
||||||
|
bool get checkForUpdates;
|
||||||
|
@override
|
||||||
|
String? get lastViewedUpdate;
|
||||||
|
@override
|
||||||
int? get libraryPageSize;
|
int? get libraryPageSize;
|
||||||
|
|
||||||
/// Create a copy of ClientSettingsModel
|
/// Create a copy of ClientSettingsModel
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,8 @@ _$ClientSettingsModelImpl _$$ClientSettingsModelImplFromJson(
|
||||||
schemeVariant: $enumDecodeNullable(
|
schemeVariant: $enumDecodeNullable(
|
||||||
_$DynamicSchemeVariantEnumMap, json['schemeVariant']) ??
|
_$DynamicSchemeVariantEnumMap, json['schemeVariant']) ??
|
||||||
DynamicSchemeVariant.rainbow,
|
DynamicSchemeVariant.rainbow,
|
||||||
|
checkForUpdates: json['checkForUpdates'] as bool? ?? true,
|
||||||
|
lastViewedUpdate: json['lastViewedUpdate'] as String?,
|
||||||
libraryPageSize: (json['libraryPageSize'] as num?)?.toInt(),
|
libraryPageSize: (json['libraryPageSize'] as num?)?.toInt(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -66,6 +68,8 @@ Map<String, dynamic> _$$ClientSettingsModelImplToJson(
|
||||||
'showAllCollectionTypes': instance.showAllCollectionTypes,
|
'showAllCollectionTypes': instance.showAllCollectionTypes,
|
||||||
'maxConcurrentDownloads': instance.maxConcurrentDownloads,
|
'maxConcurrentDownloads': instance.maxConcurrentDownloads,
|
||||||
'schemeVariant': _$DynamicSchemeVariantEnumMap[instance.schemeVariant]!,
|
'schemeVariant': _$DynamicSchemeVariantEnumMap[instance.schemeVariant]!,
|
||||||
|
'checkForUpdates': instance.checkForUpdates,
|
||||||
|
'lastViewedUpdate': instance.lastViewedUpdate,
|
||||||
'libraryPageSize': instance.libraryPageSize,
|
'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/crash_screen/crash_screen.dart';
|
||||||
import 'package:fladder/screens/settings/settings_scaffold.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_icon.dart';
|
||||||
import 'package:fladder/screens/shared/fladder_logo.dart';
|
import 'package:fladder/screens/shared/fladder_logo.dart';
|
||||||
import 'package:fladder/screens/shared/media/external_urls.dart';
|
import 'package:fladder/screens/shared/media/external_urls.dart';
|
||||||
|
|
@ -42,6 +43,7 @@ class AboutSettingsPage extends ConsumerWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final applicationInfo = ref.watch(applicationInfoProvider);
|
final applicationInfo = ref.watch(applicationInfoProvider);
|
||||||
|
|
||||||
return SettingsScaffold(
|
return SettingsScaffold(
|
||||||
label: "",
|
label: "",
|
||||||
items: [
|
items: [
|
||||||
|
|
@ -114,6 +116,7 @@ class AboutSettingsPage extends ConsumerWidget {
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
const SettingsUpdateInformation(),
|
||||||
].addInBetween(const SizedBox(height: 16)),
|
].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/arguments_provider.dart';
|
||||||
import 'package:fladder/providers/auth_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/providers/user_provider.dart';
|
||||||
import 'package:fladder/routes/auto_router.gr.dart';
|
import 'package:fladder/routes/auto_router.gr.dart';
|
||||||
import 'package:fladder/screens/settings/quick_connect_window.dart';
|
import 'package:fladder/screens/settings/quick_connect_window.dart';
|
||||||
|
|
@ -99,6 +100,10 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||||
final quickConnectAvailable =
|
final quickConnectAvailable =
|
||||||
ref.watch(userProvider.select((value) => value?.serverConfiguration?.quickConnectAvailable ?? false));
|
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(
|
return Padding(
|
||||||
padding: EdgeInsets.only(left: AdaptiveLayout.of(context).sideBarWidth),
|
padding: EdgeInsets.only(left: AdaptiveLayout.of(context).sideBarWidth),
|
||||||
child: Container(
|
child: Container(
|
||||||
|
|
@ -109,6 +114,18 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||||
showBackButtonNested: true,
|
showBackButtonNested: true,
|
||||||
showUserIcon: true,
|
showUserIcon: true,
|
||||||
items: [
|
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(
|
SettingsListTile(
|
||||||
label: Text(context.localized.settingsClientTitle),
|
label: Text(context.localized.settingsClientTitle),
|
||||||
subLabel: Text(context.localized.settingsClientDesc),
|
subLabel: Text(context.localized.settingsClientDesc),
|
||||||
|
|
@ -138,7 +155,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||||
),
|
),
|
||||||
SettingsListTile(
|
SettingsListTile(
|
||||||
label: Text(context.localized.about),
|
label: Text(context.localized.about),
|
||||||
subLabel: const Text("Fladder"),
|
subLabel: Text("Fladder, ${context.localized.latestReleases}"),
|
||||||
selected: containsRoute(const AboutSettingsRoute()),
|
selected: containsRoute(const AboutSettingsRoute()),
|
||||||
leading: Opacity(
|
leading: Opacity(
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||||
|
|
||||||
import 'package:fladder/util/list_padding.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:auto_route/auto_route.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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/providers/user_provider.dart';
|
||||||
import 'package:fladder/routes/auto_router.gr.dart';
|
import 'package:fladder/routes/auto_router.gr.dart';
|
||||||
import 'package:fladder/screens/shared/flat_button.dart';
|
import 'package:fladder/screens/shared/flat_button.dart';
|
||||||
import 'package:fladder/screens/shared/user_icon.dart';
|
import 'package:fladder/screens/shared/user_icon.dart';
|
||||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||||
import 'package:fladder/util/localization_helper.dart';
|
import 'package:fladder/util/localization_helper.dart';
|
||||||
|
import 'package:fladder/util/theme_extensions.dart';
|
||||||
|
|
||||||
class SettingsUserIcon extends ConsumerWidget {
|
class SettingsUserIcon extends ConsumerWidget {
|
||||||
const SettingsUserIcon({super.key});
|
const SettingsUserIcon({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final size = 24.0;
|
||||||
final user = ref.watch(userProvider);
|
final user = ref.watch(userProvider);
|
||||||
|
final hasNewUpdate = ref.watch(hasNewUpdateProvider);
|
||||||
return Tooltip(
|
return Tooltip(
|
||||||
message: context.localized.settings,
|
message: context.localized.settings,
|
||||||
waitDuration: const Duration(seconds: 1),
|
waitDuration: const Duration(seconds: 1),
|
||||||
|
|
@ -28,9 +33,35 @@ class SettingsUserIcon extends ConsumerWidget {
|
||||||
context.router.push(const ClientSettingsRoute());
|
context.router.push(const ClientSettingsRoute());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: UserIcon(
|
child: Stack(
|
||||||
user: user,
|
alignment: Alignment.bottomRight,
|
||||||
cornerRadius: 200,
|
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),
|
selectedIcon: const Icon(IconsaxPlusBold.setting_3),
|
||||||
horizontal: true,
|
horizontal: true,
|
||||||
expanded: shouldExpand,
|
expanded: shouldExpand,
|
||||||
icon: const SizedBox(height: 32, child: SettingsUserIcon()),
|
icon: const SettingsUserIcon(),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.single) {
|
if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.single) {
|
||||||
context.router.push(const SettingsRoute());
|
context.router.push(const SettingsRoute());
|
||||||
|
|
|
||||||
40
pubspec.lock
40
pubspec.lock
|
|
@ -595,6 +595,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
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:
|
flutter_keyboard_visibility:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -842,6 +850,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.2"
|
version: "2.3.2"
|
||||||
|
highlight:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: highlight
|
||||||
|
sha256: "5353a83ffe3e3eca7df0abfb72dcf3fa66cc56b953728e7113ad4ad88497cf21"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.0"
|
||||||
html:
|
html:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -1082,6 +1098,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.3.0"
|
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:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -1570,6 +1594,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.0"
|
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:
|
share_plus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -2055,6 +2087,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.4"
|
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:
|
vm_service:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,7 @@ dependencies:
|
||||||
reorderable_grid: ^1.0.10
|
reorderable_grid: ^1.0.10
|
||||||
overflow_view: ^0.4.0
|
overflow_view: ^0.4.0
|
||||||
flutter_sticky_header: ^0.7.0
|
flutter_sticky_header: ^0.7.0
|
||||||
|
markdown_widget: ^2.3.2+8
|
||||||
|
|
||||||
# Navigation
|
# Navigation
|
||||||
auto_route: ^9.3.0+1
|
auto_route: ^9.3.0+1
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue