From 01074b73bf5f9183e7d929fd83a7e96a4c64fa34 Mon Sep 17 00:00:00 2001 From: chaos-zhu Date: Sun, 24 May 2026 22:31:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=89=88=E6=9C=AC=E5=8F=B7=E5=B1=95?= =?UTF-8?q?=E7=A4=BA&Android=E7=AB=AF=E6=9E=84=E5=BB=BA=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mobile/.gitignore | 5 +++ mobile/android/app/build.gradle.kts | 41 +++++++++++++++---- .../github/chaoszhu/easynode}/MainActivity.kt | 2 +- mobile/android/key.properties.example | 4 ++ mobile/lib/features/auth/login_page.dart | 16 +++++--- mobile/lib/features/shell/settings_tab.dart | 18 ++++++++ mobile/lib/state/package_info_provider.dart | 10 +++++ mobile/pubspec.lock | 10 ++--- mobile/pubspec.yaml | 3 +- 9 files changed, 88 insertions(+), 21 deletions(-) rename mobile/android/app/src/main/kotlin/{com/example/mobile => io/github/chaoszhu/easynode}/MainActivity.kt (72%) create mode 100644 mobile/android/key.properties.example create mode 100644 mobile/lib/state/package_info_provider.dart diff --git a/mobile/.gitignore b/mobile/.gitignore index 3820a95..cb4023b 100644 --- a/mobile/.gitignore +++ b/mobile/.gitignore @@ -43,3 +43,8 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release + +# Android signing — never commit keystores or their passwords. +/android/key.properties +/android/*.jks +/android/*.keystore diff --git a/mobile/android/app/build.gradle.kts b/mobile/android/app/build.gradle.kts index 18a310a..7e342d2 100644 --- a/mobile/android/app/build.gradle.kts +++ b/mobile/android/app/build.gradle.kts @@ -1,3 +1,6 @@ +import java.util.Properties +import java.io.FileInputStream + plugins { id("com.android.application") id("kotlin-android") @@ -5,8 +8,15 @@ plugins { id("dev.flutter.flutter-gradle-plugin") } +val keystoreProperties = Properties().apply { + val file = rootProject.file("key.properties") + if (file.exists()) { + load(FileInputStream(file)) + } +} + android { - namespace = "com.example.mobile" + namespace = "io.github.chaoszhu.easynode" compileSdk = flutter.compileSdkVersion ndkVersion = flutter.ndkVersion @@ -20,21 +30,36 @@ android { } defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId = "com.example.mobile" - // You can update the following values to match your application needs. - // For more information, see: https://flutter.dev/to/review-gradle-config. + applicationId = "io.github.chaoszhu.easynode" minSdk = flutter.minSdkVersion targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName } + signingConfigs { + create("release") { + val storeFilePath = keystoreProperties["storeFile"] as String? + if (storeFilePath != null) { + storeFile = rootProject.file(storeFilePath) + storePassword = keystoreProperties["storePassword"] as String? + keyAlias = keystoreProperties["keyAlias"] as String? + keyPassword = keystoreProperties["keyPassword"] as String? + } + } + } + buildTypes { release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig = signingConfigs.getByName("debug") + // Fall back to the debug keystore if key.properties is missing so + // `flutter run --release` keeps working locally for contributors + // who haven't set up signing yet. Real release builds REQUIRE the + // key.properties file to be present. + signingConfig = if (keystoreProperties.isEmpty) { + signingConfigs.getByName("debug") + } else { + signingConfigs.getByName("release") + } } } } diff --git a/mobile/android/app/src/main/kotlin/com/example/mobile/MainActivity.kt b/mobile/android/app/src/main/kotlin/io/github/chaoszhu/easynode/MainActivity.kt similarity index 72% rename from mobile/android/app/src/main/kotlin/com/example/mobile/MainActivity.kt rename to mobile/android/app/src/main/kotlin/io/github/chaoszhu/easynode/MainActivity.kt index b5dc9d0..c15bf98 100644 --- a/mobile/android/app/src/main/kotlin/com/example/mobile/MainActivity.kt +++ b/mobile/android/app/src/main/kotlin/io/github/chaoszhu/easynode/MainActivity.kt @@ -1,4 +1,4 @@ -package com.example.mobile +package io.github.chaoszhu.easynode import io.flutter.embedding.android.FlutterActivity diff --git a/mobile/android/key.properties.example b/mobile/android/key.properties.example new file mode 100644 index 0000000..37be000 --- /dev/null +++ b/mobile/android/key.properties.example @@ -0,0 +1,4 @@ +storePassword= +keyPassword= +keyAlias=easynode +storeFile=easynode-release.jks diff --git a/mobile/lib/features/auth/login_page.dart b/mobile/lib/features/auth/login_page.dart index 59d4308..8f4726b 100644 --- a/mobile/lib/features/auth/login_page.dart +++ b/mobile/lib/features/auth/login_page.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/ui/palette.dart'; import '../../core/utils/jwt_expiry.dart'; import '../../core/utils/validators.dart'; import '../../l10n/app_localizations.dart'; +import '../../state/package_info_provider.dart'; import 'auth_session.dart'; import 'login_controller.dart'; @@ -145,7 +147,6 @@ class _LoginPageState extends State { children: [ _LoginHero( title: l.tr('app.title'), - subtitle: l.tr('app.subtitle'), ), const SizedBox(height: 22), _LoginFormCard( @@ -441,15 +442,18 @@ class _LoginBottomBar extends StatelessWidget { } } -class _LoginHero extends StatelessWidget { - const _LoginHero({required this.title, required this.subtitle}); +class _LoginHero extends ConsumerWidget { + const _LoginHero({required this.title}); final String title; - final String subtitle; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); + final info = ref.watch(packageInfoProvider).valueOrNull; + final versionLabel = info == null + ? '' + : 'v${info.version} (${info.buildNumber})'; return Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ @@ -476,7 +480,7 @@ class _LoginHero extends StatelessWidget { ), const SizedBox(height: 4), Text( - subtitle, + versionLabel, textAlign: TextAlign.center, style: theme.textTheme.bodySmall?.copyWith( color: AppPalette.softMuted, diff --git a/mobile/lib/features/shell/settings_tab.dart b/mobile/lib/features/shell/settings_tab.dart index 44c8f70..3e24071 100644 --- a/mobile/lib/features/shell/settings_tab.dart +++ b/mobile/lib/features/shell/settings_tab.dart @@ -7,6 +7,7 @@ import '../../state/auth_notifier.dart'; import '../../state/credential_list_notifier.dart'; import '../../state/host_list_notifier.dart'; import '../../state/locale_notifier.dart'; +import '../../state/package_info_provider.dart'; import '../../state/plus_discount_notifier.dart'; import '../../state/plus_info_notifier.dart'; import '../../state/script_list_notifier.dart'; @@ -207,6 +208,10 @@ class SettingsTab extends ConsumerWidget { final discount = ref.watch(plusDiscountProvider).valueOrNull ?? const PlusDiscount(discount: false, content: ''); final hasDiscount = discount.discount && discount.content.isNotEmpty; + final packageInfo = ref.watch(packageInfoProvider).valueOrNull; + final versionLabel = packageInfo == null + ? '' + : 'v${packageInfo.version} (${packageInfo.buildNumber})'; return Scaffold( backgroundColor: AppPalette.canvas, @@ -221,6 +226,19 @@ class SettingsTab extends ConsumerWidget { child: TabHeader( title: l.tr('settings.title'), actions: [ + if (versionLabel.isNotEmpty) + Padding( + padding: const EdgeInsets.only(right: 8), + child: Text( + versionLabel, + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.w500, + color: AppPalette.softMuted, + letterSpacing: 0.2, + ), + ), + ), if (!plusActive) _SettingsBellButton( hasDiscount: hasDiscount, diff --git a/mobile/lib/state/package_info_provider.dart b/mobile/lib/state/package_info_provider.dart new file mode 100644 index 0000000..5384bba --- /dev/null +++ b/mobile/lib/state/package_info_provider.dart @@ -0,0 +1,10 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +/// Resolves the running app's version / build number from the platform's +/// PackageInfo so UI surfaces don't hardcode strings that drift from +/// `pubspec.yaml`. Cached for the lifetime of the process — package info +/// never changes after install. +final packageInfoProvider = FutureProvider((ref) async { + return PackageInfo.fromPlatform(); +}); diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 6dce166..2386d09 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -486,13 +486,13 @@ packages: source: hosted version: "2.2.0" package_info_plus: - dependency: transitive + dependency: "direct main" description: name: package_info_plus - sha256: "468c26b4254ab01979fa5e4a98cb343ea3631b9acee6f21028997419a80e1a20" + sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968" url: "https://pub.dev" source: hosted - version: "9.0.1" + version: "8.3.1" package_info_plus_platform_interface: dependency: transitive description: @@ -966,10 +966,10 @@ packages: dependency: transitive description: name: wakelock_plus - sha256: ddf3db70eaa10c37558ff817519b85d527dbd21034fd5d8e1c2e85f31588f1c1 + sha256: "61713aa82b7f85c21c9f4cd0a148abd75f38a74ec645fcb1e446f882c82fd09b" url: "https://pub.dev" source: hosted - version: "1.5.2" + version: "1.3.3" wakelock_plus_platform_interface: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 5a2ce71..d251868 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -1,7 +1,7 @@ name: mobile description: "EasyNode mobile app." publish_to: 'none' -version: 0.1.0+1 +version: 0.1.0-beta.1+2 environment: sdk: ^3.11.5 @@ -34,6 +34,7 @@ dependencies: video_player: ^2.9.2 chewie: ^1.8.5 url_launcher: ^6.3.1 + package_info_plus: ^8.1.0 dependency_overrides: # video_player_android 2.9.x has a known Pigeon channel-error regression on