Files
Musify/lib/services/update_manager.dart
2026-01-02 00:17:56 +04:00

310 lines
10 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* Copyright (C) 2026 Valeri Gokadze
*
* Musify is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Musify is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*
* For more information about Musify, including how to contribute,
* please visit: https://github.com/gokadzev/Musify
*/
import 'dart:convert';
import 'dart:io';
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:musify/API/version.dart';
import 'package:musify/extensions/l10n.dart';
import 'package:musify/main.dart';
import 'package:musify/services/data_manager.dart';
import 'package:musify/services/router_service.dart';
import 'package:musify/services/settings_manager.dart';
import 'package:musify/utilities/url_launcher.dart';
import 'package:musify/widgets/auto_format_text.dart';
const String checkUrl =
'https://raw.githubusercontent.com/gokadzev/Musify/update/check.json';
const String releasesUrl =
'https://api.github.com/repos/gokadzev/Musify/releases/latest';
const String downloadUrlKey = 'url';
const String downloadUrlArm64Key = 'arm64url';
const String downloadFilename = 'Musify.apk';
Future<void> checkAppUpdates() async {
try {
final response = await http.get(Uri.parse(checkUrl));
if (response.statusCode != 200) {
logger.log(
'Fetch update API (checkUrl) call returned status code ${response.statusCode}',
null,
null,
);
return;
}
final map = json.decode(response.body) as Map<String, dynamic>;
announcementURL.value = map['announcementurl'];
final latestVersion = map['version'].toString();
if (!isLatestVersionHigher(appVersion, latestVersion)) {
return;
}
final releasesRequest = await http.get(Uri.parse(releasesUrl));
if (releasesRequest.statusCode != 200) {
logger.log(
'Fetch update API (releasesUrl) call returned status code ${response.statusCode}',
null,
null,
);
return;
}
final releasesResponse =
json.decode(releasesRequest.body) as Map<String, dynamic>;
await showDialog(
context: NavigationManager().context,
builder: (BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return AlertDialog(
backgroundColor: colorScheme.surface,
surfaceTintColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(28),
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: colorScheme.primaryContainer,
shape: BoxShape.circle,
),
child: Icon(
FluentIcons.arrow_download_24_filled,
color: colorScheme.onPrimaryContainer,
size: 32,
),
),
const SizedBox(height: 16),
Text(
context.l10n!.appUpdateIsAvailable,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(20),
),
child: Text(
'V$latestVersion',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: colorScheme.onPrimaryContainer,
),
),
),
const SizedBox(height: 16),
Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.sizeOf(context).height / 2.5,
),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(16),
),
child: SingleChildScrollView(
child: AutoFormatText(text: releasesResponse['body']),
),
),
],
),
actionsAlignment: MainAxisAlignment.center,
actions: <Widget>[
OutlinedButton(
onPressed: () => Navigator.pop(context),
style: OutlinedButton.styleFrom(
side: BorderSide(color: colorScheme.outline),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(context.l10n!.cancel),
),
FilledButton.icon(
onPressed: () {
getDownloadUrl(map).then(
(url) => {launchURL(Uri.parse(url)), Navigator.pop(context)},
);
},
icon: const Icon(FluentIcons.arrow_download_20_filled),
label: Text(context.l10n!.download),
),
],
);
},
);
} catch (e, stackTrace) {
logger.log('Error in checkAppUpdates', e, stackTrace);
}
}
void showUpdateCheckDialog(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
showDialog(
context: context,
builder: (context) {
return AlertDialog(
backgroundColor: colorScheme.surface,
surfaceTintColor: Colors.transparent,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)),
icon: Icon(
FluentIcons.arrow_sync_circle_24_regular,
color: colorScheme.primary,
size: 40,
),
title: Text(
context.l10n!.checkForUpdates,
style: TextStyle(
color: colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
content: Text(
context.l10n!.enableUpdateChecksDescription,
style: TextStyle(color: colorScheme.onSurfaceVariant),
textAlign: TextAlign.center,
),
actionsAlignment: MainAxisAlignment.center,
actions: [
OutlinedButton(
onPressed: () {
shouldWeCheckUpdates.value = false;
addOrUpdateData('settings', 'shouldWeCheckUpdates', false);
Navigator.of(context).pop();
},
style: OutlinedButton.styleFrom(
side: BorderSide(color: colorScheme.outline),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(context.l10n!.no),
),
FilledButton(
onPressed: () {
shouldWeCheckUpdates.value = true;
addOrUpdateData('settings', 'shouldWeCheckUpdates', true);
if (!isFdroidBuild && kReleaseMode && !offlineMode.value) {
checkAppUpdates();
isUpdateChecked = true;
}
Navigator.of(context).pop();
},
child: Text(context.l10n!.yes),
),
],
);
},
);
}
bool isLatestVersionHigher(String appVersion, String latestVersion) {
final parsedAppVersion = appVersion.split('.');
final parsedAppLatestVersion = latestVersion.split('.');
final length = parsedAppVersion.length > parsedAppLatestVersion.length
? parsedAppVersion.length
: parsedAppLatestVersion.length;
for (var i = 0; i < length; i++) {
final value1 = i < parsedAppVersion.length
? int.parse(parsedAppVersion[i])
: 0;
final value2 = i < parsedAppLatestVersion.length
? int.parse(parsedAppLatestVersion[i])
: 0;
if (value2 > value1) {
return true;
} else if (value2 < value1) {
return false;
}
}
return false;
}
Future<String> getCPUArchitecture() async {
final info = await Process.run('uname', ['-m']);
final cpu = info.stdout.toString().replaceAll('\n', '');
return cpu;
}
Future<String> getDownloadUrl(Map<String, dynamic> map) async {
final cpuArchitecture = await getCPUArchitecture();
final url = cpuArchitecture == 'aarch64'
? map[downloadUrlArm64Key].toString()
: map[downloadUrlKey].toString();
return url;
}
/// Fetch only the announcement URL from the `check.json` file and set the
/// global `announcementURL` ValueNotifier. This does not trigger releases
/// fetching or any update dialogs/downloads and is safe to call for FDroid
/// builds where update prompts are not allowed.
Future<void> fetchAnnouncementOnly() async {
try {
final response = await http.get(Uri.parse(checkUrl));
if (response.statusCode != 200) {
logger.log(
'Fetch announcement (checkUrl) call returned status code ${response.statusCode}',
null,
null,
);
return;
}
final map = json.decode(response.body) as Map<String, dynamic>;
final ann = map['announcementurl'];
if (ann != null) {
announcementURL.value = ann.toString();
}
} catch (e, stackTrace) {
logger.log('Error in fetchAnnouncementOnly', e, stackTrace);
}
}