✨ Proper windows update
This commit is contained in:
@@ -233,8 +233,208 @@ class UpdateService {
|
|||||||
return 'https://fs.solsynth.dev/d/official/solian/build-output-windows-installer.zip';
|
return 'https://fs.solsynth.dev/d/official/solian/build-output-windows-installer.zip';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Performs automatic Windows update: download, extract, and install
|
||||||
|
Future<void> performAutomaticWindowsUpdate(
|
||||||
|
BuildContext context,
|
||||||
|
String url,
|
||||||
|
) async {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (context) => _WindowsUpdateDialog(
|
||||||
|
updateUrl: url,
|
||||||
|
onComplete: () {
|
||||||
|
// Close the update sheet
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetch the latest release info from GitHub.
|
||||||
|
/// Public so other screens (e.g., About) can manually trigger update checks.
|
||||||
|
Future<GithubReleaseInfo?> fetchLatestRelease() async {
|
||||||
|
final apiEndpoint =
|
||||||
|
useProxy
|
||||||
|
? '$_proxyBaseUrl${Uri.encodeComponent(_releasesLatestApi)}'
|
||||||
|
: _releasesLatestApi;
|
||||||
|
|
||||||
|
log(
|
||||||
|
'[Update] Fetching latest release from GitHub API: $apiEndpoint (Proxy: $useProxy)',
|
||||||
|
);
|
||||||
|
final resp = await _dio.get(apiEndpoint);
|
||||||
|
if (resp.statusCode != 200) {
|
||||||
|
log(
|
||||||
|
'[Update] Failed to fetch latest release. Status code: ${resp.statusCode}',
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final data = resp.data as Map<String, dynamic>;
|
||||||
|
log('[Update] Successfully fetched release data.');
|
||||||
|
|
||||||
|
final tagName = (data['tag_name'] ?? '').toString();
|
||||||
|
final name = (data['name'] ?? tagName).toString();
|
||||||
|
final body = (data['body'] ?? '').toString();
|
||||||
|
final htmlUrl = (data['html_url'] ?? '').toString();
|
||||||
|
final createdAtStr = (data['created_at'] ?? '').toString();
|
||||||
|
final createdAt = DateTime.tryParse(createdAtStr) ?? DateTime.now();
|
||||||
|
final assetsData =
|
||||||
|
(data['assets'] as List<dynamic>?)
|
||||||
|
?.map((e) => GithubReleaseAsset.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList() ??
|
||||||
|
[];
|
||||||
|
|
||||||
|
if (tagName.isEmpty || htmlUrl.isEmpty) {
|
||||||
|
log(
|
||||||
|
'[Update] Missing tag_name or html_url in release data. TagName: "$tagName", HtmlUrl: "$htmlUrl"',
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
log('[Update] Returning GithubReleaseInfo for tag: $tagName');
|
||||||
|
return GithubReleaseInfo(
|
||||||
|
tagName: tagName,
|
||||||
|
name: name,
|
||||||
|
body: body,
|
||||||
|
htmlUrl: htmlUrl,
|
||||||
|
createdAt: createdAt,
|
||||||
|
assets: assetsData,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WindowsUpdateDialog extends StatefulWidget {
|
||||||
|
const _WindowsUpdateDialog({
|
||||||
|
required this.updateUrl,
|
||||||
|
required this.onComplete,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String updateUrl;
|
||||||
|
final VoidCallback onComplete;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_WindowsUpdateDialog> createState() => _WindowsUpdateDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WindowsUpdateDialogState extends State<_WindowsUpdateDialog> {
|
||||||
|
final ValueNotifier<double?> progressNotifier = ValueNotifier<double?>(null);
|
||||||
|
final ValueNotifier<String> messageNotifier = ValueNotifier<String>('Downloading installer...');
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_startUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _startUpdate() async {
|
||||||
|
try {
|
||||||
|
// Step 1: Download
|
||||||
|
final zipPath = await _downloadWindowsInstaller(
|
||||||
|
widget.updateUrl,
|
||||||
|
onProgress: (received, total) {
|
||||||
|
if (total == -1) {
|
||||||
|
progressNotifier.value = null;
|
||||||
|
} else {
|
||||||
|
progressNotifier.value = received / total;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (zipPath == null) {
|
||||||
|
_showError('Failed to download installer');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Extract
|
||||||
|
messageNotifier.value = 'Extracting installer...';
|
||||||
|
progressNotifier.value = null; // Indeterminate for extraction
|
||||||
|
|
||||||
|
final extractDir = await _extractWindowsInstaller(zipPath);
|
||||||
|
if (extractDir == null) {
|
||||||
|
_showError('Failed to extract installer');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Run installer
|
||||||
|
messageNotifier.value = 'Running installer...';
|
||||||
|
|
||||||
|
final success = await _runWindowsInstaller(extractDir);
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
messageNotifier.value = 'Update Complete';
|
||||||
|
progressNotifier.value = 1.0;
|
||||||
|
await Future.delayed(const Duration(seconds: 2));
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
widget.onComplete();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_showError('Failed to run installer');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
try {
|
||||||
|
await File(zipPath).delete();
|
||||||
|
await Directory(extractDir).delete(recursive: true);
|
||||||
|
} catch (e) {
|
||||||
|
log('[Update] Error cleaning up temporary files: $e');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
_showError('Update failed: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showError(String message) {
|
||||||
|
if (!mounted) return;
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('Update Failed'),
|
||||||
|
content: Text(message),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('OK'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Installing Update'),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
ValueListenableBuilder<double?>(
|
||||||
|
valueListenable: progressNotifier,
|
||||||
|
builder: (context, progress, child) {
|
||||||
|
return LinearProgressIndicator(value: progress);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ValueListenableBuilder<String>(
|
||||||
|
valueListenable: messageNotifier,
|
||||||
|
builder: (context, message, child) {
|
||||||
|
return Text(message);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Downloads the Windows installer ZIP file
|
/// Downloads the Windows installer ZIP file
|
||||||
Future<String?> _downloadWindowsInstaller(String url) async {
|
Future<String?> _downloadWindowsInstaller(
|
||||||
|
String url, {
|
||||||
|
void Function(int received, int total)? onProgress,
|
||||||
|
}) async {
|
||||||
try {
|
try {
|
||||||
log('[Update] Starting Windows installer download from: $url');
|
log('[Update] Starting Windows installer download from: $url');
|
||||||
|
|
||||||
@@ -243,7 +443,7 @@ class UpdateService {
|
|||||||
'solian-installer-${DateTime.now().millisecondsSinceEpoch}.zip';
|
'solian-installer-${DateTime.now().millisecondsSinceEpoch}.zip';
|
||||||
final filePath = path.join(tempDir.path, fileName);
|
final filePath = path.join(tempDir.path, fileName);
|
||||||
|
|
||||||
final response = await _dio.download(
|
final response = await Dio().download(
|
||||||
url,
|
url,
|
||||||
filePath,
|
filePath,
|
||||||
onReceiveProgress: (received, total) {
|
onReceiveProgress: (received, total) {
|
||||||
@@ -252,6 +452,7 @@ class UpdateService {
|
|||||||
'[Update] Download progress: ${(received / total * 100).toStringAsFixed(1)}%',
|
'[Update] Download progress: ${(received / total * 100).toStringAsFixed(1)}%',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
onProgress?.call(received, total);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -311,13 +512,20 @@ class UpdateService {
|
|||||||
try {
|
try {
|
||||||
log('[Update] Running Windows installer from: $extractDir');
|
log('[Update] Running Windows installer from: $extractDir');
|
||||||
|
|
||||||
final setupExePath = path.join(extractDir, 'setup.exe');
|
final dir = Directory(extractDir);
|
||||||
|
final exeFiles = dir
|
||||||
|
.listSync()
|
||||||
|
.where((f) => f is File && f.path.endsWith('.exe'))
|
||||||
|
.toList();
|
||||||
|
|
||||||
if (!await File(setupExePath).exists()) {
|
if (exeFiles.isEmpty) {
|
||||||
log('[Update] setup.exe not found in extracted directory');
|
log('[Update] No .exe file found in extracted directory');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final setupExePath = exeFiles.first.path;
|
||||||
|
log('[Update] Found installer executable: $setupExePath');
|
||||||
|
|
||||||
final shell = Shell();
|
final shell = Shell();
|
||||||
final results = await shell.run(setupExePath);
|
final results = await shell.run(setupExePath);
|
||||||
final result = results.first;
|
final result = results.first;
|
||||||
@@ -338,202 +546,6 @@ class UpdateService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Performs automatic Windows update: download, extract, and install
|
|
||||||
Future<void> _performAutomaticWindowsUpdate(
|
|
||||||
BuildContext context,
|
|
||||||
String url,
|
|
||||||
) async {
|
|
||||||
if (!context.mounted) return;
|
|
||||||
|
|
||||||
// Show progress dialog
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
barrierDismissible: false,
|
|
||||||
builder:
|
|
||||||
(context) => const AlertDialog(
|
|
||||||
title: Text('Installing Update'),
|
|
||||||
content: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
CircularProgressIndicator(),
|
|
||||||
SizedBox(height: 16),
|
|
||||||
Text('Downloading installer...'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Step 1: Download
|
|
||||||
if (!context.mounted) return;
|
|
||||||
Navigator.of(context).pop(); // Close progress dialog
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
barrierDismissible: false,
|
|
||||||
builder:
|
|
||||||
(context) => const AlertDialog(
|
|
||||||
title: Text('Installing Update'),
|
|
||||||
content: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
CircularProgressIndicator(),
|
|
||||||
SizedBox(height: 16),
|
|
||||||
Text('Extracting installer...'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final zipPath = await _downloadWindowsInstaller(url);
|
|
||||||
if (zipPath == null) {
|
|
||||||
if (!context.mounted) return;
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
_showErrorDialog(context, 'Failed to download installer');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2: Extract
|
|
||||||
if (!context.mounted) return;
|
|
||||||
Navigator.of(context).pop(); // Close progress dialog
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
barrierDismissible: false,
|
|
||||||
builder:
|
|
||||||
(context) => const AlertDialog(
|
|
||||||
title: Text('Installing Update'),
|
|
||||||
content: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
CircularProgressIndicator(),
|
|
||||||
SizedBox(height: 16),
|
|
||||||
Text('Running installer...'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final extractDir = await _extractWindowsInstaller(zipPath);
|
|
||||||
if (extractDir == null) {
|
|
||||||
if (!context.mounted) return;
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
_showErrorDialog(context, 'Failed to extract installer');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 3: Run installer
|
|
||||||
if (!context.mounted) return;
|
|
||||||
Navigator.of(context).pop(); // Close progress dialog
|
|
||||||
|
|
||||||
final success = await _runWindowsInstaller(extractDir);
|
|
||||||
if (!context.mounted) return;
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder:
|
|
||||||
(context) => AlertDialog(
|
|
||||||
title: const Text('Update Complete'),
|
|
||||||
content: const Text(
|
|
||||||
'The application has been updated successfully. Please restart the application.',
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
// Close the update sheet
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
child: const Text('OK'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
_showErrorDialog(context, 'Failed to run installer');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
try {
|
|
||||||
await File(zipPath).delete();
|
|
||||||
await Directory(extractDir).delete(recursive: true);
|
|
||||||
} catch (e) {
|
|
||||||
log('[Update] Error cleaning up temporary files: $e');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (!context.mounted) return;
|
|
||||||
Navigator.of(context).pop(); // Close any open dialogs
|
|
||||||
_showErrorDialog(context, 'Update failed: $e');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showErrorDialog(BuildContext context, String message) {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder:
|
|
||||||
(context) => AlertDialog(
|
|
||||||
title: const Text('Update Failed'),
|
|
||||||
content: Text(message),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
child: const Text('OK'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch the latest release info from GitHub.
|
|
||||||
/// Public so other screens (e.g., About) can manually trigger update checks.
|
|
||||||
Future<GithubReleaseInfo?> fetchLatestRelease() async {
|
|
||||||
final apiEndpoint =
|
|
||||||
useProxy
|
|
||||||
? '$_proxyBaseUrl${Uri.encodeComponent(_releasesLatestApi)}'
|
|
||||||
: _releasesLatestApi;
|
|
||||||
|
|
||||||
log(
|
|
||||||
'[Update] Fetching latest release from GitHub API: $apiEndpoint (Proxy: $useProxy)',
|
|
||||||
);
|
|
||||||
final resp = await _dio.get(apiEndpoint);
|
|
||||||
if (resp.statusCode != 200) {
|
|
||||||
log(
|
|
||||||
'[Update] Failed to fetch latest release. Status code: ${resp.statusCode}',
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final data = resp.data as Map<String, dynamic>;
|
|
||||||
log('[Update] Successfully fetched release data.');
|
|
||||||
|
|
||||||
final tagName = (data['tag_name'] ?? '').toString();
|
|
||||||
final name = (data['name'] ?? tagName).toString();
|
|
||||||
final body = (data['body'] ?? '').toString();
|
|
||||||
final htmlUrl = (data['html_url'] ?? '').toString();
|
|
||||||
final createdAtStr = (data['created_at'] ?? '').toString();
|
|
||||||
final createdAt = DateTime.tryParse(createdAtStr) ?? DateTime.now();
|
|
||||||
final assetsData =
|
|
||||||
(data['assets'] as List<dynamic>?)
|
|
||||||
?.map((e) => GithubReleaseAsset.fromJson(e as Map<String, dynamic>))
|
|
||||||
.toList() ??
|
|
||||||
[];
|
|
||||||
|
|
||||||
if (tagName.isEmpty || htmlUrl.isEmpty) {
|
|
||||||
log(
|
|
||||||
'[Update] Missing tag_name or html_url in release data. TagName: "$tagName", HtmlUrl: "$htmlUrl"',
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
log('[Update] Returning GithubReleaseInfo for tag: $tagName');
|
|
||||||
return GithubReleaseInfo(
|
|
||||||
tagName: tagName,
|
|
||||||
name: name,
|
|
||||||
body: body,
|
|
||||||
htmlUrl: htmlUrl,
|
|
||||||
createdAt: createdAt,
|
|
||||||
assets: assetsData,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _UpdateSheet extends StatefulWidget {
|
class _UpdateSheet extends StatefulWidget {
|
||||||
@@ -655,7 +667,7 @@ class _UpdateSheetState extends State<_UpdateSheet> {
|
|||||||
final updateService = UpdateService(
|
final updateService = UpdateService(
|
||||||
useProxy: widget.useProxy,
|
useProxy: widget.useProxy,
|
||||||
);
|
);
|
||||||
updateService._performAutomaticWindowsUpdate(
|
updateService.performAutomaticWindowsUpdate(
|
||||||
context,
|
context,
|
||||||
widget.windowsUpdateUrl!,
|
widget.windowsUpdateUrl!,
|
||||||
);
|
);
|
||||||
|
Reference in New Issue
Block a user