diff --git a/.github/workflows/fastlane.yml b/.github/workflows/fastlane.yml deleted file mode 100644 index b88c5eb34..000000000 --- a/.github/workflows/fastlane.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Validate Fastlane metadata - -on: - workflow_dispatch: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - -jobs: - go: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Validate Fastlane Supply Metadata - uses: ashutoshgngwr/validate-fastlane-supply-metadata@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fcc36b2b8..bc88098bf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,7 +1,4 @@ name: Build and Release -permissions: - contents: write - packages: write on: workflow_dispatch: @@ -9,6 +6,9 @@ on: beta: type: boolean description: Is beta? + draft: + type: boolean + description: Is draft? jobs: build: @@ -18,17 +18,22 @@ jobs: - uses: actions/checkout@v3 - uses: subosito/flutter-action@v2 - with: - channel: stable - uses: actions/setup-java@v4 with: distribution: 'temurin' # See 'Supported distributions' for available options - java-version: '21' + java-version: '17' - name: Flutter Doctor id: flutter_doctor run: | flutter doctor -v + + - name: Import GPG key + id: import_pgp_key + uses: crazy-max/ghaction-import-gpg@v6 + with: + gpg_private_key: ${{ secrets.PGP_KEY_BASE64 }} + passphrase: ${{ secrets.PGP_PASSPHRASE }} - name: Check submodule id: check_submodule @@ -47,18 +52,29 @@ jobs: - name: Build APKs run: | - sed -i 's/signingConfig = signingConfigs.getByName("release")//g' android/app/build.gradle.kts + sed -i 's/signingConfig signingConfigs.release//g' android/app/build.gradle flutter build apk --flavor normal && flutter build apk --split-per-abi --flavor normal for file in build/app/outputs/flutter-apk/app-*normal*.apk*; do mv "$file" "${file//-normal/}"; done flutter build apk --flavor fdroid -t lib/main_fdroid.dart && flutter build apk --split-per-abi --flavor fdroid -t lib/main_fdroid.dart rm ./build/app/outputs/flutter-apk/*.sha1 - cp ./sign.sh ./build/app/outputs/flutter-apk/ ls -l ./build/app/outputs/flutter-apk/ - - - name: Save Unsigned APKs as Action Artifacts - uses: actions/upload-artifact@v4 - with: - path: build/app/outputs/flutter-apk/* + + - name: Sign APKs + env: + KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }} + KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} + PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} + run: | + echo "${KEYSTORE_BASE64}" | base64 -d > apksign.keystore + for apk in ./build/app/outputs/flutter-apk/*-release*.apk; do + unsignedFn=${apk/-release/-unsigned} + mv "$apk" "$unsignedFn" + ${ANDROID_HOME}/build-tools/$(ls ${ANDROID_HOME}/build-tools/ | tail -1)/apksigner sign --ks apksign.keystore --ks-pass pass:"${KEYSTORE_PASSWORD}" --out "${apk}" "${unsignedFn}" + sha256sum ${apk} | cut -d " " -f 1 > "$apk".sha256 + gpg --batch --pinentry-mode loopback --passphrase "${PGP_PASSPHRASE}" --sign --detach-sig "$apk".sha256 + done + rm apksign.keystore + PGP_KEY_FINGERPRINT="${{ steps.import_pgp_key.outputs.fingerprint }}" - name: Create Tag uses: mathieudutour/github-tag-action@v6.1 @@ -67,11 +83,12 @@ jobs: custom_tag: "${{ steps.extract_version.outputs.tag }}" tag_prefix: "" - - name: Create Draft Release + - name: Create Release And Upload APKs uses: ncipollo/release-action@v1 with: token: ${{ secrets.GH_ACCESS_TOKEN }} tag: "${{ steps.extract_version.outputs.tag }}" prerelease: "${{ steps.extract_version.outputs.beta }}" - draft: "true" + draft: "${{ inputs.draft }}" + artifacts: ./build/app/outputs/flutter-apk/*-release*.apk* generateReleaseNotes: true diff --git a/.github/workflows/translations.yml b/.github/workflows/translations.yml new file mode 100644 index 000000000..55de391c0 --- /dev/null +++ b/.github/workflows/translations.yml @@ -0,0 +1,41 @@ +name: Auto-Update Translations + +on: + push: + branches: [ main ] + paths: + - 'lib/**.dart' + +jobs: + update-translations: + permissions: + contents: write + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Install gettext + run: sudo apt-get install -y gettext + + - name: Extract Strings and Update PO Files + run: | + mkdir -p assets/translations + # Search for tr('...') or tr("...") in the code + find lib -name "*.dart" | xgettext -f - \ + --from-code=UTF-8 \ + --language=Python \ + --keyword=tr \ + --output=assets/translations/app.pot || touch assets/translations/app.pot + + # Sync PO files with the new template + [ -f assets/translations/en.po ] && msgmerge -U assets/translations/en.po assets/translations/app.pot || cp assets/translations/app.pot assets/translations/en.po + [ -f assets/translations/he.po ] && msgmerge -U assets/translations/he.po assets/translations/app.pot || cp assets/translations/app.pot assets/translations/he.po + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v5 + with: + commit-message: "chore: update translation files" + title: "🌍 Translation Update" + body: "Found new strings via tr() function." + branch: translations-update \ No newline at end of file diff --git a/README.md b/README.md index 685d6421a..04cdfb9d2 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![Download Nightly](https://img.shields.io/badge/Download-Nightly-blue?style=for-the-badge&logo=android)](https://github.com/omeritzics/Updatium/releases/tag/nightly-build) (Note: Updatium is still not ready to function as an independant app, this is a work in progress) # ![Updatium Icon](./assets/graphics/icon_small.png) Updatium -Update your Android apps directly from the APK source. Forked from [Obtainium](https://github.com/ImranR98/Obtainium) due to the developer's problematic political views and his terrible behavior towards Jewish people who wanted to contribute to his app. +Update your Android apps directly from the APK source. Forked from [Obtainium](https://github.com/ImranR98/Obtainium) due to the developer's problematic political views and his terrible behaviour towards Jewish people who wanted to contribute to his app. Updatium allows you to install and update apps directly from their releases pages, and receive notifications when new releases are made available. diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts index ab39a10a2..32bae7193 100644 --- a/android/settings.gradle.kts +++ b/android/settings.gradle.kts @@ -1,13 +1,30 @@ pluginManagement { val flutterSdkPath = run { val properties = java.util.Properties() - file("local.properties").inputStream().use { properties.load(it) } - val flutterSdkPath = properties.getProperty("flutter.sdk") - require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } - flutterSdkPath + val localPropsFile = file("local.properties") + + val sdkFromProps = if (localPropsFile.exists()) { + localPropsFile.inputStream().use { properties.load(it) } + properties.getProperty("flutter.sdk") + } else null + + // Fallbacks if local.properties is missing or doesn't contain flutter.sdk: + // 1) Environment variable FLUTTER_ROOT + // 2) Environment variable FLUTTER_HOME + // 3) $HOME/flutter + sdkFromProps + ?: System.getenv("FLUTTER_ROOT") + ?: System.getenv("FLUTTER_HOME") + ?: "${System.getProperty("user.home")}/flutter" } - includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + val flutterSdkDir = file(flutterSdkPath) + if (flutterSdkDir.exists()) { + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + } else { + // Avoid failing the build; warn and continue (CI builds can set the path separately). + println("Warning: Flutter SDK not found at '$flutterSdkPath'. Skipping includeBuild for flutter_tools.") + } repositories { google() diff --git a/lib/l10n/translation_service.dart b/lib/l10n/translation_service.dart new file mode 100644 index 000000000..c4e65a2af --- /dev/null +++ b/lib/l10n/translation_service.dart @@ -0,0 +1,31 @@ +import 'package:flutter/services.dart'; +import 'package:gettext/gettext.dart'; +import 'dart:ui' as ui; + +class I18n { + static final Gettext _gt = Gettext(); + + static Future init() async { + // Determine the device language + String locale = ui.window.locale.languageCode; + + try { + // Load the .po file from assets + String poContent = await rootBundle.loadString('lib/l10n/$locale.po'); + _gt.addTranslations(locale, poContent); + _gt.locale = locale; + } catch (e) { + // Fallback to English if the language file is missing + try { + String enContent = await rootBundle.loadString('lib/l10n/en.po'); + _gt.addTranslations('en', enContent); + _gt.locale = 'en'; + } catch (err) { + print("Localization error: $err"); + } + } + } + + // Translation helper function + static String t(String key) => _gt.gettext(key); +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 1b94ebb6f..40cbc755e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -19,6 +19,8 @@ import 'package:easy_localization/src/easy_localization_controller.dart'; // ignore: implementation_imports import 'package:easy_localization/src/localization.dart'; import 'package:flutter_foreground_task/flutter_foreground_task.dart'; +// Added for PO file support +import 'package:easy_gettext/easy_gettext.dart'; List> supportedLocales = const [ MapEntry(Locale('en'), 'English'), @@ -28,7 +30,7 @@ List> supportedLocales = const [ MapEntry(Locale('ja'), '日本語'), MapEntry(Locale('hu'), 'Magyar'), MapEntry(Locale('de'), 'Deutsch'), - MapEntry(Locale('fa'), 'فارسی'), + MapEntry(Locale('fa'), 'فارסי'), MapEntry(Locale('fr'), 'Français'), MapEntry(Locale('es'), 'Español'), MapEntry(Locale('pl'), 'Polski'), @@ -70,7 +72,8 @@ Future loadTranslations() async { forceLocale: forceLocale, fallbackLocale: fallbackLocale, supportedLocales: supportedLocales.map((e) => e.key).toList(), - assetLoader: const RootBundleAssetLoader(), + // Updated to use GettextLoader for PO files + assetLoader: const GettextLoader(), useOnlyLangCode: false, useFallbackTranslations: true, path: localeDir, @@ -160,264 +163,4 @@ void main() async { child: EasyLocalization( supportedLocales: supportedLocales.map((e) => e.key).toList(), path: localeDir, - fallbackLocale: fallbackLocale, - useOnlyLangCode: false, - child: const Updatium(), - ), - ), - ); - BackgroundFetch.registerHeadlessTask(backgroundFetchHeadlessTask); -} - -class Updatium extends StatefulWidget { - const Updatium({super.key}); - - @override - State createState() => _UpdatiumState(); -} - -class _UpdatiumState extends State { - var existingUpdateInterval = -1; - - @override - void initState() { - super.initState(); - initPlatformState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - requestNonOptionalPermissions(); - }); - } - - Future requestNonOptionalPermissions() async { - final NotificationPermission notificationPermission = - await FlutterForegroundTask.checkNotificationPermission(); - if (notificationPermission != NotificationPermission.granted) { - await FlutterForegroundTask.requestNotificationPermission(); - } - if (!await FlutterForegroundTask.isIgnoringBatteryOptimizations) { - await FlutterForegroundTask.requestIgnoreBatteryOptimization(); - } - } - - void initForegroundService() { - // ignore: invalid_use_of_visible_for_testing_member - if (!FlutterForegroundTask.isInitialized) { - FlutterForegroundTask.init( - androidNotificationOptions: AndroidNotificationOptions( - channelId: 'bg_update', - channelName: tr('foregroundService'), - channelDescription: tr('foregroundService'), - onlyAlertOnce: true, - ), - iosNotificationOptions: const IOSNotificationOptions( - showNotification: false, - playSound: false, - ), - foregroundTaskOptions: ForegroundTaskOptions( - eventAction: ForegroundTaskEventAction.repeat(900000), - autoRunOnBoot: true, - autoRunOnMyPackageReplaced: true, - allowWakeLock: true, - allowWifiLock: true, - ), - ); - } - } - - Future startForegroundService(bool restart) async { - initForegroundService(); - if (await FlutterForegroundTask.isRunningService) { - if (restart) { - return FlutterForegroundTask.restartService(); - } - } else { - return FlutterForegroundTask.startService( - serviceTypes: [ForegroundServiceTypes.specialUse], - serviceId: 666, - notificationTitle: tr('foregroundService'), - notificationText: tr('fgServiceNotice'), - notificationIcon: NotificationIcon( - metaDataName: 'com.omeritzics.updatium.service.NOTIFICATION_ICON', - ), - callback: startCallback, - ); - } - return null; - } - - stopForegroundService() async { - if (await FlutterForegroundTask.isRunningService) { - return FlutterForegroundTask.stopService(); - } - } - - // void onReceiveForegroundServiceData(Object data) { - // print('onReceiveTaskData: $data'); - // } - - @override - void dispose() { - // Remove a callback to receive data sent from the TaskHandler. - // FlutterForegroundTask.removeTaskDataCallback(onReceiveForegroundServiceData); - super.dispose(); - } - - Future initPlatformState() async { - await BackgroundFetch.configure( - BackgroundFetchConfig( - minimumFetchInterval: 15, - stopOnTerminate: false, - startOnBoot: true, - enableHeadless: true, - requiresBatteryNotLow: false, - requiresCharging: false, - requiresStorageNotLow: false, - requiresDeviceIdle: false, - requiredNetworkType: NetworkType.ANY, - ), - (String taskId) async { - await bgUpdateCheck(taskId, null); - BackgroundFetch.finish(taskId); - }, - (String taskId) async { - context.read().add('BG update task timed out.'); - BackgroundFetch.finish(taskId); - }, - ); - if (!mounted) return; - } - - @override - Widget build(BuildContext context) { - SettingsProvider settingsProvider = context.watch(); - AppsProvider appsProvider = context.read(); - LogsProvider logs = context.read(); - NotificationsProvider notifs = context.read(); - if (settingsProvider.updateInterval == 0) { - stopForegroundService(); - BackgroundFetch.stop(); - } else { - if (settingsProvider.useFGService) { - BackgroundFetch.stop(); - startForegroundService(false); - } else { - stopForegroundService(); - BackgroundFetch.start(); - } - } - if (settingsProvider.prefs == null) { - settingsProvider.initializeSettings(); - } else { - bool isFirstRun = settingsProvider.checkAndFlipFirstRun(); - if (isFirstRun) { - logs.add('This is the first ever run of Updatium.'); - // If this is the first run, add Updatium to the Apps list - if (!fdroid) { - getInstalledInfo(updatiumId) - .then((value) { - if (value?.versionName != null) { - appsProvider.saveApps([ - App( - updatiumId, - updatiumUrl, - 'omeritzics', - 'Updatium', - value!.versionName, - value.versionName!, - [], - 0, - { - 'versionDetection': true, - 'apkFilterRegEx': 'fdroid', - 'invertAPKFilter': true, - }, - null, - false, - ), - ], onlyIfExists: false); - } - }) - .catchError((err) { - print(err); - }); - } - } - if (!supportedLocales.map((e) => e.key).contains(context.locale) || - (settingsProvider.forcedLocale == null && - context.deviceLocale != context.locale)) { - settingsProvider.resetLocaleSafe(context); - } - } - - WidgetsBinding.instance.addPostFrameCallback((_) { - notifs.checkLaunchByNotif(); - }); - - return WithForegroundTask( - child: DynamicColorBuilder( - builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) { - // Decide on a colour/brightness scheme based on OS and user settings - ColorScheme lightColorScheme; - ColorScheme darkColorScheme; - if (lightDynamic != null && - darkDynamic != null && - settingsProvider.useMaterialYou) { - lightColorScheme = lightDynamic.harmonized(); - darkColorScheme = darkDynamic.harmonized(); - } else { - lightColorScheme = ColorScheme.fromSeed( - seedColor: settingsProvider.themeColor, - ); - darkColorScheme = ColorScheme.fromSeed( - seedColor: settingsProvider.themeColor, - brightness: Brightness.dark, - ); - } - - // set the background and surface colors to pure black in the amoled theme - if (settingsProvider.useBlackTheme) { - darkColorScheme = darkColorScheme - .copyWith(surface: Colors.black) - .harmonized(); - } - - if (settingsProvider.useSystemFont) NativeFeatures.loadSystemFont(); - - return MaterialApp( - title: 'Updatium', - localizationsDelegates: context.localizationDelegates, - supportedLocales: context.supportedLocales, - locale: context.locale, - navigatorKey: globalNavigatorKey, - debugShowCheckedModeBanner: false, - theme: ThemeData( - useMaterial3: true, - colorScheme: settingsProvider.theme == ThemeSettings.dark - ? darkColorScheme - : lightColorScheme, - fontFamily: settingsProvider.useSystemFont - ? 'SystemFont' - : 'Montserrat', - ), - darkTheme: ThemeData( - useMaterial3: true, - colorScheme: settingsProvider.theme == ThemeSettings.light - ? lightColorScheme - : darkColorScheme, - fontFamily: settingsProvider.useSystemFont - ? 'SystemFont' - : 'Montserrat', - ), - home: Shortcuts( - shortcuts: { - LogicalKeySet(LogicalKeyboardKey.select): - const ActivateIntent(), - }, - child: const HomePage(), - ), - ); - }, - ), - ); - } -} + fallbackLocale: \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 43db3e2b3..ae0ac1c0b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,6 +30,13 @@ environment: dependencies: flutter: sdk: flutter + gettext: ^0.2.0 + easy_gettext: ^0.0.1 + +flutter: + assets: + - lib/l10n/en.po + - lib/l10n/he.po # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons.