feature: Added update notification and download links per platform (#362)

Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
PartyDonut 2025-06-01 21:36:50 +02:00 committed by GitHub
parent 5ef7936c33
commit 2c71dde228
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 712 additions and 8 deletions

View file

@ -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)),
);
}

View file

@ -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,

View file

@ -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';

View 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)),
);
}
}