mirror of
https://github.com/gokadzev/Musify.git
synced 2026-03-13 15:20:46 +08:00
310 lines
10 KiB
Dart
310 lines
10 KiB
Dart
/*
|
||
* 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 F‑Droid
|
||
/// 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);
|
||
}
|
||
}
|