✨ Android install update
This commit is contained in:
@@ -30,7 +30,6 @@ import 'package:image_picker_platform_interface/image_picker_platform_interface.
|
|||||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
import 'package:flutter_langdetect/flutter_langdetect.dart' as langdetect;
|
import 'package:flutter_langdetect/flutter_langdetect.dart' as langdetect;
|
||||||
import 'package:island/services/update_service.dart';
|
|
||||||
|
|
||||||
@pragma('vm:entry-point')
|
@pragma('vm:entry-point')
|
||||||
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||||
@@ -144,15 +143,6 @@ void main() async {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Schedule update check shortly after startup, when a context is available.
|
|
||||||
// Uses the global overlay key to obtain a BuildContext safely.
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
final ctx = globalOverlay.currentContext;
|
|
||||||
if (ctx != null) {
|
|
||||||
UpdateService().checkForUpdates(ctx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Router will be provided through Riverpod
|
// Router will be provided through Riverpod
|
||||||
|
@@ -7,12 +7,12 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/services/udid.native.dart';
|
import 'package:island/services/udid.native.dart';
|
||||||
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:island/services/update_service.dart';
|
import 'package:island/services/update_service.dart';
|
||||||
import 'package:island/widgets/content/sheet.dart';
|
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
@@ -205,33 +205,16 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
|
|||||||
// Fetch latest release and show the unified sheet
|
// Fetch latest release and show the unified sheet
|
||||||
final svc = UpdateService();
|
final svc = UpdateService();
|
||||||
// Reuse service fetch + compare to decide content
|
// Reuse service fetch + compare to decide content
|
||||||
|
showLoadingModal(context);
|
||||||
final release = await svc.fetchLatestRelease();
|
final release = await svc.fetchLatestRelease();
|
||||||
|
if (!context.mounted) return;
|
||||||
|
hideLoadingModal(context);
|
||||||
if (release != null) {
|
if (release != null) {
|
||||||
await svc.showUpdateSheet(context, release);
|
await svc.showUpdateSheet(context, release);
|
||||||
} else {
|
} else {
|
||||||
// Fallback: show a simple sheet indicating no info
|
showInfoAlert(
|
||||||
// Use your SheetScaffold for consistent styling
|
'Currently cannot get update from the GitHub.',
|
||||||
// Show a minimal message
|
'Unable to check for updates',
|
||||||
// ignore: use_build_context_synchronously
|
|
||||||
showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
isScrollControlled: true,
|
|
||||||
useSafeArea: true,
|
|
||||||
showDragHandle: true,
|
|
||||||
backgroundColor:
|
|
||||||
Theme.of(context).colorScheme.surface,
|
|
||||||
builder:
|
|
||||||
(_) => const SheetScaffold(
|
|
||||||
titleText: 'Update',
|
|
||||||
child: Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.all(24),
|
|
||||||
child: Text(
|
|
||||||
'Unable to fetch release info at this time.',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -1,19 +1,27 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:developer';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_app_update/azhon_app_update.dart';
|
||||||
|
import 'package:flutter_app_update/update_model.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
import 'package:collection/collection.dart'; // Added for firstWhereOrNull
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import 'package:island/widgets/content/sheet.dart';
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
|
|
||||||
/// Data model for a GitHub release we care about
|
/// Data model for a GitHub release we care about
|
||||||
class GithubReleaseInfo {
|
class GithubReleaseInfo {
|
||||||
final String tagName; // e.g. 3.1.0+118
|
final String tagName;
|
||||||
final String name; // release title
|
final String name;
|
||||||
final String body; // changelog markdown
|
final String body;
|
||||||
final String htmlUrl; // release page
|
final String htmlUrl;
|
||||||
final DateTime createdAt;
|
final DateTime createdAt;
|
||||||
|
final List<GithubReleaseAsset> assets;
|
||||||
|
|
||||||
const GithubReleaseInfo({
|
const GithubReleaseInfo({
|
||||||
required this.tagName,
|
required this.tagName,
|
||||||
@@ -21,9 +29,28 @@ class GithubReleaseInfo {
|
|||||||
required this.body,
|
required this.body,
|
||||||
required this.htmlUrl,
|
required this.htmlUrl,
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
|
this.assets = const [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Data model for a GitHub release asset
|
||||||
|
class GithubReleaseAsset {
|
||||||
|
final String name;
|
||||||
|
final String browserDownloadUrl;
|
||||||
|
|
||||||
|
const GithubReleaseAsset({
|
||||||
|
required this.name,
|
||||||
|
required this.browserDownloadUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory GithubReleaseAsset.fromJson(Map<String, dynamic> json) {
|
||||||
|
return GithubReleaseAsset(
|
||||||
|
name: json['name'] as String,
|
||||||
|
browserDownloadUrl: json['browser_download_url'] as String,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Parses version and build number from "x.y.z+build"
|
/// Parses version and build number from "x.y.z+build"
|
||||||
class _ParsedVersion implements Comparable<_ParsedVersion> {
|
class _ParsedVersion implements Comparable<_ParsedVersion> {
|
||||||
final int major;
|
final int major;
|
||||||
@@ -85,31 +112,52 @@ class UpdateService {
|
|||||||
/// Checks GitHub for the latest release and compares against the current app version.
|
/// Checks GitHub for the latest release and compares against the current app version.
|
||||||
/// If update is available, shows a bottom sheet with changelog and an action to open release page.
|
/// If update is available, shows a bottom sheet with changelog and an action to open release page.
|
||||||
Future<void> checkForUpdates(BuildContext context) async {
|
Future<void> checkForUpdates(BuildContext context) async {
|
||||||
|
log('[Update] Checking for updates...');
|
||||||
try {
|
try {
|
||||||
final release = await fetchLatestRelease();
|
final release = await fetchLatestRelease();
|
||||||
if (release == null) return;
|
if (release == null) {
|
||||||
|
log('[Update] No latest release found or could not fetch.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log('[Update] Fetched latest release: ${release.tagName}');
|
||||||
|
|
||||||
final info = await PackageInfo.fromPlatform();
|
final info = await PackageInfo.fromPlatform();
|
||||||
final localVersionStr = '${info.version}+${info.buildNumber}';
|
final localVersionStr = '${info.version}+${info.buildNumber}';
|
||||||
|
log('[Update] Local app version: $localVersionStr');
|
||||||
|
|
||||||
final latest = _ParsedVersion.tryParse(release.tagName);
|
final latest = _ParsedVersion.tryParse(release.tagName);
|
||||||
final local = _ParsedVersion.tryParse(localVersionStr);
|
final local = _ParsedVersion.tryParse(localVersionStr);
|
||||||
|
|
||||||
if (latest == null || local == null) {
|
if (latest == null || local == null) {
|
||||||
|
log(
|
||||||
|
'[Update] Failed to parse versions. Latest: ${release.tagName}, Local: $localVersionStr',
|
||||||
|
);
|
||||||
// If parsing fails, do nothing silently
|
// If parsing fails, do nothing silently
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
log('[Update] Parsed versions. Latest: $latest, Local: $local');
|
||||||
|
|
||||||
final needsUpdate = latest.compareTo(local) > 0;
|
final needsUpdate = latest.compareTo(local) > 0;
|
||||||
if (!needsUpdate) return;
|
if (!needsUpdate) {
|
||||||
|
log('[Update] App is up to date. No update needed.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log('[Update] Update available! Latest: $latest, Local: $local');
|
||||||
|
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) {
|
||||||
|
log('[Update] Context not mounted, cannot show update sheet.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Delay to ensure UI is ready (if called at startup)
|
// Delay to ensure UI is ready (if called at startup)
|
||||||
await Future.delayed(const Duration(milliseconds: 100));
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
await showUpdateSheet(context, release);
|
await showUpdateSheet(context, release);
|
||||||
} catch (_) {
|
log('[Update] Update sheet shown.');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log('[Update] Error checking for updates: $e');
|
||||||
// Ignore errors (network, api, etc.)
|
// Ignore errors (network, api, etc.)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -126,8 +174,12 @@ class UpdateService {
|
|||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
useRootNavigator: true,
|
useRootNavigator: true,
|
||||||
builder:
|
builder: (ctx) {
|
||||||
(ctx) => _UpdateSheet(
|
String? androidUpdateUrl;
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
androidUpdateUrl = _getAndroidUpdateUrl(release.assets);
|
||||||
|
}
|
||||||
|
return _UpdateSheet(
|
||||||
release: release,
|
release: release,
|
||||||
onOpen: () async {
|
onOpen: () async {
|
||||||
final uri = Uri.parse(release.htmlUrl);
|
final uri = Uri.parse(release.htmlUrl);
|
||||||
@@ -135,16 +187,49 @@ class UpdateService {
|
|||||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
androidUpdateUrl: androidUpdateUrl,
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _getAndroidUpdateUrl(List<GithubReleaseAsset> assets) {
|
||||||
|
final arm64 = assets.firstWhereOrNull(
|
||||||
|
(asset) => asset.name == 'app-arm64-v8a-release.apk',
|
||||||
|
);
|
||||||
|
final armeabi = assets.firstWhereOrNull(
|
||||||
|
(asset) => asset.name == 'app-armeabi-v7a-release.apk',
|
||||||
|
);
|
||||||
|
final x86_64 = assets.firstWhereOrNull(
|
||||||
|
(asset) => asset.name == 'app-x86_64-release.apk',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Prioritize arm64, then armeabi, then x86_64
|
||||||
|
if (arm64 != null) {
|
||||||
|
return arm64.browserDownloadUrl;
|
||||||
|
} else if (armeabi != null) {
|
||||||
|
return armeabi.browserDownloadUrl;
|
||||||
|
} else if (x86_64 != null) {
|
||||||
|
return x86_64.browserDownloadUrl;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch the latest release info from GitHub.
|
/// Fetch the latest release info from GitHub.
|
||||||
/// Public so other screens (e.g., About) can manually trigger update checks.
|
/// Public so other screens (e.g., About) can manually trigger update checks.
|
||||||
Future<GithubReleaseInfo?> fetchLatestRelease() async {
|
Future<GithubReleaseInfo?> fetchLatestRelease() async {
|
||||||
|
log(
|
||||||
|
'[Update] Fetching latest release from GitHub API: $_releasesLatestApi',
|
||||||
|
);
|
||||||
final resp = await _dio.get(_releasesLatestApi);
|
final resp = await _dio.get(_releasesLatestApi);
|
||||||
if (resp.statusCode != 200) return null;
|
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>;
|
final data = resp.data as Map<String, dynamic>;
|
||||||
|
log('[Update] Successfully fetched release data.');
|
||||||
|
|
||||||
final tagName = (data['tag_name'] ?? '').toString();
|
final tagName = (data['tag_name'] ?? '').toString();
|
||||||
final name = (data['name'] ?? tagName).toString();
|
final name = (data['name'] ?? tagName).toString();
|
||||||
@@ -152,25 +237,52 @@ class UpdateService {
|
|||||||
final htmlUrl = (data['html_url'] ?? '').toString();
|
final htmlUrl = (data['html_url'] ?? '').toString();
|
||||||
final createdAtStr = (data['created_at'] ?? '').toString();
|
final createdAtStr = (data['created_at'] ?? '').toString();
|
||||||
final createdAt = DateTime.tryParse(createdAtStr) ?? DateTime.now();
|
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) return null;
|
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(
|
return GithubReleaseInfo(
|
||||||
tagName: tagName,
|
tagName: tagName,
|
||||||
name: name,
|
name: name,
|
||||||
body: body,
|
body: body,
|
||||||
htmlUrl: htmlUrl,
|
htmlUrl: htmlUrl,
|
||||||
createdAt: createdAt,
|
createdAt: createdAt,
|
||||||
|
assets: assetsData,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _UpdateSheet extends StatelessWidget {
|
class _UpdateSheet extends StatelessWidget {
|
||||||
const _UpdateSheet({required this.release, required this.onOpen});
|
const _UpdateSheet({
|
||||||
|
required this.release,
|
||||||
|
required this.onOpen,
|
||||||
|
this.androidUpdateUrl, // Made nullable
|
||||||
|
});
|
||||||
|
|
||||||
|
final String? androidUpdateUrl; // Changed to nullable
|
||||||
final GithubReleaseInfo release;
|
final GithubReleaseInfo release;
|
||||||
final VoidCallback onOpen;
|
final VoidCallback onOpen;
|
||||||
|
|
||||||
|
Future<void> installUpdate(String url) async {
|
||||||
|
UpdateModel model = UpdateModel(
|
||||||
|
url,
|
||||||
|
"solian-update-${release.tagName}.apk",
|
||||||
|
"ic_launcher",
|
||||||
|
'https://apps.apple.com/us/app/solian/id6499032345',
|
||||||
|
);
|
||||||
|
AzhonAppUpdate.update(model);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
@@ -208,7 +320,21 @@ class _UpdateSheet extends StatelessWidget {
|
|||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
|
spacing: 8,
|
||||||
children: [
|
children: [
|
||||||
|
if (!kIsWeb &&
|
||||||
|
Platform.isAndroid &&
|
||||||
|
androidUpdateUrl != null)
|
||||||
|
Expanded(
|
||||||
|
child: FilledButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
log(androidUpdateUrl!);
|
||||||
|
installUpdate(androidUpdateUrl!);
|
||||||
|
},
|
||||||
|
icon: const Icon(Symbols.update),
|
||||||
|
label: const Text('Install update'),
|
||||||
|
),
|
||||||
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: FilledButton.icon(
|
child: FilledButton.icon(
|
||||||
onPressed: onOpen,
|
onPressed: onOpen,
|
||||||
|
@@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:island/pods/websocket.dart';
|
import 'package:island/pods/websocket.dart';
|
||||||
import 'package:island/services/notify.dart';
|
import 'package:island/services/notify.dart';
|
||||||
import 'package:island/services/sharing_intent.dart';
|
import 'package:island/services/sharing_intent.dart';
|
||||||
|
import 'package:island/services/update_service.dart';
|
||||||
import 'package:island/widgets/content/network_status_sheet.dart';
|
import 'package:island/widgets/content/network_status_sheet.dart';
|
||||||
import 'package:island/widgets/tour/tour.dart';
|
import 'package:island/widgets/tour/tour.dart';
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ class AppWrapper extends HookConsumerWidget {
|
|||||||
});
|
});
|
||||||
final sharingService = SharingIntentService();
|
final sharingService = SharingIntentService();
|
||||||
sharingService.initialize(context);
|
sharingService.initialize(context);
|
||||||
|
UpdateService().checkForUpdates(context);
|
||||||
return () {
|
return () {
|
||||||
sharingService.dispose();
|
sharingService.dispose();
|
||||||
ntySubs?.cancel();
|
ntySubs?.cancel();
|
||||||
|
Reference in New Issue
Block a user