Prepare Lunaris 1.1.2 experimental

This commit is contained in:
2026-05-26 11:43:25 +03:00
parent 369dee02be
commit 9cff9e5307
42 changed files with 2041 additions and 303 deletions
@@ -29,6 +29,25 @@ class DownloadManagerService {
return task;
}
Future<DownloadTask> startUrl({
required String url,
required String fileName,
bool openAfterDownload = false,
}) async {
final id = 'url:$url:${DateTime.now().microsecondsSinceEpoch}';
final task = DownloadTask(
id: id,
sourceUrl: url,
fileName: fileName,
progress: 0,
status: DownloadTaskStatus.queued,
openAfterDownload: openAfterDownload,
);
_set(task);
unawaited(_run(task));
return task;
}
Future<void> retry(String taskId) async {
final existing = _tasks[taskId];
if (existing == null) return;
@@ -51,18 +70,22 @@ class DownloadManagerService {
if (_tasks[task.id]?.status == DownloadTaskStatus.canceled) return;
_set(task.copyWith(status: DownloadTaskStatus.running));
try {
final saved = await _downloadService.downloadPost(
task.post,
onProgress: (received, total) {
if (total <= 0) return;
final current = _tasks[task.id];
if (current == null ||
current.status == DownloadTaskStatus.canceled) {
return;
}
_set(current.copyWith(progress: received / total));
},
);
final saved = task.sourceUrl == null
? await _downloadService.downloadPost(
task.post!,
onProgress: (received, total) {
_updateProgress(task.id, received, total);
},
)
: await _downloadService.downloadUrl(
task.sourceUrl!,
fileName: task.fileName,
mimeType: _mimeType(task.fileName),
openAfterDownload: task.openAfterDownload,
onProgress: (received, total) {
_updateProgress(task.id, received, total);
},
);
final current = _tasks[task.id];
if (current == null || current.status == DownloadTaskStatus.canceled) {
return;
@@ -94,6 +117,15 @@ class DownloadManagerService {
_controller.add(tasks);
}
void _updateProgress(String taskId, int received, int total) {
if (total <= 0) return;
final current = _tasks[taskId];
if (current == null || current.status == DownloadTaskStatus.canceled) {
return;
}
_set(current.copyWith(progress: received / total));
}
void _scheduleAutoRemove(String taskId, Duration delay) {
Future<void>.delayed(delay, () {
final current = _tasks[taskId];
@@ -111,4 +143,18 @@ class DownloadManagerService {
if (last.contains('.')) return last;
return '${post.providerId}_${post.id}';
}
String _mimeType(String fileName) {
final lower = fileName.toLowerCase();
if (lower.endsWith('.apk')) {
return 'application/vnd.android.package-archive';
}
if (lower.endsWith('.exe')) {
return 'application/vnd.microsoft.portable-executable';
}
if (lower.endsWith('.zip')) {
return 'application/zip';
}
return 'application/octet-stream';
}
}
@@ -43,6 +43,54 @@ class DownloadService {
return location.path;
}
Future<String?> downloadUrl(
String url, {
required String fileName,
String mimeType = 'application/octet-stream',
bool openAfterDownload = false,
void Function(int received, int total)? onProgress,
}) async {
if (Platform.isAndroid) {
final tempDir = await getTemporaryDirectory();
final tempPath = p.join(tempDir.path, fileName);
await _dio.download(url, tempPath, onReceiveProgress: onProgress);
if (openAfterDownload) {
await openFile(tempPath, mimeType: mimeType);
return tempPath;
}
final saved = await _channel.invokeMethod<String>('saveToDownloads', {
'path': tempPath,
'fileName': fileName,
'mimeType': mimeType,
});
return saved ?? fileName;
}
final downloads =
await getDownloadsDirectory() ?? await getTemporaryDirectory();
final path = p.join(downloads.path, fileName);
await _dio.download(url, path, onReceiveProgress: onProgress);
if (openAfterDownload) await openFile(path, mimeType: mimeType);
return path;
}
Future<void> openFile(String path, {String? mimeType}) async {
if (Platform.isAndroid) {
await _channel.invokeMethod<void>('openFile', {
'path': path,
'mimeType': mimeType ?? _mimeType(path, ''),
});
return;
}
if (Platform.isWindows) {
await Process.start('explorer.exe', [path]);
return;
}
if (Platform.isLinux) {
await Process.start('xdg-open', [path]);
}
}
String? _downloadUrl(Post post) {
return MediaUrlSelector.download(post);
}
+24
View File
@@ -1,4 +1,5 @@
import 'package:dio/dio.dart';
import 'dart:io';
import '../../app/app_version.dart';
import '../../core/errors/failure.dart';
@@ -71,12 +72,14 @@ class UpdateService {
final assets = (json['assets'] as List?) ?? const [];
String? apkUrl;
String? installerUrl;
String? portableUrl;
for (final item in assets.whereType<Map>()) {
final name = (item['name'] ?? '').toString().toLowerCase();
final url = (item['browser_download_url'] ?? '').toString();
if (url.isEmpty) continue;
if (name.endsWith('.apk')) apkUrl = url;
if (name.endsWith('.exe')) installerUrl = url;
if (name.endsWith('.zip')) portableUrl = url;
}
return AppUpdateInfo(
version: _versionFromTag(tag),
@@ -87,9 +90,30 @@ class UpdateService {
publishedAt: DateTime.tryParse((json['published_at'] ?? '').toString()),
apkUrl: apkUrl,
windowsInstallerUrl: installerUrl,
portableZipUrl: portableUrl,
);
}
String? assetUrlForCurrentPlatform(AppUpdateInfo info) {
if (Platform.isAndroid) {
return info.apkUrl;
}
if (Platform.isWindows) {
return info.windowsInstallerUrl ?? info.portableZipUrl;
}
return info.portableZipUrl;
}
String assetFileName(AppUpdateInfo info, String url) {
final parsed = Uri.tryParse(url);
final fromUrl =
parsed?.pathSegments.isEmpty ?? true ? '' : parsed!.pathSegments.last;
if (fromUrl.contains('.')) return fromUrl;
if (Platform.isAndroid) return 'Lunaris-v${info.version}.apk';
if (Platform.isWindows) return 'LunarisSetup-v${info.version}.exe';
return 'LunarisPortable-v${info.version}.zip';
}
static String _versionFromTag(String tag) {
final match = RegExp(r'v?(\d+\.\d+\.\d+)').firstMatch(tag);
return match?.group(1) ?? tag.replaceFirst(RegExp(r'^v'), '');