mirror of
https://github.com/DJDoubleD/refreezer.git
synced 2026-01-15 16:32:54 -03:00
Enable automatic update checks, download and install
- Enable update check with notification - Enable update screen with releasenotes formatted from github markdown - Update dependencies
This commit is contained in:
@@ -441,5 +441,8 @@ const language_en_us = {
|
||||
'Open system settings': 'Open system settings',
|
||||
'Application Log': 'Application Log',
|
||||
'Are you sure you want to log out?': 'Are you sure you want to log out?',
|
||||
|
||||
// 0.7.13
|
||||
'Download failed!': 'Download failed!',
|
||||
}
|
||||
};
|
||||
|
||||
@@ -27,7 +27,7 @@ import 'ui/home_screen.dart';
|
||||
import 'ui/library.dart';
|
||||
import 'ui/login_screen.dart';
|
||||
import 'ui/player_bar.dart';
|
||||
//import 'ui/updater.dart';
|
||||
import 'ui/updater.dart';
|
||||
import 'ui/search.dart';
|
||||
import 'utils/logging.dart';
|
||||
import 'utils/navigator_keys.dart';
|
||||
@@ -243,11 +243,9 @@ class _MainScreenState extends State<MainScreen>
|
||||
_prepareQuickActions();
|
||||
|
||||
//Check for updates on background
|
||||
/* No automatic updates yet
|
||||
Future.delayed(Duration(seconds: 5), () {
|
||||
FreezerVersions.checkUpdate();
|
||||
Future.delayed(const Duration(seconds: 5), () {
|
||||
ReFreezerLatest.checkUpdate();
|
||||
});
|
||||
*/
|
||||
|
||||
//Restore saved queue
|
||||
_loadSavedQueue();
|
||||
|
||||
@@ -143,7 +143,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
leading: const LeadingIcon(Icons.update, color: Color(0xff2ba766)),
|
||||
onTap: () => Navigator.push(context,
|
||||
MaterialPageRoute(builder: (context) => const UpdaterScreen())),
|
||||
enabled: false,
|
||||
),
|
||||
ListTile(
|
||||
title: Text('About'.i18n),
|
||||
|
||||
@@ -2,10 +2,13 @@ import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:open_filex/open_filex.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
@@ -26,65 +29,113 @@ class UpdaterScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _UpdaterScreenState extends State<UpdaterScreen> {
|
||||
static const MethodChannel _platform = MethodChannel('r.r.refreezer/native');
|
||||
bool _loading = true;
|
||||
bool _error = false;
|
||||
FreezerVersions? _versions;
|
||||
String? _current;
|
||||
ReFreezerLatest? _latestRelease;
|
||||
Version _currentVersion = Version.parse('0.0.0');
|
||||
String? _arch;
|
||||
double _progress = 0.0;
|
||||
bool _buttonEnabled = true;
|
||||
|
||||
static Future<bool> _checkInstallPackagesPermission() async {
|
||||
try {
|
||||
return await _platform.invokeMethod('checkInstallPackagesPermission');
|
||||
} catch (e) {
|
||||
Logger.root.severe('Failed to check install packages permission', e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _requestInstallPackagesPermission() async {
|
||||
try {
|
||||
await _platform.invokeMethod('requestInstallPackagesPermission');
|
||||
} catch (e) {
|
||||
Logger.root.severe('Failed to request install packages permission', e);
|
||||
}
|
||||
}
|
||||
|
||||
Future _load() async {
|
||||
//Load current version
|
||||
// Load current version (convert DEBUG mode to SemVer)
|
||||
PackageInfo info = await PackageInfo.fromPlatform();
|
||||
setState(() => _current = info.version);
|
||||
String versionString = info.version.replaceFirst(' DEBUG', '-debug');
|
||||
|
||||
// Parse the version string
|
||||
setState(() {
|
||||
_currentVersion =
|
||||
Version.tryParse(versionString) ?? Version.parse('0.0.0');
|
||||
});
|
||||
|
||||
//Get architecture
|
||||
_arch = await DownloadManager.platform.invokeMethod('arch');
|
||||
if (_arch == 'armv8l') _arch = 'arm32';
|
||||
|
||||
//Load from website
|
||||
try {
|
||||
FreezerVersions versions = await FreezerVersions.fetch();
|
||||
ReFreezerLatest latestRelease = await ReFreezerLatest.fetch();
|
||||
setState(() {
|
||||
_versions = versions;
|
||||
_latestRelease = latestRelease;
|
||||
_loading = false;
|
||||
});
|
||||
} catch (e, st) {
|
||||
if (kDebugMode) {
|
||||
print(e.toString() + st.toString());
|
||||
}
|
||||
Logger.root.severe('Failed to load latest release', e, st);
|
||||
_error = true;
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
FreezerDownload? get _versionDownload {
|
||||
return _versions?.versions[0].downloads.firstWhere((d) => d.version.toLowerCase().contains(_arch!.toLowerCase()));
|
||||
ReFreezerDownload? get _versionDownload {
|
||||
return _latestRelease?.downloads.firstWhereOrNull(
|
||||
(d) => d.architectures
|
||||
.any((arch) => arch.toLowerCase() == _arch?.toLowerCase()),
|
||||
);
|
||||
}
|
||||
|
||||
Future _download() async {
|
||||
String? url = _versionDownload?.directUrl;
|
||||
//Start request
|
||||
http.Client client = http.Client();
|
||||
http.StreamedResponse res = await client.send(http.Request('GET', Uri.parse(url ?? '')));
|
||||
int? size = res.contentLength;
|
||||
//Open file
|
||||
String path = p.join((await getExternalStorageDirectory())!.path, 'update.apk');
|
||||
File file = File(path);
|
||||
IOSink fileSink = file.openWrite();
|
||||
//Update progress
|
||||
Future.doWhile(() async {
|
||||
int received = await file.length();
|
||||
setState(() => _progress = received / size!.toInt());
|
||||
return received != size;
|
||||
});
|
||||
//Pipe
|
||||
await res.stream.pipe(fileSink);
|
||||
fileSink.close();
|
||||
if (!await _checkInstallPackagesPermission()) {
|
||||
await _requestInstallPackagesPermission();
|
||||
}
|
||||
|
||||
OpenFilex.open(path);
|
||||
setState(() => _buttonEnabled = true);
|
||||
try {
|
||||
String? url = _versionDownload?.directUrl;
|
||||
if (url == null) {
|
||||
throw Exception('No compatible download available');
|
||||
}
|
||||
//Start request
|
||||
http.Client client = http.Client();
|
||||
http.StreamedResponse res =
|
||||
await client.send(http.Request('GET', Uri.parse(url)));
|
||||
int? size = res.contentLength;
|
||||
//Open file
|
||||
String path =
|
||||
p.join((await getExternalStorageDirectory())!.path, 'update.apk');
|
||||
File file = File(path);
|
||||
IOSink fileSink = file.openWrite();
|
||||
//Update progress
|
||||
Future.doWhile(() async {
|
||||
int received = await file.length();
|
||||
setState(() => _progress = received / size!.toInt());
|
||||
return received != size;
|
||||
});
|
||||
//Pipe
|
||||
await res.stream.pipe(fileSink);
|
||||
fileSink.close();
|
||||
|
||||
OpenFilex.open(path);
|
||||
setState(() {
|
||||
_buttonEnabled = true;
|
||||
_progress = 0.0;
|
||||
});
|
||||
} catch (e) {
|
||||
Logger.root.severe('Failed to download latest release file', e);
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Download failed!'.i18n,
|
||||
toastLength: Toast.LENGTH_LONG,
|
||||
gravity: ToastGravity.BOTTOM);
|
||||
setState(() {
|
||||
_progress = 0.0;
|
||||
_buttonEnabled = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -110,42 +161,60 @@ class _UpdaterScreenState extends State<UpdaterScreen> {
|
||||
),
|
||||
if (!_error &&
|
||||
!_loading &&
|
||||
Version.parse((_versions?.latest.toString() ?? '0.0.0')) <= Version.parse(_current!))
|
||||
(_latestRelease?.version ?? Version(0, 0, 0)) <=
|
||||
_currentVersion)
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text('You are running latest version!'.i18n,
|
||||
textAlign: TextAlign.center, style: const TextStyle(fontSize: 26.0)),
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 26.0)),
|
||||
)),
|
||||
if (!_error &&
|
||||
!_loading &&
|
||||
Version.parse((_versions?.latest.toString() ?? '0.0.0')) > Version.parse(_current!))
|
||||
(_latestRelease?.version ?? Version(0, 0, 0)) > _currentVersion)
|
||||
Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'New update available!'.i18n + ' ' + _versions!.latest.toString(),
|
||||
'New update available!'.i18n +
|
||||
' ' +
|
||||
_latestRelease!.version.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold),
|
||||
style: const TextStyle(
|
||||
fontSize: 20.0, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Current version: ' + _current!,
|
||||
style: const TextStyle(fontSize: 14.0, fontStyle: FontStyle.italic),
|
||||
'Current version: ' + _currentVersion.toString(),
|
||||
style: const TextStyle(
|
||||
fontSize: 14.0, fontStyle: FontStyle.italic),
|
||||
),
|
||||
Container(height: 8.0),
|
||||
const FreezerDivider(),
|
||||
Container(height: 8.0),
|
||||
const Text(
|
||||
'Changelog',
|
||||
style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold),
|
||||
style:
|
||||
TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold),
|
||||
),
|
||||
Container(height: 8.0),
|
||||
const FreezerDivider(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 4, 16, 8),
|
||||
child: Text(
|
||||
_versions!.versions[0].changelog,
|
||||
/*child: Text(
|
||||
_latestRelease?.changelog ?? '',
|
||||
style: const TextStyle(fontSize: 16.0),
|
||||
),*/
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: 350 // Screen Title height, ...
|
||||
),
|
||||
child: Markdown(
|
||||
data: _latestRelease?.changelog ?? '',
|
||||
shrinkWrap: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
const FreezerDivider(),
|
||||
@@ -154,16 +223,23 @@ class _UpdaterScreenState extends State<UpdaterScreen> {
|
||||
if (_versionDownload != null)
|
||||
Column(children: [
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
),
|
||||
onPressed: _buttonEnabled
|
||||
? () {
|
||||
setState(() => _buttonEnabled = false);
|
||||
_download();
|
||||
}
|
||||
: null,
|
||||
child: Text('Download'.i18n + ' (${_versionDownload?.version})')),
|
||||
child: Text(
|
||||
'Download'.i18n + ' (${_versionDownload?.abi})')),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: LinearProgressIndicator(value: _progress),
|
||||
child: LinearProgressIndicator(
|
||||
value: _progress,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
)
|
||||
]),
|
||||
//Unsupported arch
|
||||
@@ -180,79 +256,130 @@ class _UpdaterScreenState extends State<UpdaterScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
class FreezerVersions {
|
||||
String latest;
|
||||
List<FreezerVersion> versions;
|
||||
class ReFreezerLatest {
|
||||
final String versionString;
|
||||
final Version version;
|
||||
final String changelog;
|
||||
final List<ReFreezerDownload> downloads;
|
||||
|
||||
FreezerVersions({required this.latest, required this.versions});
|
||||
static const Map<String, List<String>> abiMap = {
|
||||
'arm64-v8a': ['arm64', 'aarch64'],
|
||||
'armeabi-v7a': ['arm32', 'armhf', 'armv8l'],
|
||||
'x86_64': ['x86_64'],
|
||||
};
|
||||
|
||||
factory FreezerVersions.fromJson(Map data) => FreezerVersions(
|
||||
latest: data['android']['latest'],
|
||||
versions: data['android']['versions'].map<FreezerVersion>((v) => FreezerVersion.fromJson(v)).toList());
|
||||
ReFreezerLatest({
|
||||
required this.versionString,
|
||||
required this.changelog,
|
||||
required this.downloads,
|
||||
}) : version = Version.tryParse(versionString) ?? Version.parse('0.0.0');
|
||||
|
||||
//Fetch from website API
|
||||
static Future<FreezerVersions> fetch() async {
|
||||
http.Response response = await http.get('https://freezer.life/api/versions' as Uri);
|
||||
// http.Response response = await http.get('https://cum.freezerapp.workers.dev/api/versions');
|
||||
return FreezerVersions.fromJson(jsonDecode(response.body));
|
||||
static Future<ReFreezerLatest> fetch() async {
|
||||
http.Response res = await http.get(
|
||||
Uri.parse(
|
||||
'https://api.github.com/repos/DJDoubleD/refreezer/releases/latest'),
|
||||
headers: {'Accept': 'application/vnd.github.v3+json'},
|
||||
);
|
||||
|
||||
if (res.statusCode != 200) {
|
||||
throw Exception(
|
||||
'Failed to load latest version from Github API: $res.statusCode $res.statusMessage');
|
||||
}
|
||||
|
||||
Map<String, dynamic> data = jsonDecode(res.body);
|
||||
|
||||
List<ReFreezerDownload> downloads = (data['assets'] as List)
|
||||
.map((asset) {
|
||||
String abi = abiMap.keys.firstWhere(
|
||||
(key) => asset['name'].contains(key),
|
||||
orElse: () => 'unknown',
|
||||
);
|
||||
return ReFreezerDownload(
|
||||
abi: abi,
|
||||
directUrl: asset['browser_download_url'],
|
||||
);
|
||||
})
|
||||
.where((download) => download.abi != 'unknown')
|
||||
.toList();
|
||||
|
||||
return ReFreezerLatest(
|
||||
versionString: data['tag_name'],
|
||||
changelog: data['body'],
|
||||
downloads: downloads,
|
||||
);
|
||||
}
|
||||
|
||||
static Future checkUpdate() async {
|
||||
//Check only each 24h
|
||||
int updateDelay = 86400000;
|
||||
if ((DateTime.now().millisecondsSinceEpoch - (cache.lastUpdateCheck ?? 0)) < updateDelay) return;
|
||||
cache.lastUpdateCheck = DateTime.now().millisecondsSinceEpoch;
|
||||
static Future<void> checkUpdate() async {
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
|
||||
// Check every 24 hours
|
||||
if (now - (cache.lastUpdateCheck ?? 0) <=
|
||||
const Duration(hours: 24).inMilliseconds) {
|
||||
return;
|
||||
}
|
||||
cache.lastUpdateCheck = now;
|
||||
await cache.save();
|
||||
|
||||
FreezerVersions versions = await FreezerVersions.fetch();
|
||||
try {
|
||||
final latestVersion = await fetch();
|
||||
|
||||
//Load current version
|
||||
PackageInfo info = await PackageInfo.fromPlatform();
|
||||
if (Version.parse(versions.latest) <= Version.parse(info.version)) return;
|
||||
//Load current version
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
final currentVersion =
|
||||
Version.tryParse(packageInfo.version) ?? Version.parse('0.0.0');
|
||||
|
||||
//Get architecture
|
||||
String arch = await DownloadManager.platform.invokeMethod('arch');
|
||||
if (arch == 'armv8l') arch = 'arm32';
|
||||
//Check compatible architecture
|
||||
var compatibleVersion =
|
||||
versions.versions[0].downloads.firstWhereOrNull((d) => d.version.toLowerCase().contains(arch.toLowerCase()));
|
||||
if (compatibleVersion == null) return;
|
||||
if (latestVersion.version <= currentVersion) return;
|
||||
|
||||
//Show notification
|
||||
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
|
||||
const AndroidInitializationSettings androidInitializationSettings =
|
||||
AndroidInitializationSettings('drawable/ic_logo');
|
||||
const InitializationSettings initializationSettings =
|
||||
InitializationSettings(android: androidInitializationSettings);
|
||||
await flutterLocalNotificationsPlugin.initialize(initializationSettings);
|
||||
AndroidNotificationDetails androidNotificationDetails = AndroidNotificationDetails(
|
||||
'freezerupdates', 'Freezer Updates'.i18n,
|
||||
channelDescription: 'Freezer Updates'.i18n);
|
||||
NotificationDetails notificationDetails = NotificationDetails(android: androidNotificationDetails);
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
0, 'New update available!'.i18n, 'Update to latest version in the settings.'.i18n, notificationDetails);
|
||||
//Get architecture
|
||||
String arch = await DownloadManager.platform.invokeMethod('arch');
|
||||
Logger.root
|
||||
.info('Checking for updates to version $currentVersion on $arch');
|
||||
|
||||
if (!latestVersion.downloads.any((download) => download.architectures.any(
|
||||
(architecture) =>
|
||||
architecture.toLowerCase() == arch.toLowerCase()))) {
|
||||
Logger.root.warning('No assets found for architecture $arch');
|
||||
return;
|
||||
}
|
||||
|
||||
//Show notification
|
||||
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
const AndroidInitializationSettings androidInitializationSettings =
|
||||
AndroidInitializationSettings('drawable/ic_logo');
|
||||
const InitializationSettings initializationSettings =
|
||||
InitializationSettings(
|
||||
android: androidInitializationSettings, iOS: null);
|
||||
await flutterLocalNotificationsPlugin.initialize(initializationSettings);
|
||||
|
||||
AndroidNotificationDetails androidNotificationDetails =
|
||||
AndroidNotificationDetails(
|
||||
'freezerupdates',
|
||||
'Freezer Updates'.i18n,
|
||||
channelDescription: 'Freezer Updates'.i18n,
|
||||
importance: Importance.high,
|
||||
priority: Priority.high,
|
||||
);
|
||||
|
||||
NotificationDetails notificationDetails =
|
||||
NotificationDetails(android: androidNotificationDetails, iOS: null);
|
||||
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
0,
|
||||
'New update available!'.i18n,
|
||||
'Update to latest version in the settings.'.i18n,
|
||||
notificationDetails);
|
||||
} catch (e) {
|
||||
Logger.root.severe('Error checking for updates', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FreezerVersion {
|
||||
String version;
|
||||
String changelog;
|
||||
List<FreezerDownload> downloads;
|
||||
class ReFreezerDownload {
|
||||
final String abi;
|
||||
final String directUrl;
|
||||
final List<String> architectures;
|
||||
|
||||
FreezerVersion({required this.version, required this.changelog, required this.downloads});
|
||||
|
||||
factory FreezerVersion.fromJson(Map data) => FreezerVersion(
|
||||
version: data['version'],
|
||||
changelog: data['changelog'],
|
||||
downloads: data['downloads'].map<FreezerDownload>((d) => FreezerDownload.fromJson(d)).toList());
|
||||
}
|
||||
|
||||
class FreezerDownload {
|
||||
String version;
|
||||
String directUrl;
|
||||
|
||||
FreezerDownload({required this.version, required this.directUrl});
|
||||
|
||||
factory FreezerDownload.fromJson(Map data) =>
|
||||
FreezerDownload(version: data['version'], directUrl: data['links'].first['url']);
|
||||
ReFreezerDownload({required this.abi, required this.directUrl})
|
||||
: architectures = ReFreezerLatest.abiMap[abi] ?? [abi];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user