From d21bc9e063436bede1ec71e0639b918f2c01cad1 Mon Sep 17 00:00:00 2001
From: David Df
Date: Tue, 2 Jun 2026 17:14:11 +0200
Subject: [PATCH 01/26] Dev (#193)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* master (#126)
* Update version badge in README.md to 0.4.4
* Update version badge in README.md
* Feat/live updates (#129)
* Feat/color picker (#127)
* feat(theme): implement HSL color picker and enhanced lightness slider
- **New `CustomColorBottomSheet`**: Introduced a dedicated HSL (Hue, Saturation, Lightness) color picker with gradient sliders for precise theme color customization.
- **Enhanced `ColorsDetailContent`**:
- Overhauled the preset tab with an animated lightness slider that appears when a preset is selected.
- Improved the preset selection logic to maintain the "base" hue even when adjusting lightness to extreme white or black.
- Redesigned the "Custom" colors section to use the new HSL bottom sheet instead of a simple hex text field.
- Switched from simple circular bubbles to a more refined UI with selection rings and rounded-square transitions for active colors.
- **Improved Theme Creator State**:
- Implemented strict state clearing (nulling `currentEditingThemeId`) when exiting the creator to prevent state leakage between editing sessions.
- Updated save/apply dialog logic to ensure the UI resets correctly after persistence.
- **Shared Components**: Added `GradientSlider` and `ColorBubble` to `HyperOSColorPicker.kt` for reusable, stylized color selection components.
- **Localization**: Added missing string resources for color labels (Hue, Saturation, Lightness) in English and Spanish.
* #### feat(theme): implement advanced color modes and redesigned color picker
- **Color Modes**: Introduced a `ColorMode` system (`CUSTOM`, `APP_ICON`, `MATERIAL_YOU`) to replace the binary "Use App Colors" toggle. This allows themes to dynamically adapt to Material You system colors or extract colors from app icons globally and per-app.
- **Improved Color Picker**: Replaced standard text fields with a new `CustomColorBottomSheet` in the Call Style and Theme Creator screens, featuring a visual HSL-based selection and hex formatting.
- **UI & UX Refinement**:
- Redesigned `ColorsDetailContent` with a scrollable layout and new sections for dynamic color modes.
- Updated the Theme Creator preview to correctly reflect the active color mode.
- Standardized "My Theme" and "Share Theme" strings to use localized resources.
- **Data Persistence**: Updated `HyperTheme`, `GlobalConfig`, and `AppThemeOverride` models to support the new `ColorMode` enum while maintaining backward compatibility with legacy themes.
- **Theme Creator**: Refactored `ThemeViewModel` to handle more robust state initialization and migration when editing or creating new themes.
- **Tooling**: Added Crowdin configuration settings for the IDE.
* feat(widget): add favorites, recommended filtering, and update notice (#128)
- **Widget Picker**:
- Implemented a "Recommended" filter to highlight 1x4 and 2x4 layouts compatible with the Island size.
- Added a Favorite system for widget apps, allowing users to star apps for top-level sorting.
- Added a floating navigation bar to switch between Recommended and All widgets.
- Improved dimension display by converting DP to grid cell proportions (e.g., 4x2).
- **Saved Widgets**:
- Added a persistent "Update Notice" dialog to remind users to check Autostart/Battery permissions after an app update.
- Implemented a "Kill Active Widgets" action to instantly dismiss all running widget overlays.
- Redesigned the UI with a centered filter toolbar (Favorites vs. All) and a dedicated Add FAB.
- Added reactive listening for newly added widgets to ensure the list updates immediately.
- **Service & Data**:
- Enhanced `WidgetOverlayService` with specific `ACTION_KILL_WIDGET` and `ACTION_KILL_ALL_WIDGETS` handlers for immediate UI cleanup.
- Added `favoriteWidgetAppsFlow` to `AppPreferences` to persist starred widget providers.
- **Localization**:
- Added English and Spanish strings for filters, permission reminders, and widget management.
CLOSE #72
* feat(service): implement Native Android 16 Live Updates support
- Added `LiveUpdateTranslator` to transform standard notifications into native Android 16 promoted ongoing tasks using `android.requestPromotedOngoing` and `android.shortCriticalText`.
- Implemented a toggle in Global Settings to choose between custom Xiaomi Islands and native Android 16 Live Updates.
- Created a dedicated `hyper_bridge_live_update_channel` for native updates to manage notifications and visibility independently.
- Added `POST_PROMOTED_NOTIFICATIONS` permission to `AndroidManifest.xml` to support Android 16's promoted notification features.
- Enhanced `LiveUpdateTranslator` with a `generateCriticalShortText` helper to dynamically extract progress percentages or ETAs for the system status chip.
- Fixed Android 16 category filtering by forcing Live Updates into strictly valid categories (`PROGRESS` or `TRANSPORT`) to prevent OS blocking.
- Ensured original notification timestamps are preserved for accurate system-wide "time active" calculations.
- Updated `NotificationReaderService` to support hot-swapping between custom injection and native updates via shared preferences.
* feat(service): enhance Live Update processing and Android 16 compatibility
- **NotificationReaderService**: Implemented a more robust content hashing algorithm that tracks Title, Text, Progress (max, current, indeterminate), and Button Action states (e.g., Play to Pause transitions) to ensure accurate UI updates.
- **LiveUpdateTranslator**:
- Added support for large icons and picture extras in live updates.
- Improved media notification handling by suppressing progress bars that often persist in the background.
- Optimized category mapping (Progress vs. Transport) to comply with Android 16 ongoing promotion requirements.
- Updated `shortCriticalText` generation logic to prioritize media titles, percentages, or time ETAs for the Dynamic Island chip.
- Switched to native Android 16 APIs for promoted ongoing requests and short critical text injection.
* feat(settings): refactor Global Settings UI and update version
- Updated `versionName` to `0.5.0-alpha01`.
- Redesigned `GlobalSettingsScreen` to use consistent card-based layouts for all settings items.
- Replaced the "Native Live Updates" `ListItem` with a new standardized `SettingsSwitchItem` component.
- Improved UI spacing and styling across the settings screen, including unified icon background treatments.
- Simplified state management and click behavior for toggleable settings.
* Beta/0 5 0 (#190)
* Feat/color picker (#127)
* feat(theme): implement HSL color picker and enhanced lightness slider
- **New `CustomColorBottomSheet`**: Introduced a dedicated HSL (Hue, Saturation, Lightness) color picker with gradient sliders for precise theme color customization.
- **Enhanced `ColorsDetailContent`**:
- Overhauled the preset tab with an animated lightness slider that appears when a preset is selected.
- Improved the preset selection logic to maintain the "base" hue even when adjusting lightness to extreme white or black.
- Redesigned the "Custom" colors section to use the new HSL bottom sheet instead of a simple hex text field.
- Switched from simple circular bubbles to a more refined UI with selection rings and rounded-square transitions for active colors.
- **Improved Theme Creator State**:
- Implemented strict state clearing (nulling `currentEditingThemeId`) when exiting the creator to prevent state leakage between editing sessions.
- Updated save/apply dialog logic to ensure the UI resets correctly after persistence.
- **Shared Components**: Added `GradientSlider` and `ColorBubble` to `HyperOSColorPicker.kt` for reusable, stylized color selection components.
- **Localization**: Added missing string resources for color labels (Hue, Saturation, Lightness) in English and Spanish.
* #### feat(theme): implement advanced color modes and redesigned color picker
- **Color Modes**: Introduced a `ColorMode` system (`CUSTOM`, `APP_ICON`, `MATERIAL_YOU`) to replace the binary "Use App Colors" toggle. This allows themes to dynamically adapt to Material You system colors or extract colors from app icons globally and per-app.
- **Improved Color Picker**: Replaced standard text fields with a new `CustomColorBottomSheet` in the Call Style and Theme Creator screens, featuring a visual HSL-based selection and hex formatting.
- **UI & UX Refinement**:
- Redesigned `ColorsDetailContent` with a scrollable layout and new sections for dynamic color modes.
- Updated the Theme Creator preview to correctly reflect the active color mode.
- Standardized "My Theme" and "Share Theme" strings to use localized resources.
- **Data Persistence**: Updated `HyperTheme`, `GlobalConfig`, and `AppThemeOverride` models to support the new `ColorMode` enum while maintaining backward compatibility with legacy themes.
- **Theme Creator**: Refactored `ThemeViewModel` to handle more robust state initialization and migration when editing or creating new themes.
- **Tooling**: Added Crowdin configuration settings for the IDE.
* feat(widget): add favorites, recommended filtering, and update notice (#128)
- **Widget Picker**:
- Implemented a "Recommended" filter to highlight 1x4 and 2x4 layouts compatible with the Island size.
- Added a Favorite system for widget apps, allowing users to star apps for top-level sorting.
- Added a floating navigation bar to switch between Recommended and All widgets.
- Improved dimension display by converting DP to grid cell proportions (e.g., 4x2).
- **Saved Widgets**:
- Added a persistent "Update Notice" dialog to remind users to check Autostart/Battery permissions after an app update.
- Implemented a "Kill Active Widgets" action to instantly dismiss all running widget overlays.
- Redesigned the UI with a centered filter toolbar (Favorites vs. All) and a dedicated Add FAB.
- Added reactive listening for newly added widgets to ensure the list updates immediately.
- **Service & Data**:
- Enhanced `WidgetOverlayService` with specific `ACTION_KILL_WIDGET` and `ACTION_KILL_ALL_WIDGETS` handlers for immediate UI cleanup.
- Added `favoriteWidgetAppsFlow` to `AppPreferences` to persist starred widget providers.
- **Localization**:
- Added English and Spanish strings for filters, permission reminders, and widget management.
CLOSE #72
* feat(service): implement Native Android 16 Live Updates support
- Added `LiveUpdateTranslator` to transform standard notifications into native Android 16 promoted ongoing tasks using `android.requestPromotedOngoing` and `android.shortCriticalText`.
- Implemented a toggle in Global Settings to choose between custom Xiaomi Islands and native Android 16 Live Updates.
- Created a dedicated `hyper_bridge_live_update_channel` for native updates to manage notifications and visibility independently.
- Added `POST_PROMOTED_NOTIFICATIONS` permission to `AndroidManifest.xml` to support Android 16's promoted notification features.
- Enhanced `LiveUpdateTranslator` with a `generateCriticalShortText` helper to dynamically extract progress percentages or ETAs for the system status chip.
- Fixed Android 16 category filtering by forcing Live Updates into strictly valid categories (`PROGRESS` or `TRANSPORT`) to prevent OS blocking.
- Ensured original notification timestamps are preserved for accurate system-wide "time active" calculations.
- Updated `NotificationReaderService` to support hot-swapping between custom injection and native updates via shared preferences.
* feat(service): enhance Live Update processing and Android 16 compatibility
- **NotificationReaderService**: Implemented a more robust content hashing algorithm that tracks Title, Text, Progress (max, current, indeterminate), and Button Action states (e.g., Play to Pause transitions) to ensure accurate UI updates.
- **LiveUpdateTranslator**:
- Added support for large icons and picture extras in live updates.
- Improved media notification handling by suppressing progress bars that often persist in the background.
- Optimized category mapping (Progress vs. Transport) to comply with Android 16 ongoing promotion requirements.
- Updated `shortCriticalText` generation logic to prioritize media titles, percentages, or time ETAs for the Dynamic Island chip.
- Switched to native Android 16 APIs for promoted ongoing requests and short critical text injection.
* feat(settings): refactor Global Settings UI and update version
- Updated `versionName` to `0.5.0-alpha01`.
- Redesigned `GlobalSettingsScreen` to use consistent card-based layouts for all settings items.
- Replaced the "Native Live Updates" `ListItem` with a new standardized `SettingsSwitchItem` component.
- Improved UI spacing and styling across the settings screen, including unified icon background treatments.
- Simplified state management and click behavior for toggleable settings.
* Update changelog resource to v0.4.2
* build: update project dependencies and gradle configuration (#134)
- **Gradle & AGP**:
- Upgraded Android Gradle Plugin (AGP) from `8.13.2` to `9.1.0`.
- Upgraded Gradle wrapper from `8.13` to `9.3.1`.
- Added `foojay-resolver-convention` plugin to `settings.gradle.kts` for automated toolchain management.
- **Dependencies**:
- Updated Kotlin to `2.3.20` and KSP to `2.3.2`.
- Updated Compose BOM to `2026.03.00` and Material3 to `1.5.0-alpha15`.
- Updated various AndroidX libraries: `core-ktx` (`1.18.0`), `datastore-preferences` (`1.2.1`), `activity-compose` (`1.13.0`), and `ui` (`1.10.5`).
- Bumped `kotlinx-serialization-json` to `1.10.0`.
- **Build Configuration**:
- Removed `kotlin-android` plugin in favor of `kotlin-compose` and built-in Kotlin support.
- Removed explicit `jvmTarget` configuration in `app/build.gradle.kts`.
- Enabled several experimental/new Android Gradle properties in `gradle.properties`, including `android.builtInKotlin` and `android.newDsl`.
- **Optimization**:
- Updated `proguard-rules.pro` to strip Debug and Info logs in release builds using AGP 9.1+ syntax.
* Feature/ux iprovement global configuration (#137)
* feat(settings): redesign Global Settings and enhance Theme Engine
- **Global Settings UI**:
- Refactored `GlobalSettingsScreen` to use a card-based list layout with standardized `ListOptionCard` components.
- Split settings into dedicated screens: `EngineSettingsScreen`, `IslandSettingsScreen`, and `NavCustomizationScreen`.
- Added an "Engine" section to toggle between "Xiaomi Featured Design" and "Native Android Live Updates".
- Implemented a more granular "Island Behavior" configuration including an improved auto-hide timeout slider with snap points (1s to 1h).
- **Theme Engine**:
- Integrated "Engine", "Island Behavior", and "Notification Types" selection directly into the Theme Creator flow.
- Added a real-time `EnginePreview` component to visualize differences between Xiaomi and Native Live Update designs.
- Added a "Notification Types" manager to toggle triggers for Standard, Progress, Media, Navigation, Call, and Timer events.
- **Functionality & Fixes**:
- **Crash Fix**: Resolved a critical issue causing the app to crash when opening settings in Traditional Chinese.
- Updated `AppPreferences` to support persistent storage for the native notification engine toggle.
- Improved `ThemeViewModel` with a more efficient bitmap scaling method for theme thumbnails.
- Updated `StandardTranslator`, `NavTranslator`, and `ProgressTranslator` to better respect global timeout and dismissal configurations.
- Set the default notification shade visibility to `false` for a cleaner "Island-only" experience by default.
- **Localization**:
- Added extensive string resources for version 0.4.2 hotfix and new engine settings across multiple languages (SK, PL, RU, ZH-TW, TR, UK, PT-BR, CS, DE, HU, IN, IT, KO, ES).
- Fixed several translation issues in Traditional Chinese and Spanish.
* feat(settings): externalize and localize auto-hide island strings
- **UI**: Refactored `IslandSettingsControl.kt` to use `stringResource` instead of hardcoded strings for the Auto-hide Island feature.
- **Localization**:
- Added new string resources (`auto_hide_island`, `hides_after_a_set_time`, `behavior_hide_desc`, `behavior_desc_hide_long`) to multiple locales: Slovak, Polish, Russian, Traditional Chinese, Turkish, Ukrainian, Portuguese (Brazil), Czech, German, Indonesian, Italian, Korean, Spanish, and Hungarian.
- Updated Spanish translations for behavior and trigger settings.
- Standardized ellipsis usage from `…` to `...` in Portuguese (Brazil) and Korean `strings.xml`.
* build: update version and remove redundant material3 dependency
- Updated `versionName` to `0.5.0-alpha02`.
- Removed explicit `androidx-material3` dependency from `libs.versions.toml` and `app/build.gradle.kts` (now managed via Compose BOM).
* feat(localization): add French (France) translation
- Added complete French localization (`strings.xml`) for the entire application.
- Translated all core UI components, including onboarding, settings, library, and about screens.
- Localized complex features such as the Theme Engine, Backup & Restore, and the new Notification Engine settings.
- Provided French translations for all historical changelogs (v0.1.0 through v0.4.2).
- Included localized string arrays for specific notification keyword matching (navigation, calls, and progress updates).
* feat(theme): implement layered app-specific behavior overrides
- **Notification Service**:
- Implemented a layered resolution system (Theme > App > Global) for notification triggers and engine selection.
- Updated `NotificationReaderService` to respect theme-managed app configurations and simplified processing logic.
- Enhanced `LiveUpdateTranslator` to support themed colors and icons for the Native Live Update engine, specifically for navigation events.
- Improved title and text resolution with smarter fallback mechanisms and content hashing for updates.
- **Theme Engine & Editor**:
- Added app-specific behavior overrides to the Theme Creator, allowing per-app configuration of notification types and engine selection (Xiaomi vs. Native).
- Integrated "Engine", "Island Behavior", and "Notification Types" into the App Theme Editor.
- Added `AppNotificationTypesEditor` for granular control over which event types (Standard, Media, Nav, etc.) trigger the island for specific apps.
- Refactored `EngineThemeContent` to support an optional "Use Global Default" state.
- **UI & Settings**:
- Redesigned the "Notification Types" settings using `SettingsToggleCard` for a more consistent card-based look.
- Added a theme-managed warning to the App Settings dialog when behaviors are controlled by an active theme.
- Externalized and localized several strings related to triggers and engine management.
- Added French language support to the locale selector.
- **Technical Improvements**:
- Introduced `ConfigResolver` for fast, in-memory resolution of effective configurations for translators.
- Updated `AppPreferences` and `ThemeRepository` to support persistent storage of per-app engine and trigger overrides.
- Standardized navigation layout resolution to respect layered preferences (Global Fallback vs. App-Specific).
* feat(localization): add trigger configuration and override strings
- **Localization**: Added new string resources for "Active Triggers", "Trigger Override", and "Notification Engine" configuration across multiple locales (SK, PL, RU, FR, ZH-TW, TR, UK, PT-BR, CS, DE, IN, IT, KO, HU).
- **Strings**:
- Added descriptions for notification types: Standard, Progress, Media, Navigation, Call, and Timer.
- Added UI strings for global trigger settings and per-app overrides.
- Added `managed_by_theme_desc` and `use_global_default_engine` to support theme-based behavior logic.
- **Cleanup**: Removed unused mock engine strings (`engine_mock_charging`, `engine_mock_percentage`, etc.) from all translated `strings.xml` files.
* feat(theme): enhance engine resolution and theme-persistent configuration (#147)
- **Notification Service**:
- Refactored `getEffectiveEngine` in `NotificationReaderService` to implement a 4-tier resolution priority: Theme App Override > User App Override > Theme Global Override > User Global Fallback.
- **Theme Engine & Editor**:
- **Global Settings**: Added `use_native_live_updates` to `GlobalConfig` model, allowing themes to define a default engine for all apps.
- **Persistence**: Fixed an issue where changing the engine via the UI while a custom theme was active would not persist; it now correctly patches the active Theme JSON.
- **Theme Creator**:
- Updated `ThemeViewModel` to correctly load and save the global engine selection to the Theme JSON.
- Fixed a bug where existing call configurations were wiped when updating app-specific live update settings.
- Improved state management by making `themeUseNativeLiveUpdates` nullable to support inheritance.
- **UI & Settings**:
- **Engine Settings**: Refactored `EngineSettingsScreen` to use `AppPreferences` and `ThemeRepository` instead of raw `SharedPreferences`.
- Integrated real-time engine updates by sending `ACTION_RELOAD_THEME` to the background service upon configuration changes.
- Standardized UI spacing and typography in the Engine selection screen.
- **Bug Fixes**:
- Fixed a bug in `AppListViewModel` where app-specific engine overrides were not being saved to the active theme if the override didn't already exist.
- Corrected `@StringRes` annotation usage in `ShapeOption` enum.
* build: update version to 0.5.0-alpha03 and update localizations
- **Build**: Bumped `versionCode` to `17` and `versionName` to `0.5.0-alpha03` in `app/build.gradle.kts`.
- **Localization**:
- **French (FR)**: Completed translations for the "Engine" configuration, "Island Behavior", and "Active Triggers" sections. Updated backup and app configuration strings.
- **Italian (IT)**: Translated the v0.4.2 changelog, engine settings, color labels, and navigation arrival keywords.
- **Traditional Chinese (ZH-TW)**: Fully localized the new Engine, Behavior, and Trigger settings. Refined onboarding and permission strings (e.g., "HyperIsland" to "超級島").
- **Hungarian (HU)**: Localized engine settings, behavior controls, trigger descriptions, and the v0.4.2 hotfix changelog. Updated call-related keywords.
- **Portuguese (Brazil)**: Translated the v0.4.2 changelog, color labels, and UI state strings (Recommended, Favorites, All). Updated navigation arrival keywords.
- **Strings**: Standardized engine configuration keys across all modified locales to support the transition between "Xiaomi Featured Design" and "Native Android Live Updates".
* loc(fr): fix apostrophe escaping in French strings
- Updated `island_behavior_desc` and `engine_timeouts_triggers` in the French (`fr-rFR`) `strings.xml` to properly escape apostrophes, ensuring correct XML parsing and display.
* Feature/nav3 migration (#152)
* build: integrate Navigation 3 and refactor app routing
- **Dependencies**: Added `androidx.navigation3` (runtime and UI) and `androidx-lifecycle-viewmodel-navigation3` to `libs.versions.toml` and `app/build.gradle.kts`.
- **Navigation Framework**:
- Introduced `NavigationState` to manage persistent back stacks for top-level routes (Onboarding and Home).
- Implemented a `Navigator` class to abstract forward and backward navigation logic using Navigation 3 APIs.
- Replaced the manual `Screen` enum and `AnimatedContent` router with a `@Serializable` sealed interface and `NavDisplay`.
- **Routing & State**:
- Refactored `MainActivity` to use `entryProvider` for screen definitions, simplifying navigation calls across the app.
- Updated screen transitions to leverage `NavDisplay`'s built-in handling of the back gesture and entry management.
- Improved data passing between screens (e.g., `ImportPreview` and `NavCustomization`) by using type-safe classes in the `Screen` sealed interface.
- **Models**: Added `@Serializable` annotations to backup-related models (`HyperBridgeBackup`, `BackupMetadata`, `AppSettingBackup`) to support type-safe navigation arguments.
- **UI**: Standardized back-button behavior across all settings and configuration screens to use the new `Navigator.goBack()` method.
* refactor(navigation): modularize and relocate navigation logic
- **Navigation Architecture**:
- Created a new `com.d4viddf.hyperbridge.ui.navigation` package to house navigation-related components.
- Moved `Screen` definition from `MainActivity.kt` to its own file `Screen.kt`, maintaining `@Serializable` support for all application routes.
- Relocated `NavigationState.kt` and `Navigator.kt` to the new navigation package and updated their package declarations accordingly.
- **MainActivity**:
- Cleaned up imports and removed inline `Screen` interface definition to use the new modularized navigation components.
- Updated references to use `toEntries` and other navigation extension functions from the new package.
* refactor(navigation): modularize nav graph and enhance back press handling
- **Navigation**:
- Extracted the main navigation graph from `MainActivity.kt` into a new standalone `mainNavGraph` function in `NavGraph.kt` to improve maintainability.
- Updated `Navigator.goBack()` to return a boolean indicating if the back event was handled, allowing for better control over app exit logic.
- Integrated `BackHandler` in `MainActivity` to properly handle system back gestures and call `onExit` when at the root of the navigation stack.
- **MainActivity**:
- Refactored `MainRootNavigation` into smaller, more focused composables (`MainRootNavigation` and `MainNavigationContent`).
- Improved the initialization flow to show a `CircularProgressIndicator` while the setup status is being loaded from preferences.
- Simplified the logic for switching between `Onboarding` and `Home` routes based on the setup completion state.
- **UI**: Standardized back navigation behavior across all screens, ensuring consistent exit behavior from the `Info` screen.
* feat(ui): implement custom navigation transitions and predictive back support
- **Navigation**:
- Added custom `transitionSpec` and `popTransitionSpec` using horizontal slides and fades for smoother screen transitions.
- Implemented `predictivePopTransitionSpec` to support Android's predictive back gesture with scaling and transform origin adjustments based on swipe edge.
- Dynamically updated `topLevelRoutes` to prevent navigating back to the onboarding flow once setup is complete.
- **UI**:
- Applied a rounded corner clip (32.dp) to the main `NavDisplay` container.
- Optimized transition durations to 400ms for a more fluid user experience.
* chore: code cleanup, dependency update, and UI refinements
- **UI & UX**:
- **Widget Picker**: Refactored `WidgetPickerScreen` and removed `WidgetPickerViewModel.kt` to streamline widget selection logic. Updated layout filtering to consistently use `targetCellWidth` and `targetCellHeight`.
- **Theme Creator**: Improved navigation and state management in `ThemeCreatorScreen` and `AppThemeEditorScreen`. Enhanced UI consistency for color and icon selection previews.
- **Licenses**: Updated `LicensesScreen` to dynamically load a sorted list of open-source libraries and externalized the screen title string.
- **Components**: Standardized `EmptyState` component parameters and improved layout logic in `WidgetConfigScreen` and `SavedAppWidgetsScreen`.
- **Localization**:
- Added `open_source_licenses` string resource.
- Refactored `WidgetConfigScreen` to use localized toast and error messages.
- **Translators**:
- **Standard**: Cleaned up title resolution logic in `StandardTranslator`.
- **Progress**: Updated `ProgressTranslator` to hide redundant percentage text in specific circular progress views.
- **Internal Improvements**:
- **Compatibility**: Removed redundant SDK version checks for `Parcelable` handling and notification permissions, favoring modern Android APIs.
- **Utilities**: Switched to `androidx.core` extension functions for `Bitmap` creation and `Uri` parsing.
- **Persistence**: Updated `SavedAppWidgetsScreen` to use the `edit { }` DSL for `SharedPreferences`.
- **Data**: Refined theme ID generation in `ThemeRepository` and improved rule engine comments.
- **Bug Fixes**:
- Fixed a potential nullability issue in `AppListViewModel` when patching theme configurations.
- Corrected `IslandConfig` initialization in the App Settings dialog.
* Feature/duplication toggle (#153)
* feat(settings): enhance island behavior with heads-up timeout and notification management
- **Island Configuration**:
- Introduced `floatTimeout` to `IslandConfig` to control the duration of heads-up popups.
- Added `removeOriginalNotification` toggle to allow automatic dismissal of source notifications when a Live Update is active.
- Updated `AppPreferences` and `AppSettings` to support persistent storage for these new global and per-app settings.
- **UI & Settings**:
- **Island Settings**: Redesigned the "Heads-up Popup" configuration with an animated slider for granular timeout control (1s to 30s).
- **Live Updates**: Added a new section in `IslandSettingsControl` to manage the removal of original notifications.
- Improved UI scrollability in `IslandSettingsScreen` and added several Compose previews for better development feedback.
- Refactored toggle logic to ensure consistency between heads-up and timeout states.
- **Translators**:
- Updated all translators (`Media`, `Call`, `Progress`, `Standard`, `Nav`, `Timer`) to respect the new `expandedTimeMs` (heads-up timeout) configuration.
- Cleaned up redundant theme color application logic in `TimerTranslator`.
- **Localization**:
- Added string resources for "Live updates", "Remove original notification", and "Heads-up popup" timeout descriptions.
* feat(service): implement notification timeout and original notification removal
- **Notification Service**:
- Introduced a `handlePostNotificationSideEffects` method to manage post-processing logic for translated notifications.
- Added support for automatically cancelling the original system notification when `removeOriginalNotification` is enabled in the configuration (excluding Media notifications). fixes #21, fixes #120, fixes #146
- Implemented a timeout mechanism using coroutines to automatically dismiss translated notifications after a configured duration.
- Added `intentionallyRemovedKeys` tracking to prevent recursive processing loops when the service cancels a system notification.
- Improved cleanup logic in `onNotificationRemoved` and `cleanupCache` to ensure associated timeout jobs are cancelled and resources are freed.
- **Bug Fixes**:
- Added missing cancellation of existing processing jobs and timeout jobs when a notification is removed or updated.
- **Logic Improvements**:
- Improved design logic for new notification parameters ( closes #99) and add theme configuration ( closes #107)
* fix(service): refine notification timeout logic and dismissal behavior
- **Notification Service**:
- Modified `onStartCommand` to use non-nullable `Intent` parameters for better type safety.
- Updated notification removal logic to ensure the source notification is no longer canceled when the Island notification is dismissed or times out.
- Refactored `handlePostNotificationSideEffects` to only apply timeout scheduling to "Live Update" notifications.
- Improved cache cleanup and logging during notification dismissal to prevent unintended side effects on source notifications.
* Feature/onboarding screen (#155)
* feat(ui): redesign onboarding flow and expand configuration options
- **Onboarding Experience**:
- Significantly expanded the onboarding process from 7 to 11 pages, incorporating new configuration steps for triggers, engine selection, and island behavior.
- Redesigned the `OnboardingScreen` layout with improved navigation controls, including a dedicated "Back" button and a more prominent "Get Started" entry point.
- Added several new pages: `TriggersConfigPage` (active notification types), `PriorityEducationPage` (island limit behavior), `BehaviorConfigPage` (notification removal and shade visibility), `CustomizationPage` (themes and widgets), and `EngineConfigPage` (Xiaomi vs. Native Live Updates).
- Integrated `ModalBottomSheet` for granular settings configuration directly within the onboarding flow.
- Added extensive Compose previews for all onboarding pages to improve development feedback.
- **Data & Logic**:
- **AppPreferences**: Implemented a migration check to force an onboarding reset for users upgrading to version 0.5.0 (versionCode 17).
- **MainActivity**: Removed the standalone `PriorityEducationDialog` as its logic is now integrated into the onboarding flow.
- **Build Configuration**: Enabled `buildConfig` in `app/build.gradle.kts` and updated the Kotlin serialization plugin version.
- **Localization & Resources**:
- **Strings**:
- Standardized and updated terminology across multiple languages (ES, HU, IT, ZH-TW, SK, CS, TR, DE, PT, KO, FR, UK, RU, ID).
- Added new string resources for the expanded onboarding descriptions, "Live updates" management, and "Remove original notification" toggles.
- Refined French strings to ensure proper apostrophe escaping and formatting.
- Simplified and updated battery optimization instructions ("Allow Autostart", "Disable Restrictions").
- **Icons**: Switched to modern Rounded/Filled icon variants for a more consistent visual style across the setup pages.
* feat(onboarding): implement auto-hide configuration page
- **Onboarding Flow**:
- Expanded the onboarding process from 11 to 13 pages to include more granular behavior settings.
- Introduced a dedicated `AutoHideConfigPage` to manage island timeout settings during initial setup.
- Updated `BehaviorConfigPage` description to reflect its role in global configuration.
- **UI & Components**:
- Added an interactive "Auto-hide Island" section with a `Switch` and an `AnimatedVisibility` slider for timeout selection.
- Exposed `timeoutSteps` and `formatSeconds` from `IslandSettingsControl.kt` to be reusable across the onboarding flow.
- Integrated `Icons.Rounded.Timer` and `Icons.Default.AccessTime` for improved visual cues in behavior settings.
- **State Management**:
- Updated `OnboardingScreen` to persist `IslandConfig` timeout and `isFloat` preferences directly through `AppPreferences`.
- **Localization**:
- Added `behavior_desc_glob_config` string resource for better description of island behavior settings.
* chore: bump version to 0.5.0-beta01 and refine onboarding navigation
- **Build**:
- Updated `versionCode` to `18` and `versionName` to `0.5.0-beta01` in `app/build.gradle.kts`.
- **Navigation**:
- **Navigator**: Introduced `finishOnboarding` to explicitly handle the transition from onboarding to the home screen by ensuring the home stack is initialized before clearing the onboarding route, preventing potential app closure.
- **MainActivity**: Refactored `navigationState` initialization to use a persistent set of all possible top-level routes and a fixed initial start route.
- **NavGraph**: Updated setup completion logic to use the new `navigator.finishOnboarding` method and removed redundant priority education flags.
- **Onboarding**:
- Refactored `OnboardingScreen` to dynamically determine the last page using `pagerState.pageCount` instead of a hardcoded index.
- **Data & Persistence**:
- **AppPreferences**: Updated the onboarding reset logic to target `versionCode 18` for version 0.5.0, forcing a setup reset for users on older builds.
* chore: general cleanup, bug fixes, and maintenance
- **UI & Components**:
- Removed several legacy dialogs and components: `ChangelogDialog`, `PriorityEducationDialog`, and `DeviceCompatibility`.
- Simplified `CommonCards.kt` by removing `ExpandableOptimizationCard` and localized logic.
- Refactored `HyperOSColorPicker` to remove unused `ColorBubble` component.
- Added `verticalScroll` to `ThemeSettingsContent` to improve navigation on smaller screens.
- Updated `IslandSettingsControl` and `IslandSettingsScreen` to use explicit `IslandConfig` initialization.
- Added a content description for the widget snapshot layout.
- **Notification Service & Logic**:
- Refactored `NotificationReaderService` and `LiveUpdateTranslator` to improve string handling and reliability when resolving notification titles/text.
- Enhanced exception handling across the service and data layers using suppressed exceptions (`catch (_: Exception)`) to reduce log noise.
- Improved app label caching and package filtering logic.
- **Data & Persistence**:
- Cleaned up `AppPreferences` by removing the unused `isPriorityEduShown` flow.
- Refactored `ThemeRepository` to use a lowercase tag and improved error logging for theme installation and resource loading.
- Updated `AppCacheManager` and `WidgetPickerScreen` for better state management and type safety.
- **Resources & Localization**:
- **Icons**: Added monochrome support to adaptive launcher icons and moved them to the standard `mipmap-anydpi` directory.
- **Strings**:
- **PT-BR**: Updated widget ID formatting.
- **FR**: Simplified application configuration descriptions.
- **General**: Added `widget_layout_snapshot` resource.
- **Build & System**:
- Updated Gradle wrapper from `9.3.1` to `9.4.1`.
- Refactored `DeviceUtils` for cleaner system property checks.
- Updated `AndroidManifest` with improved package visibility policy ignores.
- Cleaned up `ThemeInstallerActivity` by removing redundant SDK version checks for dynamic color schemes.
* chore(manifest): add notification listener permission
- **Permissions**:
- Added `BIND_NOTIFICATION_LISTENER_SERVICE` to `AndroidManifest.xml` to support notification listener capabilities.
- Applied `tools:ignore="ProtectedPermissions"` to acknowledge the use of the protected system permission.
* fix(service): refine progress notification behavior and navigation state (#157)
- **Notification Service & Translators**:
- **ProgressTranslator**:
- Modified `translate` to accept an `isUpdate` flag.
- Restricted "floating" behavior to the initial notification appearance only, preventing subsequent progress updates from re-triggering the float animation.
- Refined `BigIslandInfo` layout for finished progress states by simplifying `ImageTextInfoLeft` and `ImageTextInfoRight` parameters (removing redundant text info).
- Removed redundant `setIslandFirstFloat` call.
- **NotificationReaderService**: Updated `PROGRESS` type handling to pass the `isUpdate` state to the translator.
- **UI & Navigation**:
- **MainActivity**: Added `isSetupComplete` as a key to `remember` blocks for `initialStartRoute` and `allPossibleTopLevel` to ensure navigation state correctly reacts to setup completion changes.
Closes #156
* feat(boot): add support for Direct Boot (#159)
- **Boot Receiver**:
- Updated `BootReceiver.kt` to handle `ACTION_LOCKED_BOOT_COMPLETED`, allowing the app to respond to boot events before the first user unlock.
- Added `android.intent.action.LOCKED_BOOT_COMPLETED` to the `BootReceiver` intent filter in `AndroidManifest.xml`.
fixed #158
* chore(localization): update and expand string resources across multiple languages
- **Strings & Localization**:
- **General**: Added new string resources `behavior_desc_glob_config` (island behavior description) and `widget_layout_snapshot` (widget preview) across all supported locales.
- **Spanish (ES)**: Added translations for floating notification timeouts, open source licenses, live updates, and original notification removal.
- **French (FR)**: Fully localized several strings including floating notification settings, live updates, onboarding descriptions, and behavior configurations.
- **Traditional Chinese (ZH-TW)**: Localized onboarding descriptions for widgets, themes, and customization options.
- **Portuguese (PT-BR)**: Simplified the `widget_id_fmt` string from "Widget #%d" to "Widget".
- **New Keys**: Added placeholder or translated entries for `setting_float_timeout_desc`, `open_source_licenses`, `live_updates`, and `remove_original_notification` logic in relevant resource files.
* chore(changelog): update for version 0.5.0-beta
- **UI & Navigation**:
- Updated `ChangelogHistoryScreen` to include version `0.5.0` at the top of the history list.
- Updated `MainActivity` to display the `0.5.0-beta` changelog in the main `ChangelogSheet`.
- **Resources & Localization**:
- Added `changelog_0_5_0_beta` string containing release notes for Native Live Updates, HyperOS CN support, new Theme Engine features (HSL selector, behavior controls), and system fixes.
- Added `title_0_5_0` string resource ("The Live Update").
* chore: update version metadata to 0.5.0
- **Changelog**:
- Updated `ChangelogHistoryScreen` to mark version `0.5.0` as the latest release and downgraded `0.4.2` to historical status.
- **Application Versioning**:
- Updated the fallback `currentVersionName` in `MainActivity` from `0.4.2` to `0.5.0`.
* refactor(service): refine floating behavior for progress notifications (#164)
- **Notification Logic**:
- Updated `ProgressTranslator` to restrict the floating behavior (`setEnableFloat`) to only the initial appearance of a notification, preventing repeated floats during updates.
- Removed the redundant `setIslandFirstFloat` call from the builder.
close #163
* Fix/162 bug crash unable to start service with null (#165)
* fix(service): handle nullable intent in onStartCommand
- **Notification Service**:
- Updated `onStartCommand` signature to accept a nullable `Intent` to prevent potential crashes when the service is restarted by the system.
- Implemented null-safe checks for `intent.action` when processing test widgets and theme reloads.
fixed #162
* feat(service): implement localized error messages for persistent notifications
- **Notification Service**:
- Updated `NotificationReaderService` to use localized string resources for fallback content text in both standard and widget notifications.
- Replaced the hardcoded "Active Island" text with `R.string.notification_went_wrong`.
- Replaced the hardcoded "Active" widget text with `R.string.widget_went_wrong`.
- **Resources**:
- Added `notification_went_wrong` and `widget_went_wrong` string resources to provide clearer feedback when notifications or widgets fail to render correctly.
* chore: update changelog string formatting
- **Resources**:
- Refactored `changelog_0_5_0_beta` in `strings.xml` to use HTML `` tags instead of markdown-style bolding for better compatibility with Android string rendering.
- Improved indentation and formatting of the changelog text within the resource file.
* chore: bump version to 0.5.0-beta02 and refine changelog trigger logic
- **Build**:
- Updated `versionCode` to `19` and `versionName` to `0.5.0-beta02` in `app/build.gradle.kts`.
- **Navigation & Lifecycle**:
- **NavGraph**: Removed redundant `setLastSeenVersion` call during onboarding completion to centralize version tracking.
- **MainActivity**:
- Refined the changelog display logic to prevent it from appearing immediately after initial onboarding.
- Improved `lastSeenVersion` state handling by using a more reliable initial value (`-1`) to ensure version comparisons only trigger after the preference is properly loaded.
- Introduced `isInitiallySetup` check to distinguish between fresh installations and application updates.
* chore: update localizations and refine progress translation logic
- **Localization & Resources**:
- **Spanish (ES)**: Translated several core strings, including "Live updates," "Open source licenses," and "Island behavior" descriptions.
- **Indonesian (ID)**: Extensively updated translations for UI controls, navigation keywords (arrival, destination), and system permission descriptions.
- **Italian (IT)** and **Traditional Chinese (ZH-TW)**: Completed translations for onboarding descriptions and behavior settings.
- **General**: Added the `0.5.0-beta` changelog, version title, and error fallback strings (`notification_went_wrong`, `widget_went_wrong`) across all supported languages (ES, HU, IT, ZH-TW, SK, CS, TR, DE, PT, KO, FR, UK, PL, RU, ID).
- **Notification Service**:
- **ProgressTranslator**: Refactored `setIslandConfig` in the progress update logic to only apply `expandedTimeMs` (float timeout) if the floating notification setting is explicitly enabled, improving consistency with user preferences.
* chore(manifest): remove redundant notification listener permission
- **Permissions**:
- Removed the `BIND_NOTIFICATION_LISTENER_SERVICE` permission from `AndroidManifest.xml`.
- Removed the associated `tools:ignore="ProtectedPermissions"` attribute.
* Fix WidgetManager initialization and Shizuku implementation
Closes #97
Closes #119
Closes #141
Closes #177
* chore(manifest): remove redundant notification listener permission
- **Permissions**:
- Removed the `BIND_NOTIFICATION_LISTENER_SERVICE` permission from `AndroidManifest.xml`.
- Removed the associated `tools:ignore="ProtectedPermissions"` attribute.
* chore: bump version to 0.5.0-beta04 and refine Shizuku workaround
- **Build**:
- Updated `versionCode` to `21` and `versionName` to `0.5.0-beta04` in `app/build.gradle.kts`.
- **Shizuku & Notifications**:
- **ShizukuManager**: Refactored the `notify` logic to improve the reliability of the XMSF network workaround on MIUI/HyperOS.
- Introduced a `Mutex` to prevent race conditions during concurrent notification dispatches.
- Added a 50ms delay after disabling XMSF to ensure the network state change takes effect.
- Implemented a 1-second delayed restoration of XMSF network, which is reset by subsequent notifications to prevent premature re-enabling.
- Added `@RequiresPermission` annotation for `POST_NOTIFICATIONS`.
- **Resources & Localization**:
- **Changelog**: Updated `changelog_0_5_0_beta` to include information regarding CN Support and the new Shizuku workaround.
- **Traditional Chinese (ZH-TW)**: Refined the `enable_autostart` string for better clarity ("開啟後台啟動管理").
* Feature/permanent island (#179)
* feat: implement Permanent Island feature and enhance Shizuku notification handling
- **Permanent Island**:
- Introduced a "Permanent Island" feature that keeps a placeholder Dynamic Island visible when no active notifications are present.
- Added `PermanentIslandManager` to coordinate visibility based on the `activeIslands` count within `NotificationReaderService`.
- Added `isPermanentIslandEnabledFlow` and `permanentIslandWidthFlow` to `AppPreferences` to persist user configuration.
- Implemented `PermanentIslandConfigPage` in the onboarding flow and a dedicated settings screen.
Closes #94
- **Shizuku & Notifications**:
- **ShizukuManager**: Added a `cancel` function that applies the XMSF network workaround (disabling/enabling XMSF network with a delay) when programmatically removing notifications to ensure consistency on MIUI/HyperOS.
- **NotificationReaderService**: Integrated `PermanentIslandManager` to trigger updates whenever notifications are posted or removed.
- **Lifecycle & System**:
- **BootReceiver**: Added `ACTION_MY_PACKAGE_REPLACED` to the intent filter in `AndroidManifest.xml` and handled it in the receiver to ensure the service restarts correctly after app updates.
- **UI & Navigation**:
- **GlobalSettingsScreen**: Added an entry point for Permanent Island settings and updated the expressive shape logic to accommodate the additional list item.
- **NavGraph**: Defined the route and destination for `PermanentIslandConfig`.
- **Previews**: Updated `@Preview` annotations across onboarding components to use `apiLevel = 36`.
- **Resources & Localization**:
- Added strings for "Permanent Island" titles, descriptions, and width controls in English and Spanish (ES, ES-rEM).
- Refined the `shizuku_workaround_title` string for better clarity.
* feat: implement Permanent Island feature and configuration UI
- **Permanent Island Logic**:
- Introduced `PermanentIslandManager` to manage a persistent, "empty" island state when no other active notifications are present.
- Implemented logic to automatically show/hide the permanent island based on the current notification count and user preferences.
- Utilizes `ShizukuManager` to dispatch a non-dismissible, low-priority notification that uses non-breaking spaces (`\u00A0`) to simulate variable island widths.
- **Settings UI**:
- Created `PermanentIslandConfigScreen` providing users with a toggle to enable the feature and a slider to adjust the island's width.
- Integrated `AppPreferences` flows to reactively update the UI and service state.
- **Components**:
- Developed `PermanentIslandPreview`, a Compose component that provides a real-time visual mock-up of the island's appearance on a status bar, featuring smooth width animations via `animateDpAsState`.
* docs: Add permanent island feature to changelog
* chore: Update .gitignore to exclude build artifacts and IDE configs
* chore: Update .gitignore to standard Android Jetpack Compose template
* chore: initialize project configuration, dependency versions, and ignore rules
* feat: introduce dedicated Download notification type and refine progress parsing (#184)
- **Notifications & Services**:
- **NotificationType**: Added a new `DOWNLOAD` type to separate file transfers from general `PROGRESS` tasks. Close #174
- **NotificationReaderService**:
- Integrated `DownloadTranslator` to handle download-specific notification logic.
- Implemented `isDownloadNotification` and `hasProgressNotification` to smarter distinguish between types based on package names, channel IDs, and keywords.
- Added logic to implicitly enable `DOWNLOAD` if `PROGRESS` is active for a specific app to ensure backward compatibility.
- **Translators**:
- **BaseTranslator**: Introduced `extractTextPercentage` to parse progress values (e.g., "12%") from notification titles or text.
- **ProgressTranslator**: Updated to support text-based percentages and refined indeterminate state handling.
- **LiveUpdateTranslator**: Added fallback to text-based percentage parsing for system-native animations.
- **CallTranslator**: Switched to `getCharSequence` for retrieving notification titles for better compatibility.
- **Data & Migration**:
- **AppPreferences**: Implemented a migration routine (`download_type_migration_complete`) that automatically adds the `DOWNLOAD` type to global and app-specific settings if `PROGRESS` was previously enabled.
- **UI & Resources**:
- **Theme & Onboarding**:
- Updated `ThemeSettingsContent` and `OnboardingScreen` to reflect the split between Progress and Downloads.
- Updated icons: `HourglassEmpty` for Progress and `CloudDownload` for Downloads.
- **Localization**:
- Refined strings in English and Spanish to distinguish between "Progress & Tasks" and "Downloads."
- **Changelog**: Updated `changelog_0_5_0_beta` with details regarding CN support, the new Download type, text-based progress support, and smart filtering.
* feat: implement DownloadTranslator and expand configuration resources
- **Translators**:
- Introduced `DownloadTranslator` to provide specialized handling for file transfer notifications, separating them from general progress tasks.
- Implemented logic in `DownloadTranslator` to handle indeterminate states, percentage-based progress, and completion detection via keywords or 100% threshold.
- Added support for custom completion icons ("tick_icon") and automated timeout/dismissible configurations for finished downloads.
- **Resources & Localization**:
- **Permanent Island**: Added strings for titles, descriptions, and width adjustment controls across multiple locales (ID, FR, TR, CS, IT, UK, DE, ZH-TW, PT-BR, RU, SK, ES, PL, KO, HU).
- **Shizuku Workaround**: Added localized strings to guide users through enabling the Shizuku workaround, specifically for improved notification triggering on CN ROMs.
- **Do Not Disturb (DND)**: Introduced strings for managing island visibility during DND, including manual pause and auto-detection of system DND status.
- **Type Refinement**: Updated `type_progress` and `type_download` strings to clearly distinguish between "Progress & Tasks" (ongoing operations) and "Downloads" (file transfers).
- **Changelog**: Updated `changelog_0_5_0_beta` in all languages to detail CN support, native live updates, theme engine improvements (HSL selector), and the new Permanent Island feature.
* Feature/sui permission (#185)
* feat(shizuku): implement support for Sui permissions and detection
* chore(i18n): extract hardcoded onboarding text and add Spanish translations for Shizuku and DND strings
* chore(i18n): translate permanent island strings to Spanish
* Fix/widget permanent island (#186)
* fix(permanent-island): hide permanent island when a widget is active
* feat(ui): add shizuku workaround toggle to engine theme page
* Feature/better icons (#187)
* fix(notifications): tint dark icons, extract messaging avatars, fix update popups
* fix(notifications): allow group summaries with text to trigger islands
* fix(notifications): completely remove GROUP_ALERT_CHILDREN check for summaries
* fix(notifications): ensure sequential execution of cancel and notify to prevent race conditions dropping islands
* fix(notifications): use sender_person key for MessagingStyle avatar extraction
* chore: bump version to 0.5.0-beta05 and update dependencies
- **Build Configuration**:
- Updated `versionCode` to 22 and `versionName` to `0.5.0-beta05`.
- Updated Kotlin serialization plugin to 2.3.21.
- **Dependencies**:
- **Kotlin & Serialization**: Updated Kotlin to 2.3.21 and `kotlinxSerializationJson` to 1.11.0.
- **Compose**: Updated `composeBom` to 2026.05.01 and Compose UI to 1.11.2.
- **Libraries**:
- Updated `gson` to 2.14.0.
- Updated `nav3` to 1.1.2.
---------
Co-authored-by: David Df
* Beta/0 5 0 (#191)
* Feat/color picker (#127)
* feat(theme): implement HSL color picker and enhanced lightness slider
- **New `CustomColorBottomSheet`**: Introduced a dedicated HSL (Hue, Saturation, Lightness) color picker with gradient sliders for precise theme color customization.
- **Enhanced `ColorsDetailContent`**:
- Overhauled the preset tab with an animated lightness slider that appears when a preset is selected.
- Improved the preset selection logic to maintain the "base" hue even when adjusting lightness to extreme white or black.
- Redesigned the "Custom" colors section to use the new HSL bottom sheet instead of a simple hex text field.
- Switched from simple circular bubbles to a more refined UI with selection rings and rounded-square transitions for active colors.
- **Improved Theme Creator State**:
- Implemented strict state clearing (nulling `currentEditingThemeId`) when exiting the creator to prevent state leakage between editing sessions.
- Updated save/apply dialog logic to ensure the UI resets correctly after persistence.
- **Shared Components**: Added `GradientSlider` and `ColorBubble` to `HyperOSColorPicker.kt` for reusable, stylized color selection components.
- **Localization**: Added missing string resources for color labels (Hue, Saturation, Lightness) in English and Spanish.
* #### feat(theme): implement advanced color modes and redesigned color picker
- **Color Modes**: Introduced a `ColorMode` system (`CUSTOM`, `APP_ICON`, `MATERIAL_YOU`) to replace the binary "Use App Colors" toggle. This allows themes to dynamically adapt to Material You system colors or extract colors from app icons globally and per-app.
- **Improved Color Picker**: Replaced standard text fields with a new `CustomColorBottomSheet` in the Call Style and Theme Creator screens, featuring a visual HSL-based selection and hex formatting.
- **UI & UX Refinement**:
- Redesigned `ColorsDetailContent` with a scrollable layout and new sections for dynamic color modes.
- Updated the Theme Creator preview to correctly reflect the active color mode.
- Standardized "My Theme" and "Share Theme" strings to use localized resources.
- **Data Persistence**: Updated `HyperTheme`, `GlobalConfig`, and `AppThemeOverride` models to support the new `ColorMode` enum while maintaining backward compatibility with legacy themes.
- **Theme Creator**: Refactored `ThemeViewModel` to handle more robust state initialization and migration when editing or creating new themes.
- **Tooling**: Added Crowdin configuration settings for the IDE.
* feat(widget): add favorites, recommended filtering, and update notice (#128)
- **Widget Picker**:
- Implemented a "Recommended" filter to highlight 1x4 and 2x4 layouts compatible with the Island size.
- Added a Favorite system for widget apps, allowing users to star apps for top-level sorting.
- Added a floating navigation bar to switch between Recommended and All widgets.
- Improved dimension display by converting DP to grid cell proportions (e.g., 4x2).
- **Saved Widgets**:
- Added a persistent "Update Notice" dialog to remind users to check Autostart/Battery permissions after an app update.
- Implemented a "Kill Active Widgets" action to instantly dismiss all running widget overlays.
- Redesigned the UI with a centered filter toolbar (Favorites vs. All) and a dedicated Add FAB.
- Added reactive listening for newly added widgets to ensure the list updates immediately.
- **Service & Data**:
- Enhanced `WidgetOverlayService` with specific `ACTION_KILL_WIDGET` and `ACTION_KILL_ALL_WIDGETS` handlers for immediate UI cleanup.
- Added `favoriteWidgetAppsFlow` to `AppPreferences` to persist starred widget providers.
- **Localization**:
- Added English and Spanish strings for filters, permission reminders, and widget management.
CLOSE #72
* feat(service): implement Native Android 16 Live Updates support
- Added `LiveUpdateTranslator` to transform standard notifications into native Android 16 promoted ongoing tasks using `android.requestPromotedOngoing` and `android.shortCriticalText`.
- Implemented a toggle in Global Settings to choose between custom Xiaomi Islands and native Android 16 Live Updates.
- Created a dedicated `hyper_bridge_live_update_channel` for native updates to manage notifications and visibility independently.
- Added `POST_PROMOTED_NOTIFICATIONS` permission to `AndroidManifest.xml` to support Android 16's promoted notification features.
- Enhanced `LiveUpdateTranslator` with a `generateCriticalShortText` helper to dynamically extract progress percentages or ETAs for the system status chip.
- Fixed Android 16 category filtering by forcing Live Updates into strictly valid categories (`PROGRESS` or `TRANSPORT`) to prevent OS blocking.
- Ensured original notification timestamps are preserved for accurate system-wide "time active" calculations.
- Updated `NotificationReaderService` to support hot-swapping between custom injection and native updates via shared preferences.
* feat(service): enhance Live Update processing and Android 16 compatibility
- **NotificationReaderService**: Implemented a more robust content hashing algorithm that tracks Title, Text, Progress (max, current, indeterminate), and Button Action states (e.g., Play to Pause transitions) to ensure accurate UI updates.
- **LiveUpdateTranslator**:
- Added support for large icons and picture extras in live updates.
- Improved media notification handling by suppressing progress bars that often persist in the background.
- Optimized category mapping (Progress vs. Transport) to comply with Android 16 ongoing promotion requirements.
- Updated `shortCriticalText` generation logic to prioritize media titles, percentages, or time ETAs for the Dynamic Island chip.
- Switched to native Android 16 APIs for promoted ongoing requests and short critical text injection.
* feat(settings): refactor Global Settings UI and update version
- Updated `versionName` to `0.5.0-alpha01`.
- Redesigned `GlobalSettingsScreen` to use consistent card-based layouts for all settings items.
- Replaced the "Native Live Updates" `ListItem` with a new standardized `SettingsSwitchItem` component.
- Improved UI spacing and styling across the settings screen, including unified icon background treatments.
- Simplified state management and click behavior for toggleable settings.
* Update changelog resource to v0.4.2
* build: update project dependencies and gradle configuration (#134)
- **Gradle & AGP**:
- Upgraded Android Gradle Plugin (AGP) from `8.13.2` to `9.1.0`.
- Upgraded Gradle wrapper from `8.13` to `9.3.1`.
- Added `foojay-resolver-convention` plugin to `settings.gradle.kts` for automated toolchain management.
- **Dependencies**:
- Updated Kotlin to `2.3.20` and KSP to `2.3.2`.
- Updated Compose BOM to `2026.03.00` and Material3 to `1.5.0-alpha15`.
- Updated various AndroidX libraries: `core-ktx` (`1.18.0`), `datastore-preferences` (`1.2.1`), `activity-compose` (`1.13.0`), and `ui` (`1.10.5`).
- Bumped `kotlinx-serialization-json` to `1.10.0`.
- **Build Configuration**:
- Removed `kotlin-android` plugin in favor of `kotlin-compose` and built-in Kotlin support.
- Removed explicit `jvmTarget` configuration in `app/build.gradle.kts`.
- Enabled several experimental/new Android Gradle properties in `gradle.properties`, including `android.builtInKotlin` and `android.newDsl`.
- **Optimization**:
- Updated `proguard-rules.pro` to strip Debug and Info logs in release builds using AGP 9.1+ syntax.
* Feature/ux iprovement global configuration (#137)
* feat(settings): redesign Global Settings and enhance Theme Engine
- **Global Settings UI**:
- Refactored `GlobalSettingsScreen` to use a card-based list layout with standardized `ListOptionCard` components.
- Split settings into dedicated screens: `EngineSettingsScreen`, `IslandSettingsScreen`, and `NavCustomizationScreen`.
- Added an "Engine" section to toggle between "Xiaomi Featured Design" and "Native Android Live Updates".
- Implemented a more granular "Island Behavior" configuration including an improved auto-hide timeout slider with snap points (1s to 1h).
- **Theme Engine**:
- Integrated "Engine", "Island Behavior", and "Notification Types" selection directly into the Theme Creator flow.
- Added a real-time `EnginePreview` component to visualize differences between Xiaomi and Native Live Update designs.
- Added a "Notification Types" manager to toggle triggers for Standard, Progress, Media, Navigation, Call, and Timer events.
- **Functionality & Fixes**:
- **Crash Fix**: Resolved a critical issue causing the app to crash when opening settings in Traditional Chinese.
- Updated `AppPreferences` to support persistent storage for the native notification engine toggle.
- Improved `ThemeViewModel` with a more efficient bitmap scaling method for theme thumbnails.
- Updated `StandardTranslator`, `NavTranslator`, and `ProgressTranslator` to better respect global timeout and dismissal configurations.
- Set the default notification shade visibility to `false` for a cleaner "Island-only" experience by default.
- **Localization**:
- Added extensive string resources for version 0.4.2 hotfix and new engine settings across multiple languages (SK, PL, RU, ZH-TW, TR, UK, PT-BR, CS, DE, HU, IN, IT, KO, ES).
- Fixed several translation issues in Traditional Chinese and Spanish.
* feat(settings): externalize and localize auto-hide island strings
- **UI**: Refactored `IslandSettingsControl.kt` to use `stringResource` instead of hardcoded strings for the Auto-hide Island feature.
- **Localization**:
- Added new string resources (`auto_hide_island`, `hides_after_a_set_time`, `behavior_hide_desc`, `behavior_desc_hide_long`) to multiple locales: Slovak, Polish, Russian, Traditional Chinese, Turkish, Ukrainian, Portuguese (Brazil), Czech, German, Indonesian, Italian, Korean, Spanish, and Hungarian.
- Updated Spanish translations for behavior and trigger settings.
- Standardized ellipsis usage from `…` to `...` in Portuguese (Brazil) and Korean `strings.xml`.
* build: update version and remove redundant material3 dependency
- Updated `versionName` to `0.5.0-alpha02`.
- Removed explicit `androidx-material3` dependency from `libs.versions.toml` and `app/build.gradle.kts` (now managed via Compose BOM).
* feat(localization): add French (France) translation
- Added complete French localization (`strings.xml`) for the entire application.
- Translated all core UI components, including onboarding, settings, library, and about screens.
- Localized complex features such as the Theme Engine, Backup & Restore, and the new Notification Engine settings.
- Provided French translations for all historical changelogs (v0.1.0 through v0.4.2).
- Included localized string arrays for specific notification keyword matching (navigation, calls, and progress updates).
* feat(theme): implement layered app-specific behavior overrides
- **Notification Service**:
- Implemented a layered resolution system (Theme > App > Global) for notification triggers and engine selection.
- Updated `NotificationReaderService` to respect theme-managed app configurations and simplified processing logic.
- Enhanced `LiveUpdateTranslator` to support themed colors and icons for the Native Live Update engine, specifically for navigation events.
- Improved title and text resolution with smarter fallback mechanisms and content hashing for updates.
- **Theme Engine & Editor**:
- Added app-specific behavior overrides to the Theme Creator, allowing per-app configuration of notification types and engine selection (Xiaomi vs. Native).
- Integrated "Engine", "Island Behavior", and "Notification Types" into the App Theme Editor.
- Added `AppNotificationTypesEditor` for granular control over which event types (Standard, Media, Nav, etc.) trigger the island for specific apps.
- Refactored `EngineThemeContent` to support an optional "Use Global Default" state.
- **UI & Settings**:
- Redesigned the "Notification Types" settings using `SettingsToggleCard` for a more consistent card-based look.
- Added a theme-managed warning to the App Settings dialog when behaviors are controlled by an active theme.
- Externalized and localized several strings related to triggers and engine management.
- Added French language support to the locale selector.
- **Technical Improvements**:
- Introduced `ConfigResolver` for fast, in-memory resolution of effective configurations for translators.
- Updated `AppPreferences` and `ThemeRepository` to support persistent storage of per-app engine and trigger overrides.
- Standardized navigation layout resolution to respect layered preferences (Global Fallback vs. App-Specific).
* feat(localization): add trigger configuration and override strings
- **Localization**: Added new string resources for "Active Triggers", "Trigger Override", and "Notification Engine" configuration across multiple locales (SK, PL, RU, FR, ZH-TW, TR, UK, PT-BR, CS, DE, IN, IT, KO, HU).
- **Strings**:
- Added descriptions for notification types: Standard, Progress, Media, Navigation, Call, and Timer.
- Added UI strings for global trigger settings and per-app overrides.
- Added `managed_by_theme_desc` and `use_global_default_engine` to support theme-based behavior logic.
- **Cleanup**: Removed unused mock engine strings (`engine_mock_charging`, `engine_mock_percentage`, etc.) from all translated `strings.xml` files.
* feat(theme): enhance engine resolution and theme-persistent configuration (#147)
- **Notification Service**:
- Refactored `getEffectiveEngine` in `NotificationReaderService` to implement a 4-tier resolution priority: Theme App Override > User App Override > Theme Global Override > User Global Fallback.
- **Theme Engine & Editor**:
- **Global Settings**: Added `use_native_live_updates` to `GlobalConfig` model, allowing themes to define a default engine for all apps.
- **Persistence**: Fixed an issue where changing the engine via the UI while a custom theme was active would not persist; it now correctly patches the active Theme JSON.
- **Theme Creator**:
- Updated `ThemeViewModel` to correctly load and save the global engine selection to the Theme JSON.
- Fixed a bug where existing call configurations were wiped when updating app-specific live update settings.
- Improved state management by making `themeUseNativeLiveUpdates` nullable to support inheritance.
- **UI & Settings**:
- **Engine Settings**: Refactored `EngineSettingsScreen` to use `AppPreferences` and `ThemeRepository` instead of raw `SharedPreferences`.
- Integrated real-time engine updates by sending `ACTION_RELOAD_THEME` to the background service upon configuration changes.
- Standardized UI spacing and typography in the Engine selection screen.
- **Bug Fixes**:
- Fixed a bug in `AppListViewModel` where app-specific engine overrides were not being saved to the active theme if the override didn't already exist.
- Corrected `@StringRes` annotation usage in `ShapeOption` enum.
* build: update version to 0.5.0-alpha03 and update localizations
- **Build**: Bumped `versionCode` to `17` and `versionName` to `0.5.0-alpha03` in `app/build.gradle.kts`.
- **Localization**:
- **French (FR)**: Completed translations for the "Engine" configuration, "Island Behavior", and "Active Triggers" sections. Updated backup and app configuration strings.
- **Italian (IT)**: Translated the v0.4.2 changelog, engine settings, color labels, and navigation arrival keywords.
- **Traditional Chinese (ZH-TW)**: Fully localized the new Engine, Behavior, and Trigger settings. Refined onboarding and permission strings (e.g., "HyperIsland" to "超級島").
- **Hungarian (HU)**: Localized engine settings, behavior controls, trigger descriptions, and the v0.4.2 hotfix changelog. Updated call-related keywords.
- **Portuguese (Brazil)**: Translated the v0.4.2 changelog, color labels, and UI state strings (Recommended, Favorites, All). Updated navigation arrival keywords.
- **Strings**: Standardized engine configuration keys across all modified locales to support the transition between "Xiaomi Featured Design" and "Native Android Live Updates".
* loc(fr): fix apostrophe escaping in French strings
- Updated `island_behavior_desc` and `engine_timeouts_triggers` in the French (`fr-rFR`) `strings.xml` to properly escape apostrophes, ensuring correct XML parsing and display.
* Feature/nav3 migration (#152)
* build: integrate Navigation 3 and refactor app routing
- **Dependencies**: Added `androidx.navigation3` (runtime and UI) and `androidx-lifecycle-viewmodel-navigation3` to `libs.versions.toml` and `app/build.gradle.kts`.
- **Navigation Framework**:
- Introduced `NavigationState` to manage persistent back stacks for top-level routes (Onboarding and Home).
- Implemented a `Navigator` class to abstract forward and backward navigation logic using Navigation 3 APIs.
- Replaced the manual `Screen` enum and `AnimatedContent` router with a `@Serializable` sealed interface and `NavDisplay`.
- **Routing & State**:
- Refactored `MainActivity` to use `entryProvider` for screen definitions, simplifying navigation calls across the app.
- Updated screen transitions to leverage `NavDisplay`'s built-in handling of the back gesture and entry management.
- Improved data passing between screens (e.g., `ImportPreview` and `NavCustomization`) by using type-safe classes in the `Screen` sealed interface.
- **Models**: Added `@Serializable` annotations to backup-related models (`HyperBridgeBackup`, `BackupMetadata`, `AppSettingBackup`) to support type-safe navigation arguments.
- **UI**: Standardized back-button behavior across all settings and configuration screens to use the new `Navigator.goBack()` method.
* refactor(navigation): modularize and relocate navigation logic
- **Navigation Architecture**:
- Created a new `com.d4viddf.hyperbridge.ui.navigation` package to house navigation-related components.
- Moved `Screen` definition from `MainActivity.kt` to its own file `Screen.kt`, maintaining `@Serializable` support for all application routes.
- Relocated `NavigationState.kt` and `Navigator.kt` to the new navigation package and updated their package declarations accordingly.
- **MainActivity**:
- Cleaned up imports and removed inline `Screen` interface definition to use the new modularized navigation components.
- Updated references to use `toEntries` and other navigation extension functions from the new package.
* refactor(navigation): modularize nav graph and enhance back press handling
- **Navigation**:
- Extracted the main navigation graph from `MainActivity.kt` into a new standalone `mainNavGraph` function in `NavGraph.kt` to improve maintainability.
- Updated `Navigator.goBack()` to return a boolean indicating if the back event was handled, allowing for better control over app exit logic.
- Integrated `BackHandler` in `MainActivity` to properly handle system back gestures and call `onExit` when at the root of the navigation stack.
- **MainActivity**:
- Refactored `MainRootNavigation` into smaller, more focused composables (`MainRootNavigation` and `MainNavigationContent`).
- Improved the initialization flow to show a `CircularProgressIndicator` while the setup status is being loaded from preferences.
- Simplified the logic for switching between `Onboarding` and `Home` routes based on the setup completion state.
- **UI**: Standardized back navigation behavior across all screens, ensuring consistent exit behavior from the `Info` screen.
* feat(ui): implement custom navigation transitions and predictive back support
- **Navigation**:
- Added custom `transitionSpec` and `popTransitionSpec` using horizontal slides and fades for smoother screen transitions.
- Implemented `predictivePopTransitionSpec` to support Android's predictive back gesture with scaling and transform origin adjustments based on swipe edge.
- Dynamically updated `topLevelRoutes` to prevent navigating back to the onboarding flow once setup is complete.
- **UI**:
- Applied a rounded corner clip (32.dp) to the main `NavDisplay` container.
- Optimized transition durations to 400ms for a more fluid user experience.
* chore: code cleanup, dependency update, and UI refinements
- **UI & UX**:
- **Widget Picker**: Refactored `WidgetPickerScreen` and removed `WidgetPickerViewModel.kt` to streamline widget selection logic. Updated layout filtering to consistently use `targetCellWidth` and `targetCellHeight`.
- **Theme Creator**: Improved navigation and state management in `ThemeCreatorScreen` and `AppThemeEditorScreen`. Enhanced UI consistency for color and icon selection previews.
- **Licenses**: Updated `LicensesScreen` to dynamically load a sorted list of open-source libraries and externalized the screen title string.
- **Components**: Standardized `EmptyState` component parameters and improved layout logic in `WidgetConfigScreen` and `SavedAppWidgetsScreen`.
- **Localization**:
- Added `open_source_licenses` string resource.
- Refactored `WidgetConfigScreen` to use localized toast and error messages.
- **Translators**:
- **Standard**: Cleaned up title resolution logic in `StandardTranslator`.
- **Progress**: Updated `ProgressTranslator` to hide redundant percentage text in specific circular progress views.
- **Internal Improvements**:
- **Compatibility**: Removed redundant SDK version checks for `Parcelable` handling and notification permissions, favoring modern Android APIs.
- **Utilities**: Switched to `androidx.core` extension functions for `Bitmap` creation and `Uri` parsing.
- **Persistence**: Updated `SavedAppWidgetsScreen` to use the `edit { }` DSL for `SharedPreferences`.
- **Data**: Refined theme ID generation in `ThemeRepository` and improved rule engine comments.
- **Bug Fixes**:
- Fixed a potential nullability issue in `AppListViewModel` when patching theme configurations.
- Corrected `IslandConfig` initialization in the App Settings dialog.
* Feature/duplication toggle (#153)
* feat(settings): enhance island behavior with heads-up timeout and notification management
- **Island Configuration**:
- Introduced `floatTimeout` to `IslandConfig` to control the duration of heads-up popups.
- Added `removeOriginalNotification` toggle to allow automatic dismissal of source notifications when a Live Update is active.
- Updated `AppPreferences` and `AppSettings` to support persistent storage for these new global and per-app settings.
- **UI & Settings**:
- **Island Settings**: Redesigned the "Heads-up Popup" configuration with an animated slider for granular timeout control (1s to 30s).
- **Live Updates**: Added a new section in `IslandSettingsControl` to manage the removal of original notifications.
- Improved UI scrollability in `IslandSettingsScreen` and added several Compose previews for better development feedback.
- Refactored toggle logic to ensure consistency between heads-up and timeout states.
- **Translators**:
- Updated all translators (`Media`, `Call`, `Progress`, `Standard`, `Nav`, `Timer`) to respect the new `expandedTimeMs` (heads-up timeout) configuration.
- Cleaned up redundant theme color application logic in `TimerTranslator`.
- **Localization**:
- Added string resources for "Live updates", "Remove original notification", and "Heads-up popup" timeout descriptions.
* feat(service): implement notification timeout and original notification removal
- **Notification Service**:
- Introduced a `handlePostNotificationSideEffects` method to manage post-processing logic for translated notifications.
- Added support for automatically cancelling the original system notification when `removeOriginalNotification` is enabled in the configuration (excluding Media notifications). fixes #21, fixes #120, fixes #146
- Implemented a timeout mechanism using coroutines to automatically dismiss translated notifications after a configured duration.
- Added `intentionallyRemovedKeys` tracking to prevent recursive processing loops when the service cancels a system notification.
- Improved cleanup logic in `onNotificationRemoved` and `cleanupCache` to ensure associated timeout jobs are cancelled and resources are freed.
- **Bug Fixes**:
- Added missing cancellation of existing processing jobs and timeout jobs when a notification is removed or updated.
- **Logic Improvements**:
- Improved design logic for new notification parameters ( closes #99) and add theme configuration ( closes #107)
* fix(service): refine notification timeout logic and dismissal behavior
- **Notification Service**:
- Modified `onStartCommand` to use non-nullable `Intent` parameters for better type safety.
- Updated notification removal logic to ensure the source notification is no longer canceled when the Island notification is dismissed or times out.
- Refactored `handlePostNotificationSideEffects` to only apply timeout scheduling to "Live Update" notifications.
- Improved cache cleanup and logging during notification dismissal to prevent unintended side effects on source notifications.
* Feature/onboarding screen (#155)
* feat(ui): redesign onboarding flow and expand configuration options
- **Onboarding Experience**:
- Significantly expanded the onboarding process from 7 to 11 pages, incorporating new configuration steps for triggers, engine selection, and island behavior.
- Redesigned the `OnboardingScreen` layout with improved navigation controls, including a dedicated "Back" button and a more prominent "Get Started" entry point.
- Added several new pages: `TriggersConfigPage` (active notification types), `PriorityEducationPage` (island limit behavior), `BehaviorConfigPage` (notification removal and shade visibility), `CustomizationPage` (themes and widgets), and `EngineConfigPage` (Xiaomi vs. Native Live Updates).
- Integrated `ModalBottomSheet` for granular settings configuration directly within the onboarding flow.
- Added extensive Compose previews for all onboarding pages to improve development feedback.
- **Data & Logic**:
- **AppPreferences**: Implemented a migration check to force an onboarding reset for users upgrading to version 0.5.0 (versionCode 17).
- **MainActivity**: Removed the standalone `PriorityEducationDialog` as its logic is now integrated into the onboarding flow.
- **Build Configuration**: Enabled `buildConfig` in `app/build.gradle.kts` and updated the Kotlin serialization plugin version.
- **Localization & Resources**:
- **Strings**:
- Standardized and updated terminology across multiple languages (ES, HU, IT, ZH-TW, SK, CS, TR, DE, PT, KO, FR, UK, RU, ID).
- Added new string resources for the expanded onboarding descriptions, "Live updates" management, and "Remove original notification" toggles.
- Refined French strings to ensure proper apostrophe escaping and formatting.
- Simplified and updated battery optimization instructions ("Allow Autostart", "Disable Restrictions").
- **Icons**: Switched to modern Rounded/Filled icon variants for a more consistent visual style across the setup pages.
* feat(onboarding): implement auto-hide configuration page
- **Onboarding Flow**:
- Expanded the onboarding process from 11 to 13 pages to include more granular behavior settings.
- Introduced a dedicated `AutoHideConfigPage` to manage island timeout settings during initial setup.
- Updated `BehaviorConfigPage` description to reflect its role in global configuration.
- **UI & Components**:
- Added an interactive "Auto-hide Island" section with a `Switch` and an `AnimatedVisibility` slider for timeout selection.
- Exposed `timeoutSteps` and `formatSeconds` from `IslandSettingsControl.kt` to be reusable across the onboarding flow.
- Integrated `Icons.Rounded.Timer` and `Icons.Default.AccessTime` for improved visual cues in behavior settings.
- **State Management**:
- Updated `OnboardingScreen` to persist `IslandConfig` timeout and `isFloat` preferences directly through `AppPreferences`.
- **Localization**:
- Added `behavior_desc_glob_config` string resource for better description of island behavior settings.
* chore: bump version to 0.5.0-beta01 and refine onboarding navigation
- **Build**:
- Updated `versionCode` to `18` and `versionName` to `0.5.0-beta01` in `app/build.gradle.kts`.
- **Navigation**:
- **Navigator**: Introduced `finishOnboarding` to explicitly handle the transition from onboarding to the home screen by ensuring the home stack is initialized before clearing the onboarding route, preventing potential app closure.
- **MainActivity**: Refactored `navigationState` initialization to use a persistent set of all possible top-level routes and a fixed initial start route.
- **NavGraph**: Updated setup completion logic to use the new `navigator.finishOnboarding` method and removed redundant priority education flags.
- **Onboarding**:
- Refactored `OnboardingScreen` to dynamically determine the last page using `pagerState.pageCount` instead of a hardcoded index.
- **Data & Persistence**:
- **AppPreferences**: Updated the onboarding reset logic to target `versionCode 18` for version 0.5.0, forcing a setup reset for users on older builds.
* chore: general cleanup, bug fixes, and maintenance
- **UI & Components**:
- Removed several legacy dialogs and components: `ChangelogDialog`, `PriorityEducationDialog`, and `DeviceCompatibility`.
- Simplified `CommonCards.kt` by removing `ExpandableOptimizationCard` and localized logic.
- Refactored `HyperOSColorPicker` to remove unused `ColorBubble` component.
- Added `verticalScroll` to `ThemeSettingsContent` to improve navigation on smaller screens.
- Updated `IslandSettingsControl` and `IslandSettingsScreen` to use explicit `IslandConfig` initialization.
- Added a content description for the widget snapshot layout.
- **Notification Service & Logic**:
- Refactored `NotificationReaderService` and `LiveUpdateTranslator` to improve string handling and reliability when resolving notification titles/text.
- Enhanced exception handling across the service and data layers using suppressed exceptions (`catch (_: Exception)`) to reduce log noise.
- Improved app label caching and package filtering logic.
- **Data & Persistence**:
- Cleaned up `AppPreferences` by removing the unused `isPriorityEduShown` flow.
- Refactored `ThemeRepository` to use a lowercase tag and improved error logging for theme installation and resource loading.
- Updated `AppCacheManager` and `WidgetPickerScreen` for better state management and type safety.
- **Resources & Localization**:
- **Icons**: Added monochrome support to adaptive launcher icons and moved them to the standard `mipmap-anydpi` directory.
- **Strings**:
- **PT-BR**: Updated widget ID formatting.
- **FR**: Simplified application configuration descriptions.
- **General**: Added `widget_layout_snapshot` resource.
- **Build & System**:
- Updated Gradle wrapper from `9.3.1` to `9.4.1`.
- Refactored `DeviceUtils` for cleaner system property checks.
- Updated `AndroidManifest` with improved package visibility policy ignores.
- Cleaned up `ThemeInstallerActivity` by removing redundant SDK version checks for dynamic color schemes.
* chore(manifest): add notification listener permission
- **Permissions**:
- Added `BIND_NOTIFICATION_LISTENER_SERVICE` to `AndroidManifest.xml` to support notification listener capabilities.
- Applied `tools:ignore="ProtectedPermissions"` to acknowledge the use of the protected system permission.
* fix(service): refine progress notification behavior and navigation state (#157)
- **Notification Service & Translators**:
- **ProgressTranslator**:
- Modified `translate` to accept an `isUpdate` flag.
- Restricted "floating" behavior to the initial notification appearance only, preventing subsequent progress updates from re-triggering the float animation.
- Refined `BigIslandInfo` layout for finished progress states by simplifying `ImageTextInfoLeft` and `ImageTextInfoRight` parameters (removing redundant text info).
- Removed redundant `setIslandFirstFloat` call.
- **NotificationReaderService**: Updated `PROGRESS` type handling to pass the `isUpdate` state to the translator.
- **UI & Navigation**:
- **MainActivity**: Added `isSetupComplete` as a key to `remember` blocks for `initialStartRoute` and `allPossibleTopLevel` to ensure navigation state correctly reacts to setup completion changes.
Closes #156
* feat(boot): add support for Direct Boot (#159)
- **Boot Receiver**:
- Updated `BootReceiver.kt` to handle `ACTION_LOCKED_BOOT_COMPLETED`, allowing the app to respond to boot events before the first user unlock.
- Added `android.intent.action.LOCKED_BOOT_COMPLETED` to the `BootReceiver` intent filter in `AndroidManifest.xml`.
fixed #158
* chore(localization): update and expand string resources across multiple languages
- **Strings & Localization**:
- **General**: Added new string resources `behavior_desc_glob_config` (island behavior description) and `widget_layout_snapshot` (widget preview) across all supported locales.
- **Spanish (ES)**: Added translations for floating notification timeouts, open source licenses, live updates, and original notification removal.
- **French (FR)**: Fully localized several strings including floating notification settings, live updates, onboarding descriptions, and behavior configurations.
- **Traditional Chinese (ZH-TW)**: Localized onboarding descriptions for widgets, themes, and customization options.
- **Portuguese (PT-BR)**: Simplified the `widget_id_fmt` string from "Widget #%d" to "Widget".
- **New Keys**: Added placeholder or translated entries for `setting_float_timeout_desc`, `open_source_licenses`, `live_updates`, and `remove_original_notification` logic in relevant resource files.
* chore(changelog): update for version 0.5.0-beta
- **UI & Navigation**:
- Updated `ChangelogHistoryScreen` to include version `0.5.0` at the top of the history list.
- Updated `MainActivity` to display the `0.5.0-beta` changelog in the main `ChangelogSheet`.
- **Resources & Localization**:
- Added `changelog_0_5_0_beta` string containing release notes for Native Live Updates, HyperOS CN support, new Theme Engine features (HSL selector, behavior controls), and system fixes.
- Added `title_0_5_0` string resource ("The Live Update").
* chore: update version metadata to 0.5.0
- **Changelog**:
- Updated `ChangelogHistoryScreen` to mark version `0.5.0` as the latest release and downgraded `0.4.2` to historical status.
- **Application Versioning**:
- Updated the fallback `currentVersionName` in `MainActivity` from `0.4.2` to `0.5.0`.
* refactor(service): refine floating behavior for progress notifications (#164)
- **Notification Logic**:
- Updated `ProgressTranslator` to restrict the floating behavior (`setEnableFloat`) to only the initial appearance of a notification, preventing repeated floats during updates.
- Removed the redundant `setIslandFirstFloat` call from the builder.
close #163
* Fix/162 bug crash unable to start service with null (#165)
* fix(service): handle nullable intent in onStartCommand
- **Notification Service**:
- Updated `onStartCommand` signature to accept a nullable `Intent` to prevent potential crashes when the service is restarted by the system.
- Implemented null-safe checks for `intent.action` when processing test widgets and theme reloads.
fixed #162
* feat(service): implement localized error messages for persistent notifications
- **Notification Service**:
- Updated `NotificationReaderService` to use localized string resources for fallback content text in both standard and widget notifications.
- Replaced the hardcoded "Active Island" text with `R.string.notification_went_wrong`.
- Replaced the hardcoded "Active" widget text with `R.string.widget_went_wrong`.
- **Resources**:
- Added `notification_went_wrong` and `widget_went_wrong` string resources to provide clearer feedback when notifications or widgets fail to render correctly.
* chore: update changelog string formatting
- **Resources**:
- Refactored `changelog_0_5_0_beta` in `strings.xml` to use HTML `` tags instead of markdown-style bolding for better compatibility with Android string rendering.
- Improved indentation and formatting of the changelog text within the resource file.
* chore: bump version to 0.5.0-beta02 and refine changelog trigger logic
- **Build**:
- Updated `versionCode` to `19` and `versionName` to `0.5.0-beta02` in `app/build.gradle.kts`.
- **Navigation & Lifecycle**:
- **NavGraph**: Removed redundant `setLastSeenVersion` call during onboarding completion to centralize version tracking.
- **MainActivity**:
- Refined the changelog display logic to prevent it from appearing immediately after initial onboarding.
- Improved `lastSeenVersion` state handling by using a more reliable initial value (`-1`) to ensure version comparisons only trigger after the preference is properly loaded.
- Introduced `isInitiallySetup` check to distinguish between fresh installations and application updates.
* chore: update localizations and refine progress translation logic
- **Localization & Resources**:
- **Spanish (ES)**: Translated several core strings, including "Live updates," "Open source licenses," and "Island behavior" descriptions.
- **Indonesian (ID)**: Extensively updated translations for UI controls, navigation keywords (arrival, destination), and system permission descriptions.
- **Italian (IT)** and **Traditional Chinese (ZH-TW)**: Completed translations for onboarding descriptions and behavior settings.
- **General**: Added the `0.5.0-beta` changelog, version title, and error fallback strings (`notification_went_wrong`, `widget_went_wrong`) across all supported languages (ES, HU, IT, ZH-TW, SK, CS, TR, DE, PT, KO, FR, UK, PL, RU, ID).
- **Notification Service**:
- **ProgressTranslator**: Refactored `setIslandConfig` in the progress update logic to only apply `expandedTimeMs` (float timeout) if the floating notification setting is explicitly enabled, improving consistency with user preferences.
* chore(manifest): remove redundant notification listener permission
- **Permissions**:
- Removed the `BIND_NOTIFICATION_LISTENER_SERVICE` permission from `AndroidManifest.xml`.
- Removed the associated `tools:ignore="ProtectedPermissions"` attribute.
* Fix WidgetManager initialization and Shizuku implementation
Closes #97
Closes #119
Closes #141
Closes #177
* chore(manifest): remove redundant notification listener permission
- **Permissions**:
- Removed the `BIND_NOTIFICATION_LISTENER_SERVICE` permission from `AndroidManifest.xml`.
- Removed the associated `tools:ignore="ProtectedPermissions"` attribute.
* chore: bump version to 0.5.0-beta04 and refine Shizuku workaround
- **Build**:
- Updated `versionCode` to `21` and `versionName` to `0.5.0-beta04` in `app/build.gradle.kts`.
- **Shizuku & Notifications**:
- **ShizukuManager**: Refactored the `notify` logic to improve the reliability of the XMSF network workaround on MIUI/HyperOS.
- Introduced a `Mutex` to prevent race conditions during concurrent notification dispatches.
- Added a 50ms delay after disabling XMSF to ensure the network state change takes effect.
- Implemented a 1-second delayed restoration of XMSF network, which is reset by subsequent notifications to prevent premature re-enabling.
- Added `@RequiresPermission` annotation for `POST_NOTIFICATIONS`.
- **Resources & Localization**:
- **Changelog**: Updated `changelog_0_5_0_beta` to include information regarding CN Support and the new Shizuku workaround.
- **Traditional Chinese (ZH-TW)**: Refined the `enable_autostart` string for better clarity ("開啟後台啟動管理").
* Feature/permanent island (#179)
* feat: implement Permanent Island feature and enhance Shizuku notification handling
- **Permanent Island**:
- Introduced a "Permanent Island" feature that keeps a placeholder Dynamic Island visible when no active notifications are present.
- Added `PermanentIslandManager` to coordinate visibility based on the `activeIslands` count within `NotificationReaderService`.
- Added `isPermanentIslandEnabledFlow` and `permanentIslandWidthFlow` to `AppPreferences` to persist user configuration.
- Implemented `PermanentIslandConfigPage` in the onboarding flow and a dedicated settings screen.
Closes #94
- **Shizuku & Notifications**:
- **ShizukuManager**: Added a `cancel` function that applies the XMSF network workaround (disabling/enabling XMSF network with a delay) when programmatically removing notifications to ensure consistency on MIUI/HyperOS.
- **NotificationReaderService**: Integrated `PermanentIslandManager` to trigger updates whenever notifications are posted or removed.
- **Lifecycle & System**:
- **BootReceiver**: Added `ACTION_MY_PACKAGE_REPLACED` to the intent filter in `AndroidManifest.xml` and handled it in the receiver to ensure the service restarts correctly after app updates.
- **UI & Navigation**:
- **GlobalSettingsScreen**: Added an entry point for Permanent Island settings and updated the expressive shape logic to accommodate the additional list item.
- **NavGraph**: Defined the route and destination for `PermanentIslandConfig`.
- **Previews**: Updated `@Preview` annotations across onboarding components to use `apiLevel = 36`.
- **Resources & Localization**:
- Added strings for "Permanent Island" titles, descriptions, and width controls in English and Spanish (ES, ES-rEM).
- Refined the `shizuku_workaround_title` string for better clarity.
* feat: implement Permanent Island feature and configuration UI
- **Permanent Island Logic**:
- Introduced `PermanentIslandManager` to manage a persistent, "empty" island state when no other active notifications are present.
- Implemented logic to automatically show/hide the permanent island based on the current notification count and user preferences.
- Utilizes `ShizukuManager` to dispatch a non-dismissible, low-priority notification that uses non-breaking spaces (`\u00A0`) to simulate variable island widths.
- **Settings UI**:
- Created `PermanentIslandConfigScreen` providing users with a toggle to enable the feature and a slider to adjust the island's width.
- Integrated `AppPreferences` flows to reactively update the UI and service state.
- **Components**:
- Developed `PermanentIslandPreview`, a Compose component that provides a real-time visual mock-up of the island's appearance on a status bar, featuring smooth width animations via `animateDpAsState`.
* docs: Add permanent island feature to changelog
* chore: Update .gitignore to exclude build artifacts and IDE configs
* chore: Update .gitignore to standard Android Jetpack Compose template
* chore: initialize project configuration, dependency versions, and ignore rules
* feat: introduce dedicated Download notification type and refine progress parsing (#184)
- **Notifications & Services**:
- **NotificationType**: Added a new `DOWNLOAD` type to separate file transfers from general `PROGRESS` tasks. Close #174
- **NotificationReaderService**:
- Integrated `DownloadTranslator` to handle download-specific notification logic.
- Implemented `isDownloadNotification` and `hasProgressNotification` to smarter distinguish between types based on package names, channel IDs, and keywords.
- Added logic to implicitly enable `DOWNLOAD` if `PROGRESS` is active for a specific app to ensure backward compatibility.
- **Translators**:
- **BaseTranslator**: Introduced `extractTextPercentage` to parse progress values (e.g., "12%") from notification titles or text.
- **ProgressTranslator**: Updated to support text-based percentages and refined indeterminate state handling.
- **LiveUpdateTranslator**: Added fallback to text-based percentage parsing for system-native animations.
- **CallTranslator**: Switched to `getCharSequence` for retrieving notification titles for better compatibility.
- **Data & Migration**:
- **AppPreferences**: Implemented a migration routine (`download_type_migration_complete`) that automatically adds the `DOWNLOAD` type to global and app-specific settings if `PROGRESS` was previously enabled.
- **UI & Resources**:
- **Theme & Onboarding**:
- Updated `ThemeSettingsContent` and `OnboardingScreen` to reflect the split between Progress and Downloads.
- Updated icons: `HourglassEmpty` for Progress and `CloudDownload` for Downloads.
- **Localization**:
- Refined strings in English and Spanish to distinguish between "Progress & Tasks" and "Downloads."
- **Changelog**: Updated `changelog_0_5_0_beta` with details regarding CN support, the new Download type, text-based progress support, and smart filtering.
* feat: implement DownloadTranslator and expand configuration resources
- **Translators**:
- Introduced `DownloadTranslator` to provide specialized handling for file transfer notifications, separating them from general progress tasks.
- Implemented logic in `DownloadTranslator` to handle indeterminate states, percentage-based progress, and completion detection via keywords or 100% threshold.
- Added support for custom completion icons ("tick_icon") and automated timeout/dismissible configurations for finished downloads.
- **Resources & Localization**:
- **Permanent Island**: Added strings for titles, descriptions, and width adjustment controls across multiple locales (ID, FR, TR, CS, IT, UK, DE, ZH-TW, PT-BR, RU, SK, ES, PL, KO, HU).
- **Shizuku Workaround**: Added localized strings to guide users through enabling the Shizuku workaround, specifically for improved notification triggering on CN ROMs.
- **Do Not Disturb (DND)**: Introduced strings for managing island visibility during DND, including manual pause and auto-detection of system DND status.
- **Type Refinement**: Updated `type_progress` and `type_download` strings to clearly distinguish between "Progress & Tasks" (ongoing operations) and "Downloads" (file transfers).
- **Changelog**: Updated `changelog_0_5_0_beta` in all languages to detail CN support, native live updates, theme engine improvements (HSL selector), and the new Permanent Island feature.
* Feature/sui permission (#185)
* feat(shizuku): implement support for Sui permissions and detection
* chore(i18n): extract hardcoded onboarding text and add Spanish translations for Shizuku and DND strings
* chore(i18n): translate permanent island strings to Spanish
* Fix/widget permanent island (#186)
* fix(permanent-island): hide permanent island when a widget is active
* feat(ui): add shizuku workaround toggle to engine theme page
* Feature/better icons (#187)
* fix(notifications): tint dark icons, extract messaging avatars, fix update popups
* fix(notifications): allow group summaries with text to trigger islands
* fix(notifications): completely remove GROUP_ALERT_CHILDREN check for summaries
* fix(notifications): ensure sequential execution of cancel and notify to prevent race conditions dropping islands
* fix(notifications): use sender_person key for MessagingStyle avatar extraction
* chore: bump version to 0.5.0-beta05 and update dependencies
- **Build Configuration**:
- Updated `versionCode` to 22 and `versionName` to `0.5.0-beta05`.
- Updated Kotlin serialization plugin to 2.3.21.
- **Dependencies**:
- **Kotlin & Serialization**: Updated Kotlin to 2.3.21 and `kotlinxSerializationJson` to 1.11.0.
- **Compose**: Updated `composeBom` to 2026.05.01 and Compose UI to 1.11.2.
- **Libraries**:
- Updated `gson` to 2.14.0.
- Updated `nav3` to 1.1.2.
* chore: bump version to 0.5.0 and update settings UI components
- **Build & Versioning**:
- Incremented `versionCode` to 23 and updated `versionName` to `0.5.0` in `build.gradle.kts`.
- **UI & Layout**:
- **GlobalSettingsScreen**: Added imports for `Switch`, `Row`, `Box`, and other Compose components to support interactive setting elements and layout refinements.
- **Documentation**:
- Updated the version badge in `README.md` to reflect the `0.5.0` release.
* docs: update README features for 0.5.0 and finalize changelog strings
* docs: add CN Support, Theme Engine Improvements, and Onboarding to the 0.5.0 changelog string
---------
Co-authored-by: David Df
* Beta/0 5 0 (#192)
* Feat/color picker (#127)
* feat(theme): implement HSL color picker and enhanced lightness slider
- **New `CustomColorBottomSheet`**: Introduced a dedicated HSL (Hue, Saturation, Lightness) color picker with gradient sliders for precise theme color customization.
- **Enhanced `ColorsDetailContent`**:
- Overhauled the preset tab with an animated lightness slider that appears when a preset is selected.
- Improved the preset selection logic to maintain the "base" hue even when adjusting lightness to extreme white or black.
- Redesigned the "Custom" colors section to use the new HSL bottom sheet instead of a simple hex text field.
- Switched from simple circular bubbles to a more refined UI with selection rings and rounded-square transitions for active colors.
- **Improved Theme Creator State**:
- Implemented strict state clearing (nulling `currentEditingThemeId`) when exiting the creator to prevent state leakage between editing sessions.
- Updated save/apply dialog logic to ensure the UI resets correctly after persistence.
- **Shared Components**: Added `GradientSlider` and `ColorBubble` to `HyperOSColorPicker.kt` for reusable, stylized color selection components.
- **Localization**: Added missing string resources for color labels (Hue, Saturation, Lightness) in English and Spanish.
* #### feat(theme): implement advanced color modes and redesigned color picker
- **Color Modes**: Introduced a `ColorMode` system (`CUSTOM`, `APP_ICON`, `MATERIAL_YOU`) to replace the binary "Use App Colors" toggle. This allows themes to dynamically adapt to Material You system colors or extract colors from app icons globally and per-app.
- **Improved Color Picker**: Replaced standard text fields with a new `CustomColorBottomSheet` in the Call Style and Theme Creator screens, featuring a visual HSL-based selection and hex formatting.
- **UI & UX Refinement**:
- Redesigned `ColorsDetailContent` with a scrollable layout and new sections for dynamic color modes.
- Updated the Theme Creator preview to correctly reflect the active color mode.
- Standardized "My Theme" and "Share Theme" strings to use localized resources.
- **Data Persistence**: Updated `HyperTheme`, `GlobalConfig`, and `AppThemeOverride` models to support the new `ColorMode` enum while maintaining backward compatibility with legacy themes.
- **Theme Creator**: Refactored `ThemeViewModel` to handle more robust state initialization and migration when editing or creating new themes.
- **Tooling**: Added Crowdin configuration settings for the IDE.
* feat(widget): add favorites, recommended filtering, and update notice (#128)
- **Widget Picker**:
- Implemented a "Recommended" filter to highlight 1x4 and 2x4 layouts compatible with the Island size.
- Added a Favorite system for widget apps, allowing users to star apps for top-level sorting.
- Added a floating navigation bar to switch between Recommended and All widgets.
- Improved dimension display by converting DP to grid cell proportions (e.g., 4x2).
- **Saved Widgets**:
- Added a persistent "Update Notice" dialog to remind users to check Autostart/Battery permissions after an app update.
- Implemented a "Kill Active Widgets" action to instantly dismiss all running widget overlays.
- Redesigned the UI with a centered filter toolbar (Favorites vs. All) and a dedicated Add FAB.
- Added reactive listening for newly added widgets to ensure the list updates immediately.
- **Service & Data**:
- Enhanced `WidgetOverlayService` with specific `ACTION_KILL_WIDGET` and `ACTION_KILL_ALL_WIDGETS` handlers for immediate UI cleanup.
- Added `favoriteWidgetAppsFlow` to `AppPreferences` to persist starred widget providers.
- **Localization**:
- Added English and Spanish strings for filters, permission reminders, and widget management.
CLOSE #72
* feat(service): implement Native Android 16 Live Updates support
- Added `LiveUpdateTranslator` to transform standard notifications into native Android 16 promoted ongoing tasks using `android.requestPromotedOngoing` and `android.shortCriticalText`.
- Implemented a toggle in Global Settings to choose between custom Xiaomi Islands and native Android 16 Live Updates.
- Created a dedicated `hyper_bridge_live_update_channel` for native updates to manage notifications and visibility independently.
- Added `POST_PROMOTED_NOTIFICATIONS` permission to `AndroidManifest.xml` to support Android 16's promoted notification features.
- Enhanced `LiveUpdateTranslator` with a `generateCriticalShortText` helper to dynamically extract progress percentages or ETAs for the system status chip.
- Fixed Android 16 category filtering by forcing Live Updates into strictly valid categories (`PROGRESS` or `TRANSPORT`) to prevent OS blocking.
- Ensured original notification timestamps are preserved for accurate system-wide "time active" calculations.
- Updated `NotificationReaderService` to support hot-swapping between custom injection and native updates via shared preferences.
* feat(service): enhance Live Update processing and Android 16 compatibility
- **NotificationReaderService**: Implemented a more robust content hashing algorithm that tracks Title, Text, Progress (max, current, indeterminate), and Button Action states (e.g., Play to Pause transitions) to ensure accurate UI updates.
- **LiveUpdateTranslator**:
- Added support for large icons and picture extras in live updates.
- Improved media notification handling by suppressing progress bars that often persist in the background.
- Optimized category mapping (Progress vs. Transport) to comply with Android 16 ongoing promotion requirements.
- Updated `shortCriticalText` generation logic to prioritize media titles, percentages, or time ETAs for the Dynamic Island chip.
- Switched to native Android 16 APIs for promoted ongoing requests and short critical text injection.
* feat(settings): refactor Global Settings UI and update version
- Updated `versionName` to `0.5.0-alpha01`.
- Redesigned `GlobalSettingsScreen` to use consistent card-based layouts for all settings items.
- Replaced the "Native Live Updates" `ListItem` with a new standardized `SettingsSwitchItem` component.
- Improved UI spacing and styling across the settings screen, including unified icon background treatments.
- Simplified state management and click behavior for toggleable settings.
* Update changelog resource to v0.4.2
* build: update project dependencies and gradle configuration (#134)
- **Gradle & AGP**:
- Upgraded Android Gradle Plugin (AGP) from `8.13.2` to `9.1.0`.
- Upgraded Gradle wrapper from `8.13` to `9.3.1`.
- Added `foojay-resolver-convention` plugin to `settings.gradle.kts` for automated toolchain management.
- **Dependencies**:
- Updated Kotlin to `2.3.20` and KSP to `2.3.2`.
- Updated Compose BOM to `2026.03.00` and Material3 to `1.5.0-alpha15`.
- Updated various AndroidX libraries: `core-ktx` (`1.18.0`), `datastore-preferences` (`1.2.1`), `activity-compose` (`1.13.0`), and `ui` (`1.10.5`).
- Bumped `kotlinx-serialization-json` to `1.10.0`.
- **Build Configuration**:
- Removed `kotlin-android` plugin in favor of `kotlin-compose` and built-in Kotlin support.
- Removed explicit `jvmTarget` configuration in `app/build.gradle.kts`.
- Enabled several experimental/new Android Gradle properties in `gradle.properties`, including `android.builtInKotlin` and `android.newDsl`.
- **Optimization**:
- Updated `proguard-rules.pro` to strip Debug and Info logs in release builds using AGP 9.1+ syntax.
* Feature/ux iprovement global configuration (#137)
* feat(settings): redesign Global Settings and enhance Theme Engine
- **Global Settings UI**:
- Refactored `GlobalSettingsScreen` to use a card-based list layout with standardized `ListOptionCard` components.
- Split settings into dedicated screens: `EngineSettingsScreen`, `IslandSettingsScreen`, and `NavCustomizationScreen`.
- Added an "Engine" section to toggle between "Xiaomi Featured Design" and "Native Android Live Updates".
- Implemented a more granular "Island Behavior" configuration including an improved auto-hide timeout slider with snap points (1s to 1h).
- **Theme Engine**:
- Integrated "Engine", "Island Behavior", and "Notification Types" selection directly into the Theme Creator flow.
- Added a real-time `EnginePreview` component to visualize differences between Xiaomi and Native Live Update designs.
- Added a "Notification Types" manager to toggle triggers for Standard, Progress, Media, Navigation, Call, and Timer events.
- **Functionality & Fixes**:
- **Crash Fix**: Resolved a critical issue causing the app to crash when opening settings in Traditional Chinese.
- Updated `AppPreferences` to support persistent storage for the native notification engine toggle.
- Improved `ThemeViewModel` with a more efficient bitmap scaling method for theme thumbnails.
- Updated `StandardTranslator`, `NavTranslator`, and `ProgressTranslator` to better respect global timeout and dismissal configurations.
- Set the default notification shade visibility to `false` for a cleaner "Island-only" experience by default.
- **Localization**:
- Added extensive string resources for version 0.4.2 hotfix and new engine settings across multiple languages (SK, PL, RU, ZH-TW, TR, UK, PT-BR, CS, DE, HU, IN, IT, KO, ES).
- Fixed several translation issues in Traditional Chinese and Spanish.
* feat(settings): externalize and localize auto-hide island strings
- **UI**: Refactored `IslandSettingsControl.kt` to use `stringResource` instead of hardcoded strings for the Auto-hide Island feature.
- **Localization**:
- Added new string resources (`auto_hide_island`, `hides_after_a_set_time`, `behavior_hide_desc`, `behavior_desc_hide_long`) to multiple locales: Slovak, Polish, Russian, Traditional Chinese, Turkish, Ukrainian, Portuguese (Brazil), Czech, German, Indonesian, Italian, Korean, Spanish, and Hungarian.
- Updated Spanish translations for behavior and trigger settings.
- Standardized ellipsis usage from `…` to `...` in Portuguese (Brazil) and Korean `strings.xml`.
* build: update version and remove redundant material3 dependency
- Updated `versionName` to `0.5.0-alpha02`.
- Removed explicit `androidx-material3` dependency from `libs.versions.toml` and `app/build.gradle.kts` (now managed via Compose BOM).
* feat(localization): add French (France) translation
- Added complete French localization (`strings.xml`) for the entire application.
- Translated all core UI components, including onboarding, settings, library, and about screens.
- Localized complex features such as the Theme Engine, Backup & Restore, and the new Notification Engine settings.
- Provided French translations for all historical changelogs (v0.1.0 through v0.4.2).
- Included localized string arrays for specific notification keyword matching (navigation, calls, and progress updates).
* feat(theme): implement layered app-specific behavior overrides
- **Notification Service**:
- Implemented a layered resolution system (Theme > App > Global) for notification triggers and engine selection.
- Updated `NotificationReaderService` to respect theme-managed app configurations and simplified processing logic.
- Enhanced `LiveUpdateTranslator` to support themed colors and icons for the Native Live Update engine, specifically for navigation events.
- Improved title and text resolution with smarter fallback mechanisms and content hashing for updates.
- **Theme Engine & Editor**:
- Added app-specific behavior overrides to the Theme Creator, allowing per-app configuration of notification types and engine selection (Xiaomi vs. Native).
- Integrated "Engine", "Island Behavior", and "Notification Types" into the App Theme Editor.
- Added `AppNotificationTypesEditor` for granular control over which event types (Standard, Media, Nav, etc.) trigger the island for specific apps.
- Refactored `EngineThemeContent` to support an optional "Use Global Default" state.
- **UI & Settings**:
- Redesigned the "Notification Types" settings using `SettingsToggleCard` for a more consistent card-based look.
- Added a theme-managed warning to the App Settings dialog when behaviors are controlled by an active theme.
- Externalized and localized several strings related to triggers and engine management.
- Added French language support to the locale selector.
- **Technical Improvements**:
- Introduced `ConfigResolver` for fast, in-memory resolution of effective configurations for translators.
- Updated `AppPreferences` and `ThemeRepository` to support persistent storage of per-app engine and trigger overrides.
- Standardized navigation layout resolution to respect layered preferences (Global Fallback vs. App-Specific).
* feat(localization): add trigger configuration and override strings
- **Localization**: Added new string resources for "Active Triggers", "Trigger Override", and "Notification Engine" configuration across multiple locales (SK, PL, RU, FR, ZH-TW, TR, UK, PT-BR, CS, DE, IN, IT, KO, HU).
- **Strings**:
- Added descriptions for notification types: Standard, Progress, Media, Navigation, Call, and Timer.
- Added UI strings for global trigger settings and per-app overrides.
- Added `managed_by_theme_desc` and `use_global_default_engine` to support theme-based behavior logic.
- **Cleanup**: Removed unused mock engine strings (`engine_mock_charging`, `engine_mock_percentage`, etc.) from all translated `strings.xml` files.
* feat(theme): enhance engine resolution and theme-persistent configuration (#147)
- **Notification Service**:
- Refactored `getEffectiveEngine` in `NotificationReaderService` to implement a 4-tier resolution priority: Theme App Override > User App Override > Theme Global Override > User Global Fallback.
- **Theme Engine & Editor**:
- **Global Settings**: Added `use_native_live_updates` to `GlobalConfig` model, allowing themes to define a default engine for all apps.
- **Persistence**: Fixed an issue where changing the engine via the UI while a custom theme was active would not persist; it now correctly patches the active Theme JSON.
- **Theme Creator**:
- Updated `ThemeViewModel` to correctly load and save the global engine selection to the Theme JSON.
- Fixed a bug where existing call configurations were wiped when updating app-specific live update settings.
- Improved state management by making `themeUseNativeLiveUpdates` nullable to support inheritance.
- **UI & Settings**:
- **Engine Settings**: Refactored `EngineSettingsScreen` to use `AppPreferences` and `ThemeRepository` instead of raw `SharedPreferences`.
- Integrated real-time engine updates by sending `ACTION_RELOAD_THEME` to the background service upon configuration changes.
- Standardized UI spacing and typography in the Engine selection screen.
- **Bug Fixes**:
- Fixed a bug in `AppListViewModel` where app-specific engine overrides were not being saved to the active theme if the override didn't already exist.
- Corrected `@StringRes` annotation usage in `ShapeOption` enum.
* build: update version to 0.5.0-alpha03 and update localizations
- **Build**: Bumped `versionCode` to `17` and `versionName` to `0.5.0-alpha03` in `app/build.gradle.kts`.
- **Localization**:
- **French (FR)**: Completed translations for the "Engine" configuration, "Island Behavior", and "Active Triggers" sections. Updated backup and app configuration strings.
- **Italian (IT)**: Translated the v0.4.2 changelog, engine settings, color labels, and navigation arrival keywords.
- **Traditional Chinese (ZH-TW)**: Fully localized the new Engine, Behavior, and Trigger settings. Refined onboarding and permission strings (e.g., "HyperIsland" to "超級島").
- **Hungarian (HU)**: Localized engine settings, behavior controls, trigger descriptions, and the v0.4.2 hotfix changelog. Updated call-related keywords.
- **Portuguese (Brazil)**: Translated the v0.4.2 changelog, color labels, and UI state strings (Recommended, Favorites, All). Updated navigation arrival keywords.
- **Strings**: Standardized engine configuration keys across all modified locales to support the transition between "Xiaomi Featured Design" and "Native Android Live Updates".
* loc(fr): fix apostrophe escaping in French strings
- Updated `island_behavior_desc` and `engine_timeouts_triggers` in the French (`fr-rFR`) `strings.xml` to properly escape apostrophes, ensuring correct XML parsing and display.
* Feature/nav3 migration (#152)
* build: integrate Navigation 3 and refactor app routing
- **Dependencies**: Added `androidx.navigation3` (runtime and UI) and `androidx-lifecycle-viewmodel-navigation3` to `libs.versions.toml` and `app/build.gradle.kts`.
- **Navigation Framework**:
- Introduced `NavigationState` to manage persistent back stacks for top-level routes (Onboarding and Home).
- Implemented a `Navigator` class to abstract forward and backward navigation logic using Navigation 3 APIs.
- Replaced the manual `Screen` enum and `AnimatedContent` router with a `@Serializable` sealed interface and `NavDisplay`.
- **Routing & State**:
- Refactored `MainActivity` to use `entryProvider` for screen definitions, simplifying navigation calls across the app.
- Updated screen transitions to leverage `NavDisplay`'s built-in handling of the back gesture and entry management.
- Improved data passing between screens (e.g., `ImportPreview` and `NavCustomization`) by using type-safe classes in the `Screen` sealed interface.
- **Models**: Added `@Serializable` annotations to backup-related models (`HyperBridgeBackup`, `BackupMetadata`, `AppSettingBackup`) to support type-safe navigation arguments.
- **UI**: Standardized back-button behavior across all settings and configuration screens to use the new `Navigator.goBack()` method.
* refactor(navigation): modularize and relocate navigation logic
- **Navigation Architecture**:
- Created a new `com.d4viddf.hyperbridge.ui.navigation` package to house navigation-related components.
- Moved `Screen` definition from `MainActivity.kt` to its own file `Screen.kt`, maintaining `@Serializable` support for all application routes.
- Relocated `NavigationState.kt` and `Navigator.kt` to the new navigation package and updated their package declarations accordingly.
- **MainActivity**:
- Cleaned up imports and removed inline `Screen` interface definition to use the new modularized navigation components.
- Updated references to use `toEntries` and other navigation extension functions from the new package.
* refactor(navigation): modularize nav graph and enhance back press handling
- **Navigation**:
- Extracted the main navigation graph from `MainActivity.kt` into a new standalone `mainNavGraph` function in `NavGraph.kt` to improve maintainability.
- Updated `Navigator.goBack()` to return a boolean indicating if the back event was handled, allowing for better control over app exit logic.
- Integrated `BackHandler` in `MainActivity` to properly handle system back gestures and call `onExit` when at the root of the navigation stack.
- **MainActivity**:
- Refactored `MainRootNavigation` into smaller, more focused composables (`MainRootNavigation` and `MainNavigationContent`).
- Improved the initialization flow to show a `CircularProgressIndicator` while the setup status is being loaded from preferences.
- Simplified the logic for switching between `Onboarding` and `Home` routes based on the setup completion state.
- **UI**: Standardized back navigation behavior across all screens, ensuring consistent exit behavior from the `Info` screen.
* feat(ui): implement custom navigation transitions and predictive back support
- **Navigation**:
- Added custom `transitionSpec` and `popTransitionSpec` using horizontal slides and fades for smoother screen transitions.
- Implemented `predictivePopTransitionSpec` to support Android's predictive back gesture with scaling and transform origin adjustments based on swipe edge.
- Dynamically updated `topLevelRoutes` to prevent navigating back to the onboarding flow once setup is complete.
- **UI**:
- Applied a rounded corner clip (32.dp) to the main `NavDisplay` container.
- Optimized transition durations to 400ms for a more fluid user experience.
* chore: code cleanup, dependency update, and UI refinements
- **UI & UX**:
- **Widget Picker**: Refactored `WidgetPickerScreen` and removed `WidgetPickerViewModel.kt` to streamline widget selection logic. Updated layout filtering to consistently use `targetCellWidth` and `targetCellHeight`.
- **Theme Creator**: Improved navigation and state management in `ThemeCreatorScreen` and `AppThemeEditorScreen`. Enhanced UI consistency for color and icon selection previews.
- **Licenses**: Updated `LicensesScreen` to dynamically load a sorted list of open-source libraries and externalized the screen title string.
- **Components**: Standardized `EmptyState` component parameters and improved layout logic in `WidgetConfigScreen` and `SavedAppWidgetsScreen`.
- **Localization**:
- Added `open_source_licenses` string resource.
- Refactored `WidgetConfigScreen` to use localized toast and error messages.
- **Translators**:
- **Standard**: Cleaned up title resolution logic in `StandardTranslator`.
- **Progress**: Updated `ProgressTranslator` to hide redundant percentage text in specific circular progress views.
- **Internal Improvements**:
- **Compatibility**: Removed redundant SDK version checks for `Parcelable` handling and notification permissions, favoring modern Android APIs.
- **Utilities**: Switched to `androidx.core` extension functions for `Bitmap` creation and `Uri` parsing.
- **Persistence**: Updated `SavedAppWidgetsScreen` to use the `edit { }` DSL for `SharedPreferences`.
- **Data**: Refined theme ID generation in `ThemeRepository` and improved rule engine comments.
- **Bug Fixes**:
- Fixed a potential nullability issue in `AppListViewModel` when patching theme configurations.
- Corrected `IslandConfig` initialization in the App Settings dialog.
* Feature/duplication toggle (#153)
* feat(settings): enhance island behavior with heads-up timeout and notification management
- **Island Configuration**:
- Introduced `floatTimeout` to `IslandConfig` to control the duration of heads-up popups.
- Added `removeOriginalNotification` toggle to allow automatic dismissal of source notifications when a Live Update is active.
- Updated `AppPreferences` and `AppSettings` to support persistent storage for these new global and per-app settings.
- **UI & Settings**:
- **Island Settings**: Redesigned the "Heads-up Popup" configuration with an animated slider for granular timeout control (1s to 30s).
- **Live Updates**: Added a new section in `IslandSettingsControl` to manage the removal of original notifications.
- Improved UI scrollability in `IslandSettingsScreen` and added several Compose previews for better development feedback.
- Refactored toggle logic to ensure consistency between heads-up and timeout states.
- **Translators**:
- Updated all translators (`Media`, `Call`, `Progress`, `Standard`, `Nav`, `Timer`) to respect the new `expandedTimeMs` (heads-up timeout) configuration.
- Cleaned up redundant theme color application logic in `TimerTranslator`.
- **Localization**:
- Added string resources for "Live updates", "Remove original notification", and "Heads-up popup" timeout descriptions.
* feat(service): implement notification timeout and original notification removal
- **Notification Service**:
- Introduced a `handlePostNotificationSideEffects` method to manage post-processing logic for translated notifications.
- Added support for automatically cancelling the original system notification when `removeOriginalNotification` is enabled in the configuration (excluding Media notifications). fixes #21, fixes #120, fixes #146
- Implemented a timeout mechanism using coroutines to automatically dismiss translated notifications after a configured duration.
- Added `intentionallyRemovedKeys` tracking to prevent recursive processing loops when the service cancels a system notification.
- Improved cleanup logic in `onNotificationRemoved` and `cleanupCache` to ensure associated timeout jobs are cancelled and resources are freed.
- **Bug Fixes**:
- Added missing cancellation of existing processing jobs and timeout jobs when a notification is removed or updated.
- **Logic Improvements**:
- Improved design logic for new notification parameters ( closes #99) and add theme configuration ( closes #107)
* fix(service): refine notification timeout logic and dismissal behavior
- **Notification Service**:
- Modified `onStartCommand` to use non-nullable `Intent` parameters for better type safety.
- Updated notification removal logic to ensure the source notification is no longer canceled when the Island notification is dismissed or times out.
- Refactored `handlePostNotificationSideEffects` to only apply timeout scheduling to "Live Update" notifications.
- Improved cache cleanup and logging during notification dismissal to prevent unintended side effects on source notifications.
* Feature/onboarding screen (#155)
* feat(ui): redesign onboarding flow and expand configuration options
- **Onboarding Experience**:
- Significantly expanded the onboarding process from 7 to 11 pages, incorporating new configuration steps for triggers, engine selection, and island behavior.
- Redesigned the `OnboardingScreen` layout with improved navigation controls, including a dedicated "Back" button and a more prominent "Get Started" entry point.
- Added several new pages: `TriggersConfigPage` (active notification types), `PriorityEducationPage` (island limit behavior), `BehaviorConfigPage` (notification removal and shade visibility), `CustomizationPage` (themes and widgets), and `EngineConfigPage` (Xiaomi vs. Native Live Updates).
- Integrated `ModalBottomSheet` for granular settings configuration directly within the onboarding flow.
- Added extensive Compose previews for all onboarding pages to improve development feedback.
- **Data & Logic**:
- **AppPreferences**: Implemented a migration check to force an onboarding reset for users upgrading to version 0.5.0 (versionCode 17).
- **MainActivity**: Removed the standalone `PriorityEducationDialog` as its logic is now integrated into the onboarding flow.
- **Build Configuration**: Enabled `buildConfig` in `app/build.gradle.kts` and updated the Kotlin serialization plugin version.
- **Localization & Resources**:
- **Strings**:
- Standardized and updated terminology across multiple languages (ES, HU, IT, ZH-TW, SK, CS, TR, DE, PT, KO, FR, UK, RU, ID).
- Added new string resources for the expanded onboarding descriptions, "Live updates" management, and "Remove original notification" toggles.
- Refined French strings to ensure proper apostrophe escaping and formatting.
- Simplified and updated battery optimization instructions ("Allow Autostart", "Disable Restrictions").
- **Icons**: Switched to modern Rounded/Filled icon variants for a more consistent visual style across the setup pages.
* feat(onboarding): implement auto-hide configuration page
- **Onboarding Flow**:
- Expanded the onboarding process from 11 to 13 pages to include more granular behavior settings.
- Introduced a dedicated `AutoHideConfigPage` to manage island timeout settings during initial setup.
- Updated `BehaviorConfigPage` description to reflect its role in global configuration.
- **UI & Components**:
- Added an interactive "Auto-hide Island" section with a `Switch` and an `AnimatedVisibility` slider for timeout selection.
- Exposed `timeoutSteps` and `formatSeconds` from `IslandSettingsControl.kt` to be reusable across the onboarding flow.
- Integrated `Icons.Rounded.Timer` and `Icons.Default.AccessTime` for improved visual cues in behavior settings.
- **State Management**:
- Updated `OnboardingScreen` to persist `IslandConfig` timeout and `isFloat` preferences directly through `AppPreferences`.
- **Localization**:
- Added `behavior_desc_glob_config` string resource for better description of island behavior settings.
* chore: bump version to 0.5.0-beta01 and refine onboarding navigation
- **Build**:
- Updated `versionCode` to `18` and `versionName` to `0.5.0-beta01` in `app/build.gradle.kts`.
- **Navigation**:
- **Navigator**: Introduced `finishOnboarding` to explicitly handle the transition from onboarding to the home screen by ensuring the home stack is initialized before clearing the onboarding route, preventing potential app closure.
- **MainActivity**: Refactored `navigationState` initialization to use a persistent set of all possible top-level routes and a fixed initial start route.
- **NavGraph**: Updated setup completion logic to use the new `navigator.finishOnboarding` method and removed redundant priority education flags.
- **Onboarding**:
- Refactored `OnboardingScreen` to dynamically determine the last page using `pagerState.pageCount` instead of a hardcoded index.
- **Data & Persistence**:
- **AppPreferences**: Updated the onboarding reset logic to target `versionCode 18` for version 0.5.0, forcing a setup reset for users on older builds.
* chore: general cleanup, bug fixes, and maintenance
- **UI & Components**:
- Removed several legacy dialogs and components: `ChangelogDialog`, `PriorityEducationDialog`, and `DeviceCompatibility`.
- Simplified `CommonCards.kt` by removing `ExpandableOptimizationCard` and localized logic.
- Refactored `HyperOSColorPicker` to remove unused `ColorBubble` component.
- Added `verticalScroll` to `ThemeSettingsContent` to improve navigation on smaller screens.
- Updated `IslandSettingsControl` and `IslandSettingsScreen` to use explicit `IslandConfig` initialization.
- Added a content description for the widget snapshot layout.
- **Notification Service & Logic**:
- Refactored `NotificationReaderService` and `LiveUpdateTranslator` to improve string handling and reliability when resolving notification titles/text.
- Enhanced exception handling across the service and data layers using suppressed exceptions (`catch (_: Exception)`) to reduce log noise.
- Improved app label caching and package filtering logic.
- **Data & Persistence**:
- Cleaned up `AppPreferences` by removing the unused `isPriorityEduShown` flow.
- Refactored `ThemeRepository` to use a lowercase tag and improved error logging for theme installation and resource loading.
- Updated `AppCacheManager` and `WidgetPickerScreen` for better state management and type safety.
- **Resources & Localization**:
- **Icons**: Added monochrome support to adaptive launcher icons and moved them to the standard `mipmap-anydpi` directory.
- **Strings**:
- **PT-BR**: Updated widget ID formatting.
- **FR**: Simplified application configuration descriptions.
- **General**: Added `widget_layout_snapshot` resource.
- **Build & System**:
- Updated Gradle wrapper from `9.3.1` to `9.4.1`.
- Refactored `DeviceUtils` for cleaner system property checks.
- Updated `AndroidManifest` with improved package visibility policy ignores.
- Cleaned up `ThemeInstallerActivity` by removing redundant SDK version checks for dynamic color schemes.
* chore(manifest): add notification listener permission
- **Permissions**:
- Added `BIND_NOTIFICATION_LISTENER_SERVICE` to `AndroidManifest.xml` to support notification listener capabilities.
- Applied `tools:ignore="ProtectedPermissions"` to acknowledge the use of the protected system permission.
* fix(service): refine progress notification behavior and navigation state (#157)
- **Notification Service & Translators**:
- **ProgressTranslator**:
- Modified `translate` to accept an `isUpdate` flag.
- Restricted "floating" behavior to the initial notification appearance only, preventing subsequent progress updates from re-triggering the float animation.
- Refined `BigIslandInfo` layout for finished progress states by simplifying `ImageTextInfoLeft` and `ImageTextInfoRight` parameters (removing redundant text info).
- Removed redundant `setIslandFirstFloat` call.
- **NotificationReaderService**: Updated `PROGRESS` type handling to pass the `isUpdate` state to the translator.
- **UI & Navigation**:
- **MainActivity**: Added `isSetupComplete` as a key to `remember` blocks for `initialStartRoute` and `allPossibleTopLevel` to ensure navigation state correctly reacts to setup completion changes.
Closes #156
* feat(boot): add support for Direct Boot (#159)
- **Boot Receiver**:
- Updated `BootReceiver.kt` to handle `ACTION_LOCKED_BOOT_COMPLETED`, allowing the app to respond to boot events before the first user unlock.
- Added `android.intent.action.LOCKED_BOOT_COMPLETED` to the `BootReceiver` intent filter in `AndroidManifest.xml`.
fixed #158
* chore(localization): update and expand string resources across multiple languages
- **Strings & Localization**:
- **General**: Added new string resources `behavior_desc_glob_config` (island behavior description) and `widget_layout_snapshot` (widget preview) across all supported locales.
- **Spanish (ES)**: Added translations for floating notification timeouts, open source licenses, live updates, and original notification removal.
- **French (FR)**: Fully localized several strings including floating notification settings, live updates, onboarding descriptions, and behavior configurations.
- **Traditional Chinese (ZH-TW)**: Localized onboarding descriptions for widgets, themes, and customization options.
- **Portuguese (PT-BR)**: Simplified the `widget_id_fmt` string from "Widget #%d" to "Widget".
- **New Keys**: Added placeholder or translated entries for `setting_float_timeout_desc`, `open_source_licenses`, `live_updates`, and `remove_original_notification` logic in relevant resource files.
* chore(changelog): update for version 0.5.0-beta
- **UI & Navigation**:
- Updated `ChangelogHistoryScreen` to include version `0.5.0` at the top of the history list.
- Updated `MainActivity` to display the `0.5.0-beta` changelog in the main `ChangelogSheet`.
- **Resources & Localization**:
- Added `changelog_0_5_0_beta` string containing release notes for Native Live Updates, HyperOS CN support, new Theme Engine features (HSL selector, behavior controls), and system fixes.
- Added `title_0_5_0` string resource ("The Live Update").
* chore: update version metadata to 0.5.0
- **Changelog**:
- Updated `ChangelogHistoryScreen` to mark version `0.5.0` as the latest release and downgraded `0.4.2` to historical status.
- **Application Versioning**:
- Updated the fallback `currentVersionName` in `MainActivity` from `0.4.2` to `0.5.0`.
* refactor(service): refine floating behavior for progress notifications (#164)
- **Notification Logic**:
- Updated `ProgressTranslator` to restrict the floating behavior (`setEnableFloat`) to only the initial appearance of a notification, preventing repeated floats during updates.
- Removed the redundant `setIslandFirstFloat` call from the builder.
close #163
* Fix/162 bug crash unable to start service with null (#165)
* fix(service): handle nullable intent in onStartCommand
- **Notification Service**:
- Updated `onStartCommand` signature to accept a nullable `Intent` to prevent potential crashes when the service is restarted by the system.
- Implemented null-safe checks for `intent.action` when processing test widgets and theme reloads.
fixed #162
* feat(service): implement localized error messages for persistent notifications
- **Notification Service**:
- Updated `NotificationReaderService` to use localized string resources for fallback content text in both standard and widget notifications.
- Replaced the hardcoded "Active Island" text with `R.string.notification_went_wrong`.
- Replaced the hardcoded "Active" widget text with `R.string.widget_went_wrong`.
- **Resources**:
- Added `notification_went_wrong` and `widget_went_wrong` string resources to provide clearer feedback when notifications or widgets fail to render correctly.
* chore: update changelog string formatting
- **Resources**:
- Refactored `changelog_0_5_0_beta` in `strings.xml` to use HTML `` tags instead of markdown-style bolding for better compatibility with Android string rendering.
- Improved indentation and formatting of the changelog text within the resource file.
* chore: bump version to 0.5.0-beta02 and refine changelog trigger logic
- **Build**:
- Updated `versionCode` to `19` and `versionName` to `0.5.0-beta02` in `app/build.gradle.kts`.
- **Navigation & Lifecycle**:
- **NavGraph**: Removed redundant `setLastSeenVersion` call during onboarding completion to centralize version tracking.
- **MainActivity**:
- Refined the changelog display logic to prevent it from appearing immediately after initial onboarding.
- Improved `lastSeenVersion` state handling by using a more reliable initial value (`-1`) to ensure version comparisons only trigger after the preference is properly loaded.
- Introduced `isInitiallySetup` check to distinguish between fresh installations and application updates.
* chore: update localizations and refine progress translation logic
- **Localization & Resources**:
- **Spanish (ES)**: Translated several core strings, including "Live updates," "Open source licenses," and "Island behavior" descriptions.
- **Indonesian (ID)**: Extensively updated translations for UI controls, navigation keywords (arrival, destination), and system permission descriptions.
- **Italian (IT)** and **Traditional Chinese (ZH-TW)**: Completed translations for onboarding descriptions and behavior settings.
- **General**: Added the `0.5.0-beta` changelog, version title, and error fallback strings (`notification_went_wrong`, `widget_went_wrong`) across all supported languages (ES, HU, IT, ZH-TW, SK, CS, TR, DE, PT, KO, FR, UK, PL, RU, ID).
- **Notification Service**:
- **ProgressTranslator**: Refactored `setIslandConfig` in the progress update logic to only apply `expandedTimeMs` (float timeout) if the floating notification setting is explicitly enabled, improving consistency with user preferences.
* chore(manifest): remove redundant notification listener permission
- **Permissions**:
- Removed the `BIND_NOTIFICATION_LISTENER_SERVICE` permission from `AndroidManifest.xml`.
- Removed the associated `tools:ignore="ProtectedPermissions"` attribute.
* Fix WidgetManager initialization and Shizuku implementation
Closes #97
Closes #119
Closes #141
Closes #177
* chore(manifest): remove redundant notification listener permission
- **Permissions**:
- Removed the `BIND_NOTIFICATION_LISTENER_SERVICE` permission from `AndroidManifest.xml`.
- Removed the associated `tools:ignore="ProtectedPermissions"` attribute.
* chore: bump version to 0.5.0-beta04 and refine Shizuku workaround
- **Build**:
- Updated `versionCode` to `21` and `versionName` to `0.5.0-beta04` in `app/build.gradle.kts`.
- **Shizuku & Notifications**:
- **ShizukuManager**: Refactored the `notify` logic to improve the reliability of the XMSF network workaround on MIUI/HyperOS.
- Introduced a `Mutex` to prevent race conditions during concurrent notification dispatches.
- Added a 50ms delay after disabling XMSF to ensure the network state change takes effect.
- Implemented a 1-second delayed restoration of XMSF network, which is reset by subsequent notifications to prevent premature re-enabling.
- Added `@RequiresPermission` annotation for `POST_NOTIFICATIONS`.
- **Resources & Localization**:
- **Changelog**: Updated `changelog_0_5_0_beta` to include information regarding CN Support and the new Shizuku workaround.
- **Traditional Chinese (ZH-TW)**: Refined the `enable_autostart` string for better clarity ("開啟後台啟動管理").
* Feature/permanent island (#179)
* feat: implement Permanent Island feature and enhance Shizuku notification handling
- **Permanent Island**:
- Introduced a "Permanent Island" feature that keeps a placeholder Dynamic Island visible when no active notifications are present.
- Added `PermanentIslandManager` to coordinate visibility based on the `activeIslands` count within `NotificationReaderService`.
- Added `isPermanentIslandEnabledFlow` and `permanentIslandWidthFlow` to `AppPreferences` to persist user configuration.
- Implemented `PermanentIslandConfigPage` in the onboarding flow and a dedicated settings screen.
Closes #94
- **Shizuku & Notifications**:
- **ShizukuManager**: Added a `cancel` function that applies the XMSF network workaround (disabling/enabling XMSF network with a delay) when programmatically removing notifications to ensure consistency on MIUI/HyperOS.
- **NotificationReaderService**: Integrated `PermanentIslandManager` to trigger updates whenever notifications are posted or removed.
- **Lifecycle & System**:
- **BootReceiver**: Added `ACTION_MY_PACKAGE_REPLACED` to the intent filter in `AndroidManifest.xml` and handled it in the receiver to ensure the service restarts correctly after app updates.
- **UI & Navigation**:
- **GlobalSettingsScreen**: Added an entry point for Permanent Island settings and updated the expressive shape logic to accommodate the additional list item.
- **NavGraph**: Defined the route and destination for `PermanentIslandConfig`.
- **Previews**: Updated `@Preview` annotations across onboarding components to use `apiLevel = 36`.
- **Resources & Localization**:
- Added strings for "Permanent Island" titles, descriptions, and width controls in English and Spanish (ES, ES-rEM).
- Refined the `shizuku_workaround_title` string for better clarity.
* feat: implement Permanent Island feature and configuration UI
- **Permanent Island Logic**:
- Introduced `PermanentIslandManager` to manage a persistent, "empty" island state when no other active notifications are present.
- Implemented logic to automatically show/hide the permanent island based on the current notification count and user preferences.
- Utilizes `ShizukuManager` to dispatch a non-dismissible, low-priority notification that uses non-breaking spaces (`\u00A0`) to simulate variable island widths.
- **Settings UI**:
- Created `PermanentIslandConfigScreen` providing users with a toggle to enable the feature and a slider to adjust the island's width.
- Integrated `AppPreferences` flows to reactively update the UI and service state.
- **Components**:
- Developed `PermanentIslandPreview`, a Compose component that provides a real-time visual mock-up of the island's appearance on a status bar, featuring smooth width animations via `animateDpAsState`.
* docs: Add permanent island feature to changelog
* chore: Update .gitignore to exclude build artifacts and IDE configs
* chore: Update .gitignore to standard Android Jetpack Compose template
* chore: initialize project configuration, dependency versions, and ignore rules
* feat: introduce dedicated Download notification type and refine progress parsing (#184)
- **Notifications & Services**:
- **NotificationType**: Added a new `DOWNLOAD` type to separate file transfers from general `PROGRESS` tasks. Close #174
- **NotificationReaderService**:
- Integrated `DownloadTranslator` to handle download-specific notification logic.
- Implemented `isDownloadNotification` and `hasProgressNotification` to smarter distinguish between types based on package names, channel IDs, and keywords.
- Added logic to implicitly enable `DOWNLOAD` if `PROGRESS` is active for a specific app to ensure backward compatibility.
- **Translators**:
- **BaseTranslator**: Introduced `extractTextPercentage` to parse progress values (e.g., "12%") from notification titles or text.
- **ProgressTranslator**: Updated to support text-based percentages and refined indeterminate state handling.
- **LiveUpdateTranslator**: Added fallback to text-based percentage parsing for system-native animations.
- **CallTranslator**: Switched to `getCharSequence` for retrieving notification titles for better compatibility.
- **Data & Migration**:
- **AppPreferences**: Implemented a migration routine (`download_type_migration_complete`) that automatically adds the `DOWNLOAD` type to global and app-specific settings if `PROGRESS` was previously enabled.
- **UI & Resources**:
- **Theme & Onboarding**:
- Updated `ThemeSettingsContent` and `OnboardingScreen` to reflect the split between Progress and Downloads.
- Updated icons: `HourglassEmpty` for Progress and `CloudDownload` for Downloads.
- **Localization**:
- Refined strings in English and Spanish to distinguish between "Progress & Tasks" and "Downloads."
- **Changelog**: Updated `changelog_0_5_0_beta` with details regarding CN support, the new Download type, text-based progress support, and smart filtering.
* feat: implement DownloadTranslator and expand configuration resources
- **Translators**:
- Introduced `DownloadTranslator` to provide specialized handling for file transfer notifications, separating them from general progress tasks.
- Implemented logic in `DownloadTranslator` to handle indeterminate states, percentage-based progress, and completion detection via keywords or 100% threshold.
- Added support for custom completion icons ("tick_icon") and automated timeout/dismissible configurations for finished downloads.
- **Resources & Localization**:
- **Permanent Island**: Added strings for titles, descriptions, and width adjustment controls across multiple locales (ID, FR, TR, CS, IT, UK, DE, ZH-TW, PT-BR, RU, SK, ES, PL, KO, HU).
- **Shizuku Workaround**: Added localized strings to guide users through enabling the Shizuku workaround, specifically for improved notification triggering on CN ROMs.
- **Do Not Disturb (DND)**: Introduced strings for managing island visibility during DND, including manual pause and auto-detection of system DND status.
- **Type Refinement**: Updated `type_progress` and `type_download` strings to clearly distinguish between "Progress & Tasks" (ongoing operations) and "Downloads" (file transfers).
- **Changelog**: Updated `changelog_0_5_0_beta` in all languages to detail CN support, native live updates, theme engine improvements (HSL selector), and the new Permanent Island feature.
* Feature/sui permission (#185)
* feat(shizuku): implement support for Sui permissions and detection
* chore(i18n): extract hardcoded onboarding text and add Spanish translations for Shizuku and DND strings
* chore(i18n): translate permanent island strings to Spanish
* Fix/widget permanent island (#186)
* fix(permanent-island): hide permanent island when a widget is active
* feat(ui): add shizuku workaround toggle to engine theme page
* Feature/better icons (#187)
* fix(notifications): tint dark icons, extract messaging avatars, fix update popups
* fix(notifications): allow group summaries with text to trigger islands
* fix(notifications): completely remove GROUP_ALERT_CHILDREN check for summaries
* fix(notifications): ensure sequential execution of cancel and notify to prevent race conditions dropping islands
* fix(notifications): use sender_person key for MessagingStyle avatar extraction
* chore: bump version to 0.5.0-beta05 and update dependencies
- **Build Configuration**:
- Updated `versionCode` to 22 and `versionName` to `0.5.0-beta05`.
- Updated Kotlin serialization plugin to 2.3.21.
- **Dependencies**:
- **Kotlin & Serialization**: Updated Kotlin to 2.3.21 and `kotlinxSerializationJson` to 1.11.0.
- **Compose**: Updated `composeBom` to 2026.05.01 and Compose UI to 1.11.2.
- **Libraries**:
- Updated `gson` to 2.14.0.
- Updated `nav3` to 1.1.2.
* chore: bump version to 0.5.0 and update settings UI components
- **Build & Versioning**:
- Incremented `versionCode` to 23 and updated `versionName` to `0.5.0` in `build.gradle.kts`.
- **UI & Layout**:
- **GlobalSettingsScreen**: Added imports for `Switch`, `Row`, `Box`, and other Compose components to support interactive setting elements and layout refinements.
- **Documentation**:
- Updated the version badge in `README.md` to reflect the `0.5.0` release.
* docs: update README features for 0.5.0 and finalize changelog strings
* docs: add CN Support, Theme Engine Improvements, and Onboarding to the 0.5.0 changelog string
* chore: update changelog reference to version 0.5.0
- **MainActivity**:
- Updated `ChangelogSheet` to use the stable `0.5.0` changelog string resource instead of the beta version.
- **UI & Settings**:
- Added `FontWeight` import to `GlobalSettingsScreen` to support text styling refinements.
* i18n: translate 0.5.0 changelog to spanish
---------
Co-authored-by: David Df
* refactor: remove local SettingsSwitchItem from GlobalSettingsScreen
- **UI & Screens**:
- **GlobalSettingsScreen**: Removed the internal `SettingsSwitchItem` composable function to streamline the file and encourage the use of shared UI components.
---------
Co-authored-by: David Df
---
.gitignore | 75 +-
.idea/deploymentTargetSelector.xml | 1 +
.idea/gradle.xml | 1 -
.idea/misc.xml | 1 -
README.md | 11 +-
app/.gitignore | 4 +-
app/build.gradle.kts | 30 +-
app/proguard-rules.pro | 10 +-
app/src/main/AndroidManifest.xml | 20 +-
.../hyperbridge/IPrivilegedLogCallback.aidl | 5 +
.../hyperbridge/IPrivilegedService.aidl | 8 +
.../com/d4viddf/hyperbridge/MainActivity.kt | 290 ++---
.../hyperbridge/data/AppCacheManager.kt | 6 +-
.../hyperbridge/data/AppPreferences.kt | 218 +++-
.../hyperbridge/data/ConfigResolver.kt | 68 +
.../hyperbridge/data/db/AppSettings.kt | 2 +
.../hyperbridge/data/model/BackupModels.kt | 6 +-
.../hyperbridge/data/theme/RulesEngine.kt | 2 +-
.../hyperbridge/data/theme/ThemeRepository.kt | 24 +-
.../hyperbridge/data/widget/WidgetManager.kt | 24 +-
.../shizuku/PrivilegedServiceImpl.kt | 178 +++
.../integration/shizuku/ShizukuHook.kt | 241 ++++
.../shizuku/ShizukuUserServiceRecycler.kt | 181 +++
.../integration/shizuku/ShizukuUtil.kt | 46 +
.../integration/shizuku/XmsfNetworkHelper.kt | 92 ++
.../hyperbridge/models/IslandConfig.kt | 7 +-
.../hyperbridge/models/NotificationType.kt | 1 +
.../hyperbridge/models/theme/ThemeModels.kt | 8 +-
.../hyperbridge/receiver/BootReceiver.kt | 2 +
.../service/NotificationReaderService.kt | 461 +++++--
.../service/PermanentIslandManager.kt | 119 ++
.../service/WidgetOverlayService.kt | 2 +-
.../service/translators/BaseTranslator.kt | 88 +-
.../service/translators/CallTranslator.kt | 4 +-
.../service/translators/DownloadTranslator.kt | 123 ++
.../translators/LiveUpdateTranslator.kt | 150 ++-
.../service/translators/MediaTranslator.kt | 2 +-
.../service/translators/NavTranslator.kt | 15 +-
.../service/translators/ProgressTranslator.kt | 34 +-
.../service/translators/StandardTranslator.kt | 14 +-
.../service/translators/TimerTranslator.kt | 4 +-
.../hyperbridge/ui/AppListViewModel.kt | 124 +-
.../ui/components/ChangelogDialog.kt | 63 -
.../hyperbridge/ui/components/CommonCards.kt | 82 +-
.../hyperbridge/ui/components/Dialogs.kt | 52 +-
.../hyperbridge/ui/components/EmptyState.kt | 4 +-
.../ui/components/EnginePreview.kt | 292 +++++
.../ui/components/HyperOSColorPicker.kt | 46 -
.../ui/components/IslandSettingsControl.kt | 353 +++--
.../ui/components/NavPreviewContent.kt | 215 ++++
.../ui/components/PermanentIslandPreview.kt | 89 ++
.../ui/components/PriorityEducationDialog.kt | 62 -
.../hyperbridge/ui/navigation/NavGraph.kt | 150 +++
.../ui/navigation/NavigationState.kt | 94 ++
.../hyperbridge/ui/navigation/Navigator.kt | 59 +
.../hyperbridge/ui/navigation/Screen.kt | 27 +
.../ui/screens/design/DesignScreen.kt | 12 +-
.../screens/design/SavedAppWidgetsScreen.kt | 15 +-
.../ui/screens/design/WidgetConfigScreen.kt | 19 +-
.../ui/screens/design/WidgetPickerScreen.kt | 24 +-
.../screens/design/WidgetPickerViewModel.kt | 57 -
.../ui/screens/onboarding/OnboardingScreen.kt | 1144 ++++++++++++++---
.../ui/screens/settings/AppPriorityScreen.kt | 6 +-
.../settings/ChangelogHistoryScreen.kt | 3 +-
.../ui/screens/settings/DndSettingsScreen.kt | 104 ++
.../screens/settings/EngineSettingsScreen.kt | 219 ++++
.../screens/settings/GlobalSettingsScreen.kt | 188 ++-
.../ui/screens/settings/InfoScreen.kt | 1 +
.../screens/settings/IslandSettingsScreen.kt | 99 ++
.../ui/screens/settings/LicensesScreen.kt | 23 +-
.../settings/NavCustomizationScreen.kt | 259 +---
.../settings/PermanentIslandConfigScreen.kt | 181 +++
.../ui/screens/theme/AppThemeEditorScreen.kt | 249 +++-
.../ui/screens/theme/ThemeCreatorScreen.kt | 234 +++-
.../screens/theme/ThemeInstallerActivity.kt | 6 -
.../ui/screens/theme/ThemePreviews.kt | 4 +-
.../ui/screens/theme/ThemeShared.kt | 5 +-
.../ui/screens/theme/ThemeViewModel.kt | 48 +-
.../theme/content/ColorsDetailContent.kt | 6 +-
.../theme/content/EngineThemeContent.kt | 176 +++
.../theme/content/ThemeSettingsContent.kt | 241 ++++
.../hyperbridge/util/DeviceCompatibility.kt | 11 -
.../d4viddf/hyperbridge/util/DeviceUtils.kt | 6 +-
.../hyperbridge/util/ImageExtensions.kt | 3 +-
.../hyperbridge/util/ShizukuManager.kt | 177 +++
.../d4viddf/hyperbridge/util/SystemIntents.kt | 24 +-
.../layout/layout_island_widget_snapshot.xml | 3 +-
.../ic_launcher.xml | 1 +
.../ic_launcher_round.xml | 1 +
app/src/main/res/values-ca-rES/strings.xml | 2 +
app/src/main/res/values-cs-rCZ/strings.xml | 125 +-
app/src/main/res/values-de-rDE/strings.xml | 125 +-
app/src/main/res/values-en-rUS/strings.xml | 257 +++-
app/src/main/res/values-es-rEM/strings.xml | 25 +
app/src/main/res/values-es-rES/strings.xml | 127 +-
app/src/main/res/values-fr-rFR/strings.xml | 673 ++++++++++
app/src/main/res/values-gl-rES/strings.xml | 2 +
app/src/main/res/values-hu-rHU/strings.xml | 139 +-
app/src/main/res/values-in-rID/strings.xml | 147 ++-
app/src/main/res/values-it-rIT/strings.xml | 135 +-
app/src/main/res/values-ko-rKR/strings.xml | 125 +-
app/src/main/res/values-pl-rPL/strings.xml | 125 +-
app/src/main/res/values-pt-rBR/strings.xml | 133 +-
app/src/main/res/values-ru-rRU/strings.xml | 125 +-
app/src/main/res/values-sk-rSK/strings.xml | 125 +-
app/src/main/res/values-tr-rTR/strings.xml | 125 +-
app/src/main/res/values-uk-rUA/strings.xml | 123 ++
app/src/main/res/values-zh-rTW/strings.xml | 151 ++-
app/src/main/res/values/strings.xml | 120 +-
build.gradle.kts | 1 -
gradle.properties | 8 +-
gradle/libs.versions.toml | 33 +-
gradle/wrapper/gradle-wrapper.properties | 2 +-
settings.gradle.kts | 3 +
114 files changed, 9165 insertions(+), 1630 deletions(-)
create mode 100644 app/src/main/aidl/com/d4viddf/hyperbridge/IPrivilegedLogCallback.aidl
create mode 100644 app/src/main/aidl/com/d4viddf/hyperbridge/IPrivilegedService.aidl
create mode 100644 app/src/main/java/com/d4viddf/hyperbridge/data/ConfigResolver.kt
create mode 100644 app/src/main/java/com/d4viddf/hyperbridge/integration/shizuku/PrivilegedServiceImpl.kt
create mode 100644 app/src/main/java/com/d4viddf/hyperbridge/integration/shizuku/ShizukuHook.kt
create mode 100644 app/src/main/java/com/d4viddf/hyperbridge/integration/shizuku/ShizukuUserServiceRecycler.kt
create mode 100644 app/src/main/java/com/d4viddf/hyperbridge/integration/shizuku/ShizukuUtil.kt
create mode 100644 app/src/main/java/com/d4viddf/hyperbridge/integration/shizuku/XmsfNetworkHelper.kt
create mode 100644 app/src/main/java/com/d4viddf/hyperbridge/service/PermanentIslandManager.kt
create mode 100644 app/src/main/java/com/d4viddf/hyperbridge/service/translators/DownloadTranslator.kt
delete mode 100644 app/src/main/java/com/d4viddf/hyperbridge/ui/components/ChangelogDialog.kt
create mode 100644 app/src/main/java/com/d4viddf/hyperbridge/ui/components/EnginePreview.kt
create mode 100644 app/src/main/java/com/d4viddf/hyperbridge/ui/components/NavPreviewContent.kt
create mode 100644 app/src/main/java/com/d4viddf/hyperbridge/ui/components/PermanentIslandPreview.kt
delete mode 100644 app/src/main/java/com/d4viddf/hyperbridge/ui/components/PriorityEducationDialog.kt
create mode 100644 app/src/main/java/com/d4viddf/hyperbridge/ui/navigation/NavGraph.kt
create mode 100644 app/src/main/java/com/d4viddf/hyperbridge/ui/navigation/NavigationState.kt
create mode 100644 app/src/main/java/com/d4viddf/hyperbridge/ui/navigation/Navigator.kt
create mode 100644 app/src/main/java/com/d4viddf/hyperbridge/ui/navigation/Screen.kt
delete mode 100644 app/src/main/java/com/d4viddf/hyperbridge/ui/screens/design/WidgetPickerViewModel.kt
create mode 100644 app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/DndSettingsScreen.kt
create mode 100644 app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/EngineSettingsScreen.kt
create mode 100644 app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/IslandSettingsScreen.kt
create mode 100644 app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/PermanentIslandConfigScreen.kt
create mode 100644 app/src/main/java/com/d4viddf/hyperbridge/ui/screens/theme/content/EngineThemeContent.kt
create mode 100644 app/src/main/java/com/d4viddf/hyperbridge/ui/screens/theme/content/ThemeSettingsContent.kt
delete mode 100644 app/src/main/java/com/d4viddf/hyperbridge/util/DeviceCompatibility.kt
create mode 100644 app/src/main/java/com/d4viddf/hyperbridge/util/ShizukuManager.kt
rename app/src/main/res/{mipmap-anydpi-v26 => mipmap-anydpi}/ic_launcher.xml (79%)
rename app/src/main/res/{mipmap-anydpi-v26 => mipmap-anydpi}/ic_launcher_round.xml (79%)
create mode 100644 app/src/main/res/values-fr-rFR/strings.xml
diff --git a/.gitignore b/.gitignore
index aa724b7..a382a9c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,15 +1,64 @@
+# Built application files
+*.apk
+*.aar
+*.ap_
+*.aab
+
+# Files for the ART/Dalvik VM
+*.dex
+
+# Java class files
+*.class
+
+# Generated files
+bin/
+gen/
+out/
+
+# Gradle files
+.gradle/
+build/
+
+# Local configuration file (sdk path, etc)
+local.properties
+
+# Proguard folder generated by Eclipse
+proguard/
+
+# Log Files
+*.log
+
+# Android Studio Navigation editor temp files
+.navigation/
+
+# Android Studio captures folder
+captures/
+
+# IntelliJ
*.iml
-.gradle
-/local.properties
-/.idea/caches
-/.idea/libraries
-/.idea/modules.xml
-/.idea/workspace.xml
-/.idea/navEditor.xml
-/.idea/assetWizardSettings.xml
-.DS_Store
-/build
-/captures
+.idea/
+!.idea/codeStyles/
+!.idea/runConfigurations/
+!.idea/vcs.xml
+
+# Keystore files
+*.jks
+*.keystore
+
+# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
-.cxx
-local.properties
+.cxx/
+
+# Mac OS X
+.DS_Store
+
+# Compose compiler metrics and reports
+*compose_metrics.txt
+*compose_reports.txt
+
+# Custom HyperBridge project additions
+caps_build.txt
+gradle/gradle-daemon-jvm.properties
+.idea/deploymentTargetSelector.xml
+.idea/deploymentTargetSelector.xml
+.idea/misc.xml
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
index b268ef3..ca16a99 100644
--- a/.idea/deploymentTargetSelector.xml
+++ b/.idea/deploymentTargetSelector.xml
@@ -4,6 +4,7 @@
+
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 639c779..02c4aa5 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -6,7 +6,6 @@
-
-
+
@@ -31,18 +31,21 @@
## 🚀 Features
* **Native Visuals:** Transforms notifications into HyperOS system-style islands.
-* **🎨 Theme Engine (New):** Customize every pixel.
+* **🎨 Theme Engine:** Customize every pixel.
* **Theme Creator:** Built-in editor to design your own themes with real-time previews.
* **Smart Colors:** Automatically extract vibrant brand colors from app icons.
* **Icon Shaping:** Choose between shapes like *Squircle*, *Clover*, *Arch*, and *Cookie*.
* **Granular Control:** Per-app overrides for colors, icons, and action styles.
-* **🧩 Widgets (New):** Pin standard Android widgets to the island layer for quick access—even on the Lockscreen!
+ * **Smart Icon Tinting:** Intelligently tints dark/monochrome icons to remain visible.
+* **🧩 Widgets:** Pin standard Android widgets to the island layer for quick access—even on the Lockscreen!
+* **🧠 Intelligent Permanent Island:** The permanent island now automatically hides itself whenever an active widget is shown on the screen, preventing awkward overlaps.
* **Smart Integration:**
* **🎵 Media:** Show album art and "Now Playing" status with visualizer support.
* **🧭 Navigation:** Real-time turn-by-turn instructions (Google Maps, Waze).
- * **⬇️ Downloads:** Circular progress ring with a satisfying "Green Tick" animation upon completion.
+ * **⬇️ Downloads:** Dedicated circular progress layout with a satisfying "Green Tick" animation upon completion.
* **📞 Calls:** Dedicated layout for incoming and active calls with timers.
* **🛡️ Spoiler Protection:** Define blocked terms globally or per-app to prevent specific notifications (e.g., message spoilers) from popping up on the Island.
+* **Sui & Shizuku Support:** Fully supports Sui and Shizuku for enhanced network operations and seamless integration on rooted devices.
* **Total Control:** Choose exactly which apps trigger the island, customize timeouts, and toggle floating behavior per app.
## 👩💻 For Developers: Create Themes
diff --git a/app/.gitignore b/app/.gitignore
index 42afabf..2bdf0f6 100644
--- a/app/.gitignore
+++ b/app/.gitignore
@@ -1 +1,3 @@
-/build
\ No newline at end of file
+/build
+/debug
+/release
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 03438ff..871c684 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,9 +1,8 @@
plugins {
alias(libs.plugins.android.application)
- alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.ksp)
- id("org.jetbrains.kotlin.plugin.serialization") version "2.3.0"
+ id("org.jetbrains.kotlin.plugin.serialization") version "2.3.21"
}
android {
@@ -15,10 +14,9 @@ android {
defaultConfig {
applicationId = "com.d4viddf.hyperbridge"
minSdk = 35
- targetSdk = 36
- versionCode = 16
- versionName = "0.5.0-alpha01"
-
+ targetSdk = 37
+ versionCode = 23
+ versionName = "0.5.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -37,12 +35,8 @@ android {
}
buildFeatures {
compose = true
- }
-}
-
-kotlin {
- compilerOptions {
- jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11)
+ buildConfig = true
+ aidl = true
}
}
@@ -59,7 +53,6 @@ dependencies {
implementation(libs.androidx.ui)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.palette.ktx)
- implementation(libs.androidx.material3)
ksp(libs.androidx.room.compiler)
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx)
@@ -77,8 +70,17 @@ dependencies {
implementation(libs.gson)
implementation(libs.kotlinx.serialization.json)
+
+ // Navigation 3
+ implementation(libs.androidx.navigation3.runtime)
+ implementation(libs.androidx.navigation3.ui)
+ implementation(libs.androidx.lifecycle.viewmodel.navigation3)
+
+ // Shizuku
+ implementation(libs.api)
+ implementation(libs.provider)
}
configurations.all {
exclude(group = "com.intellij", module = "annotations")
-}
\ No newline at end of file
+}
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 481bb43..a35b8f1 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -18,4 +18,12 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
-#-renamesourcefileattribute SourceFile
\ No newline at end of file
+#-renamesourcefileattribute SourceFile
+
+# Strip Debug and Info logs in Release (AGP 9.1+ named level syntax)
+-assumenosideeffects class android.util.Log {
+ public static boolean isLoggable(java.lang.String, int);
+ public static int v(...);
+ public static int d(...);
+ public static int i(...);
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 2d4f0b6..ad8c027 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -4,12 +4,12 @@
-
+ tools:ignore="PackageVisibilityPolicy,QueryAllPackagesPermission" />
+
@@ -22,6 +22,7 @@
+
+
@@ -108,6 +111,19 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/aidl/com/d4viddf/hyperbridge/IPrivilegedLogCallback.aidl b/app/src/main/aidl/com/d4viddf/hyperbridge/IPrivilegedLogCallback.aidl
new file mode 100644
index 0000000..ad82fbf
--- /dev/null
+++ b/app/src/main/aidl/com/d4viddf/hyperbridge/IPrivilegedLogCallback.aidl
@@ -0,0 +1,5 @@
+package com.d4viddf.hyperbridge;
+
+interface IPrivilegedLogCallback {
+ void log(int level, String tag, String message);
+}
diff --git a/app/src/main/aidl/com/d4viddf/hyperbridge/IPrivilegedService.aidl b/app/src/main/aidl/com/d4viddf/hyperbridge/IPrivilegedService.aidl
new file mode 100644
index 0000000..c6069fb
--- /dev/null
+++ b/app/src/main/aidl/com/d4viddf/hyperbridge/IPrivilegedService.aidl
@@ -0,0 +1,8 @@
+package com.d4viddf.hyperbridge;
+
+import com.d4viddf.hyperbridge.IPrivilegedLogCallback;
+
+interface IPrivilegedService {
+ void setLogCallback(IPrivilegedLogCallback callback);
+ boolean setPackageNetworkingEnabled(int uid, boolean enabled);
+}
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/MainActivity.kt b/app/src/main/java/com/d4viddf/hyperbridge/MainActivity.kt
index ace23f1..f987040 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/MainActivity.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/MainActivity.kt
@@ -1,20 +1,22 @@
package com.d4viddf.hyperbridge
+import android.content.Context
import android.os.Bundle
-import android.widget.Toast
+import androidx.activity.BackEventCompat
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
-import androidx.compose.animation.AnimatedContent
-import androidx.compose.animation.SizeTransform
+import androidx.compose.animation.EnterTransition
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
+import androidx.compose.animation.scaleOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
@@ -29,29 +31,23 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.navigation3.ui.NavDisplay
import com.d4viddf.hyperbridge.data.AppPreferences
import com.d4viddf.hyperbridge.data.db.AppDatabase
-import com.d4viddf.hyperbridge.data.model.HyperBridgeBackup
import com.d4viddf.hyperbridge.ui.components.ChangelogSheet
-import com.d4viddf.hyperbridge.ui.components.PriorityEducationDialog
-import com.d4viddf.hyperbridge.ui.screens.home.HomeScreen
-import com.d4viddf.hyperbridge.ui.screens.onboarding.OnboardingScreen
-import com.d4viddf.hyperbridge.ui.screens.settings.AppPriorityScreen
-import com.d4viddf.hyperbridge.ui.screens.settings.BackupSettingsScreen
-import com.d4viddf.hyperbridge.ui.screens.settings.BlocklistAppListScreen
-import com.d4viddf.hyperbridge.ui.screens.settings.ChangelogHistoryScreen
-import com.d4viddf.hyperbridge.ui.screens.settings.GlobalBlocklistScreen
-import com.d4viddf.hyperbridge.ui.screens.settings.GlobalSettingsScreen
-import com.d4viddf.hyperbridge.ui.screens.settings.ImportPreviewScreen
-import com.d4viddf.hyperbridge.ui.screens.settings.InfoScreen
-import com.d4viddf.hyperbridge.ui.screens.settings.LicensesScreen
-import com.d4viddf.hyperbridge.ui.screens.settings.NavCustomizationScreen
-import com.d4viddf.hyperbridge.ui.screens.settings.PrioritySettingsScreen
-import com.d4viddf.hyperbridge.ui.screens.settings.SetupHealthScreen
+import com.d4viddf.hyperbridge.ui.navigation.Navigator
+import com.d4viddf.hyperbridge.ui.navigation.Screen
+import com.d4viddf.hyperbridge.ui.navigation.mainNavGraph
+import com.d4viddf.hyperbridge.ui.navigation.rememberNavigationState
+import com.d4viddf.hyperbridge.ui.navigation.toEntries
import com.d4viddf.hyperbridge.ui.theme.HyperBridgeTheme
import com.d4viddf.hyperbridge.util.BackupManager
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
@@ -60,199 +56,153 @@ class MainActivity : AppCompatActivity() {
setContent {
HyperBridgeTheme {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
- MainRootNavigation()
+ MainRootNavigation(onExit = { finish() })
}
}
}
}
}
-enum class Screen(val depth: Int) {
- ONBOARDING(0), HOME(1), INFO(2), SETUP(3), LICENSES(3), BEHAVIOR(3), GLOBAL_SETTINGS(3), HISTORY(3),
- BACKUP(3), IMPORT_PREVIEW(4), // Backup Flow
- NAV_CUSTOMIZATION(4), APP_PRIORITY(4), GLOBAL_BLOCKLIST(4), BLOCKLIST_APPS(5)
-}
-
@Composable
-fun MainRootNavigation() {
+fun MainRootNavigation(onExit: () -> Unit) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val preferences = remember { AppPreferences(context) }
- // --- 1. INITIALIZE DB & MANAGER ---
val database = remember { AppDatabase.getDatabase(context) }
val backupManager = remember { BackupManager(context, preferences, database) }
- val packageInfo = remember { try { context.packageManager.getPackageInfo(context.packageName, 0) } catch (e: Exception) { null } }
+ val packageInfo = remember { try { context.packageManager.getPackageInfo(context.packageName, 0) } catch (_: Exception) { null } }
@Suppress("DEPRECATION")
val currentVersionCode = packageInfo?.longVersionCode?.toInt() ?: 0
- val currentVersionName = packageInfo?.versionName ?: "0.4.2"
+ val currentVersionName = packageInfo?.versionName ?: "0.5.0"
- // --- 2. ROBUST DATA COLLECTION ---
val isSetupComplete by produceState(initialValue = null) {
preferences.isSetupComplete.collect { value = it }
}
- val lastSeenVersion by preferences.lastSeenVersion.collectAsState(initial = currentVersionCode)
- val isPriorityEduShown by preferences.isPriorityEduShown.collectAsState(initial = true)
+ if (isSetupComplete == null) {
+ Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ CircularProgressIndicator()
+ }
+ } else {
+ MainNavigationContent(
+ isSetupComplete = isSetupComplete!!,
+ context = context,
+ scope = scope,
+ preferences = preferences,
+ backupManager = backupManager,
+ currentVersionCode = currentVersionCode,
+ currentVersionName = currentVersionName,
+ onExit = onExit
+ )
+ }
+}
+
+@Composable
+private fun MainNavigationContent(
+ isSetupComplete: Boolean,
+ context: Context,
+ scope: CoroutineScope,
+ preferences: AppPreferences,
+ backupManager: BackupManager,
+ currentVersionCode: Int,
+ currentVersionName: String,
+ onExit: () -> Unit
+) {
+ val isInitiallySetup = remember { isSetupComplete }
+ val lastSeenVersion by preferences.lastSeenVersion.collectAsState(initial = -1)
- var currentScreen by remember { mutableStateOf(null) }
var showChangelog by remember { mutableStateOf(false) }
- var showPriorityEdu by remember { mutableStateOf(false) }
- var navConfigPackage by remember { mutableStateOf(null) }
- // State to hold the parsed backup file before restoring
- var pendingImportBackup by remember { mutableStateOf(null) }
+ val initialStartRoute = remember(isSetupComplete) { if (isSetupComplete) Screen.Home else Screen.Onboarding }
+ val allPossibleTopLevel = remember(isSetupComplete) { setOf(Screen.Onboarding, Screen.Home) }
- // --- 3. ROUTING LOGIC ---
- LaunchedEffect(isSetupComplete) {
- if (isSetupComplete != null) {
- if (currentScreen == null) {
- currentScreen = if (isSetupComplete == true) Screen.HOME else Screen.ONBOARDING
- }
+ val navigationState = rememberNavigationState(
+ startRoute = initialStartRoute,
+ topLevelRoutes = allPossibleTopLevel
+ )
+ val navigator = remember(navigationState) { Navigator(navigationState) }
- if (isSetupComplete == true) {
- if (currentVersionCode > lastSeenVersion) {
- showChangelog = true
- } else if (!isPriorityEduShown && !showChangelog) {
- showPriorityEdu = true
- }
+ LaunchedEffect(isSetupComplete, lastSeenVersion) {
+ if (isSetupComplete && isInitiallySetup && lastSeenVersion != -1) {
+ if (currentVersionCode > lastSeenVersion) {
+ showChangelog = true
}
}
}
- // --- 4. BACK HANDLER ---
- BackHandler(enabled = currentScreen != Screen.HOME && currentScreen != Screen.ONBOARDING) {
- currentScreen = when (currentScreen) {
- Screen.IMPORT_PREVIEW -> Screen.BACKUP
- Screen.BACKUP -> Screen.INFO
- Screen.BLOCKLIST_APPS -> Screen.GLOBAL_BLOCKLIST
- Screen.GLOBAL_BLOCKLIST -> Screen.INFO
- Screen.NAV_CUSTOMIZATION -> if (navConfigPackage != null) Screen.HOME else Screen.GLOBAL_SETTINGS
- Screen.GLOBAL_SETTINGS -> Screen.INFO
- Screen.APP_PRIORITY -> Screen.BEHAVIOR
- Screen.HISTORY -> Screen.INFO
- Screen.BEHAVIOR, Screen.SETUP, Screen.LICENSES -> Screen.INFO
- Screen.INFO -> Screen.HOME
- else -> Screen.HOME
+ val entryProvider = mainNavGraph(
+ context = context,
+ scope = scope,
+ preferences = preferences,
+ navigator = navigator,
+ backupManager = backupManager,
+ currentVersionCode = currentVersionCode,
+ onExit = onExit
+ )
+
+ BackHandler {
+ if (!navigator.goBack()) {
+ onExit()
}
}
- // --- 5. RENDER SCREENS ---
- if (isSetupComplete == null || currentScreen == null) {
- Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
- CircularProgressIndicator()
- }
- } else {
- AnimatedContent(
- targetState = currentScreen!!,
- transitionSpec = {
- if (targetState.depth > initialState.depth) {
- (slideInHorizontally { width -> width } + fadeIn(tween(400))).togetherWith(slideOutHorizontally { width -> -width / 3 } + fadeOut(tween(400)))
- } else {
- (slideInHorizontally { width -> -width } + fadeIn(tween(400))).togetherWith(slideOutHorizontally { width -> width / 3 } + fadeOut(tween(400)))
- } using SizeTransform(clip = false)
- },
- label = "ScreenTransition"
- ) { target ->
- when (target) {
- Screen.ONBOARDING -> OnboardingScreen {
- scope.launch {
- preferences.setSetupComplete(true)
- preferences.setLastSeenVersion(currentVersionCode)
- preferences.setPriorityEduShown(true)
- currentScreen = Screen.HOME
- }
- }
- Screen.HOME -> HomeScreen(
- onSettingsClick = { currentScreen = Screen.INFO },
- onNavConfigClick = { pkg -> navConfigPackage = pkg; currentScreen = Screen.NAV_CUSTOMIZATION }
- )
- Screen.INFO -> InfoScreen(
- onBack = { currentScreen = Screen.HOME },
- onSetupClick = { currentScreen = Screen.SETUP },
- onLicensesClick = { currentScreen = Screen.LICENSES },
- onBehaviorClick = { currentScreen = Screen.BEHAVIOR },
- onGlobalSettingsClick = { currentScreen = Screen.GLOBAL_SETTINGS },
- onHistoryClick = { currentScreen = Screen.HISTORY },
- onBlocklistClick = { currentScreen = Screen.GLOBAL_BLOCKLIST },
- onBackupClick = { currentScreen = Screen.BACKUP }
- )
- Screen.GLOBAL_SETTINGS -> GlobalSettingsScreen(onBack = { currentScreen = Screen.INFO }, onNavSettingsClick = { navConfigPackage = null; currentScreen = Screen.NAV_CUSTOMIZATION })
- Screen.NAV_CUSTOMIZATION -> NavCustomizationScreen(onBack = { currentScreen = if (navConfigPackage != null) Screen.HOME else Screen.GLOBAL_SETTINGS }, packageName = navConfigPackage)
- Screen.SETUP -> SetupHealthScreen(onBack = { currentScreen = Screen.INFO })
- Screen.LICENSES -> LicensesScreen(onBack = { currentScreen = Screen.INFO })
- Screen.BEHAVIOR -> PrioritySettingsScreen(onBack = { currentScreen = Screen.INFO }, onNavigateToPriorityList = { currentScreen = Screen.APP_PRIORITY })
- Screen.APP_PRIORITY -> AppPriorityScreen(onBack = { currentScreen = Screen.BEHAVIOR })
- Screen.HISTORY -> ChangelogHistoryScreen(onBack = { currentScreen = Screen.INFO })
-
- Screen.GLOBAL_BLOCKLIST -> GlobalBlocklistScreen(
- onBack = { currentScreen = Screen.INFO },
- onNavigateToAppList = { currentScreen = Screen.BLOCKLIST_APPS }
- )
- Screen.BLOCKLIST_APPS -> BlocklistAppListScreen(
- onBack = { currentScreen = Screen.GLOBAL_BLOCKLIST }
- )
-
- // --- BACKUP FLOW ---
- Screen.BACKUP -> BackupSettingsScreen(
- onBack = { currentScreen = Screen.INFO },
- backupManager = backupManager,
- onBackupFileLoaded = { backup ->
- pendingImportBackup = backup
- currentScreen = Screen.IMPORT_PREVIEW
- }
- )
-
- Screen.IMPORT_PREVIEW -> {
- if (pendingImportBackup != null) {
- ImportPreviewScreen(
- backupData = pendingImportBackup!!,
- onBack = { currentScreen = Screen.BACKUP },
- onConfirmRestore = { selection ->
- scope.launch {
- val result = backupManager.restoreBackup(pendingImportBackup!!, selection)
- if (result.isSuccess) {
- Toast.makeText(context, context.getString(R.string.import_success), Toast.LENGTH_LONG).show()
- // Force restart navigation to reflect changes immediately
- currentScreen = Screen.HOME
- } else {
- Toast.makeText(context, context.getString(R.string.import_failed, result.exceptionOrNull()?.message), Toast.LENGTH_LONG).show()
- }
- }
- }
- )
- } else {
- // Fallback logic
- LaunchedEffect(Unit) { currentScreen = Screen.BACKUP }
- }
- }
+ NavDisplay(
+ entries = navigationState.toEntries(entryProvider),
+ onBack = {
+ if (!navigator.goBack()) {
+ onExit()
}
- }
- }
+ },
+ transitionSpec = {
+ slideInHorizontally(
+ initialOffsetX = { it },
+ animationSpec = tween(400)
+ ) + fadeIn(animationSpec = tween(400)) togetherWith slideOutHorizontally(
+ targetOffsetX = { -it / 3 },
+ animationSpec = tween(400)
+ ) + fadeOut(animationSpec = tween(400))
+ },
+ popTransitionSpec = {
+ slideInHorizontally(
+ initialOffsetX = { -it / 3 },
+ animationSpec = tween(400)
+ ) + fadeIn(animationSpec = tween(400)) togetherWith slideOutHorizontally(
+ targetOffsetX = { it },
+ animationSpec = tween(400)
+ ) + fadeOut(animationSpec = tween(400))
+ },
+ predictivePopTransitionSpec = { swipeEdge ->
+ val origin = if (swipeEdge == BackEventCompat.EDGE_LEFT) {
+ TransformOrigin(0.92f, 0.5f)
+ } else {
+ TransformOrigin(0.08f, 0.5f)
+ }
+ EnterTransition.None togetherWith (
+ scaleOut(
+ targetScale = 0.86f,
+ transformOrigin = origin
+ ) + slideOutHorizontally(
+ targetOffsetX = { if (swipeEdge == BackEventCompat.EDGE_LEFT) -it / 30 else it / 30 }
+ ) + fadeOut()
+ )
+ },
+ modifier = Modifier
+ .fillMaxSize()
+ .clip(RoundedCornerShape(32.dp))
+ )
if (showChangelog) {
- ChangelogSheet (
+ ChangelogSheet(
currentVersionName = currentVersionName,
- changelogText = stringResource(R.string.changelog_0_4_0),
+ changelogText = stringResource(R.string.changelog_0_5_0),
onDismiss = {
showChangelog = false
scope.launch {
preferences.setLastSeenVersion(currentVersionCode)
- if (!isPriorityEduShown) showPriorityEdu = true
}
}
)
}
-
- if (showPriorityEdu) {
- PriorityEducationDialog(
- onDismiss = { showPriorityEdu = false; scope.launch { preferences.setPriorityEduShown(true) } },
- onConfigure = {
- showPriorityEdu = false
- scope.launch { preferences.setPriorityEduShown(true) }
- currentScreen = Screen.BEHAVIOR
- }
- )
- }
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/data/AppCacheManager.kt b/app/src/main/java/com/d4viddf/hyperbridge/data/AppCacheManager.kt
index e53bd1d..4f87191 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/data/AppCacheManager.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/data/AppCacheManager.kt
@@ -11,7 +11,7 @@ import androidx.core.content.edit
/**
* Persists App Names and Icons so they can be displayed even after the app is uninstalled.
*/
-class AppCacheManager(private val context: Context) {
+class AppCacheManager(context: Context) {
private val prefs = context.getSharedPreferences("app_metadata_cache", Context.MODE_PRIVATE)
private val iconsDir = File(context.filesDir, "cached_icons").apply { mkdirs() }
@@ -24,7 +24,7 @@ class AppCacheManager(private val context: Context) {
if (icon != null) {
try {
val file = File(iconsDir, "$packageName.png")
- // Only save if it doesn't exist or we want to update it (optional optimization)
+ // Only save if it doesn't exist, or we want to update it (optional optimization)
if (!file.exists()) {
FileOutputStream(file).use { out ->
icon.compress(Bitmap.CompressFormat.PNG, 100, out)
@@ -45,7 +45,7 @@ class AppCacheManager(private val context: Context) {
return if (file.exists()) {
try {
BitmapFactory.decodeFile(file.absolutePath)
- } catch (e: Exception) {
+ } catch (_: Exception) {
null
}
} else {
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/data/AppPreferences.kt b/app/src/main/java/com/d4viddf/hyperbridge/data/AppPreferences.kt
index abbdf7f..c652feb 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/data/AppPreferences.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/data/AppPreferences.kt
@@ -34,6 +34,13 @@ class AppPreferences(context: Context) {
// --- MIGRATION LOGIC ---
CoroutineScope(Dispatchers.IO).launch {
try {
+ // Force Onboarding reset for version 0.5.0 (versionCode 17)
+ val lastResetVersion = dao.getSetting("onboarding_reset_version")?.toIntOrNull() ?: 0
+ if (lastResetVersion < 18) {
+ dao.insert(AppSetting(SettingsKeys.SETUP_COMPLETE, "false"))
+ dao.insert(AppSetting("onboarding_reset_version", "18"))
+ }
+
val isMigrated = dao.getSetting(SettingsKeys.MIGRATION_COMPLETE) == "true"
if (!isMigrated) {
val legacyPrefs = legacyDataStore.data.first().asMap()
@@ -49,6 +56,36 @@ class AppPreferences(context: Context) {
}
dao.insert(AppSetting(SettingsKeys.MIGRATION_COMPLETE, "true"))
}
+
+ // Grant DOWNLOAD notification type if PROGRESS was previously enabled
+ val isDownloadMigrated = dao.getSetting("download_type_migration_complete") == "true"
+ if (!isDownloadMigrated) {
+ // 1. Global notification types migration
+ val globalTypesStr = dao.getSetting(GLOBAL_NOTIFICATION_TYPES_KEY)
+ if (globalTypesStr != null) {
+ val globalTypes = globalTypesStr.deserializeSet()
+ if (globalTypes.contains("PROGRESS") && !globalTypes.contains("DOWNLOAD")) {
+ val newGlobalTypes = globalTypes + "DOWNLOAD"
+ dao.insert(AppSetting(GLOBAL_NOTIFICATION_TYPES_KEY, newGlobalTypes.serialize()))
+ }
+ }
+
+ // 2. App-specific notification types migration
+ val suffixes = listOf("_float", "_shade", "_timeout", "_float_timeout", "_remove_notif", "_blocked", "_nav_left", "_nav_right", "_use_native")
+ val allSettings = dao.getAllSync()
+ allSettings.forEach { setting ->
+ val key = setting.key
+ if (key.startsWith("config_") && suffixes.none { key.endsWith(it) }) {
+ val types = setting.value.deserializeSet()
+ if (types.contains("PROGRESS") && !types.contains("DOWNLOAD")) {
+ val newTypes = types + "DOWNLOAD"
+ dao.insert(AppSetting(key, newTypes.serialize()))
+ }
+ }
+ }
+
+ dao.insert(AppSetting("download_type_migration_complete", "true"))
+ }
} catch (e: Exception) {
e.printStackTrace()
}
@@ -76,7 +113,6 @@ class AppPreferences(context: Context) {
val allowedPackagesFlow: Flow> = dao.getSettingFlow(SettingsKeys.ALLOWED_PACKAGES).map { it.deserializeSet() }
val isSetupComplete: Flow = dao.getSettingFlow(SettingsKeys.SETUP_COMPLETE).map { it.toBoolean(false) }
val lastSeenVersion: Flow = dao.getSettingFlow(SettingsKeys.LAST_VERSION).map { it.toInt(0) }
- val isPriorityEduShown: Flow = dao.getSettingFlow(SettingsKeys.PRIORITY_EDU).map { it.toBoolean(false) }
suspend fun setSetupComplete(isComplete: Boolean) = save(SettingsKeys.SETUP_COMPLETE, isComplete.toString())
suspend fun setLastSeenVersion(versionCode: Int) = save(SettingsKeys.LAST_VERSION, versionCode.toString())
@@ -90,13 +126,9 @@ class AppPreferences(context: Context) {
}
// ========================================================================
- // THEME ENGINE (NEW)
+ // THEME ENGINE
// ========================================================================
- /**
- * Holds the ID (folder name) of the currently active theme.
- * Null means the system default (no theme).
- */
val activeThemeIdFlow: Flow = dao.getSettingFlow("active_theme_id")
suspend fun setActiveThemeId(id: String?) {
@@ -107,11 +139,9 @@ class AppPreferences(context: Context) {
}
}
- // ========================================================================
-
// --- LIMITS & PRIORITY ---
val limitModeFlow: Flow = dao.getSettingFlow("limit_mode").map {
- try { IslandLimitMode.valueOf(it ?: IslandLimitMode.MOST_RECENT.name) } catch(e: Exception) { IslandLimitMode.MOST_RECENT }
+ try { IslandLimitMode.valueOf(it ?: IslandLimitMode.MOST_RECENT.name) } catch(_: Exception) { IslandLimitMode.MOST_RECENT }
}
val appPriorityListFlow: Flow> = dao.getSettingFlow(SettingsKeys.PRIORITY_ORDER).map { it.deserializeList() }
@@ -126,14 +156,6 @@ class AppPreferences(context: Context) {
}
}
- suspend fun updateAppConfig(packageName: String, type: NotificationType, isEnabled: Boolean) {
- val key = "config_$packageName"
- val currentStr = dao.getSetting(key)
- val currentSet = currentStr?.deserializeSet() ?: NotificationType.entries.map { it.name }.toSet()
- val newSet = if (isEnabled) currentSet + type.name else currentSet - type.name
- save(key, newSet.serialize())
- }
-
// --- ISLAND CONFIG (Standard Notifications) ---
private fun sanitizeTimeout(raw: Long?): Long {
val value = raw ?: 5L
@@ -143,27 +165,41 @@ class AppPreferences(context: Context) {
val globalConfigFlow: Flow = combine(
dao.getSettingFlow(SettingsKeys.GLOBAL_FLOAT),
dao.getSettingFlow(SettingsKeys.GLOBAL_SHADE),
- dao.getSettingFlow(SettingsKeys.GLOBAL_TIMEOUT)
- ) { f, s, t ->
- IslandConfig(f.toBoolean(true), s.toBoolean(true), t?.toIntOrNull())
+ dao.getSettingFlow(SettingsKeys.GLOBAL_TIMEOUT),
+ dao.getSettingFlow(SettingsKeys.GLOBAL_FLOAT_TIMEOUT),
+ dao.getSettingFlow(SettingsKeys.GLOBAL_REMOVE_NOTIF)
+ ) { f, s, t, ft, rn ->
+ IslandConfig(
+ f.toBoolean(true),
+ s.toBoolean(true),
+ t?.toIntOrNull(),
+ ft?.toIntOrNull(),
+ rn?.toBoolean()
+ )
}
suspend fun updateGlobalConfig(config: IslandConfig) {
config.isFloat?.let { save(SettingsKeys.GLOBAL_FLOAT, it.toString()) }
config.isShowShade?.let { save(SettingsKeys.GLOBAL_SHADE, it.toString()) }
config.timeout?.let { save(SettingsKeys.GLOBAL_TIMEOUT, it.toString()) }
+ config.floatTimeout?.let { save(SettingsKeys.GLOBAL_FLOAT_TIMEOUT, it.toString()) }
+ config.removeOriginalNotification?.let { save(SettingsKeys.GLOBAL_REMOVE_NOTIF, it.toString()) }
}
fun getAppIslandConfig(packageName: String): Flow {
return combine(
dao.getSettingFlow("config_${packageName}_float"),
dao.getSettingFlow("config_${packageName}_shade"),
- dao.getSettingFlow("config_${packageName}_timeout")
- ) { f, s, t ->
+ dao.getSettingFlow("config_${packageName}_timeout"),
+ dao.getSettingFlow("config_${packageName}_float_timeout"),
+ dao.getSettingFlow("config_${packageName}_remove_notif")
+ ) { f, s, t, ft, rn ->
IslandConfig(
f?.toBoolean(),
s?.toBoolean(),
- t?.toIntOrNull()
+ t?.toIntOrNull(),
+ ft?.toIntOrNull(),
+ rn?.toBoolean()
)
}
}
@@ -172,10 +208,14 @@ class AppPreferences(context: Context) {
val fKey = "config_${packageName}_float"
val sKey = "config_${packageName}_shade"
val tKey = "config_${packageName}_timeout"
+ val ftKey = "config_${packageName}_float_timeout"
+ val rnKey = "config_${packageName}_remove_notif"
if (config.isFloat != null) save(fKey, config.isFloat.toString()) else remove(fKey)
if (config.isShowShade != null) save(sKey, config.isShowShade.toString()) else remove(sKey)
if (config.timeout != null) save(tKey, config.timeout.toString()) else remove(tKey)
+ if (config.floatTimeout != null) save(ftKey, config.floatTimeout.toString()) else remove(ftKey)
+ if (config.removeOriginalNotification != null) save(rnKey, config.removeOriginalNotification.toString()) else remove(rnKey)
}
// --- NAVIGATION ---
@@ -193,8 +233,8 @@ class AppPreferences(context: Context) {
dao.getSettingFlow(SettingsKeys.NAV_LEFT),
dao.getSettingFlow(SettingsKeys.NAV_RIGHT)
) { l, r ->
- val left = try { NavContent.valueOf(l ?: NavContent.DISTANCE_ETA.name) } catch (e: Exception) { NavContent.DISTANCE_ETA }
- val right = try { NavContent.valueOf(r ?: NavContent.INSTRUCTION.name) } catch (e: Exception) { NavContent.INSTRUCTION }
+ val left = try { NavContent.valueOf(l ?: NavContent.DISTANCE_ETA.name) } catch (_: Exception) { NavContent.DISTANCE_ETA }
+ val right = try { NavContent.valueOf(r ?: NavContent.INSTRUCTION.name) } catch (_: Exception) { NavContent.INSTRUCTION }
left to right
}
@@ -208,8 +248,8 @@ class AppPreferences(context: Context) {
dao.getSettingFlow("config_${packageName}_nav_left"),
dao.getSettingFlow("config_${packageName}_nav_right")
) { l, r ->
- val left = l?.let { try { NavContent.valueOf(it) } catch(e: Exception){null} }
- val right = r?.let { try { NavContent.valueOf(it) } catch(e: Exception){null} }
+ val left = l?.let { try { NavContent.valueOf(it) } catch(_: Exception){null} }
+ val right = r?.let { try { NavContent.valueOf(it) } catch(_: Exception){null} }
left to right
}
}
@@ -220,8 +260,8 @@ class AppPreferences(context: Context) {
dao.getSettingFlow("config_${packageName}_nav_right"),
globalNavLayoutFlow
) { appL, appR, global ->
- val left = appL?.let { try { NavContent.valueOf(it) } catch(e: Exception){null} } ?: global.first
- val right = appR?.let { try { NavContent.valueOf(it) } catch(e: Exception){null} } ?: global.second
+ val left = appL?.let { try { NavContent.valueOf(it) } catch(_: Exception){null} } ?: global.first
+ val right = appR?.let { try { NavContent.valueOf(it) } catch(_: Exception){null} } ?: global.second
left to right
}
}
@@ -239,19 +279,11 @@ class AppPreferences(context: Context) {
private val WIDGET_IDS_DB_KEY = "saved_widget_ids_list"
- /**
- * Provides a Flow of all currently saved Widget IDs.
- */
val savedWidgetIdsFlow: Flow> = dao.getSettingFlow(WIDGET_IDS_DB_KEY).map { str ->
str?.split(",")?.mapNotNull { it.toIntOrNull() } ?: emptyList()
}
- /**
- * Gets configuration specific to a single Widget ID.
- * Returns the clean WidgetConfig class.
- */
fun getWidgetConfigFlow(id: Int): Flow {
- // [FIX] 'combine' only supports up to 5 args nicely. For 6+, it returns an Array.
return combine(
dao.getSettingFlow("widget_${id}_shown"),
dao.getSettingFlow("widget_${id}_timeout"),
@@ -260,7 +292,6 @@ class AppPreferences(context: Context) {
dao.getSettingFlow("widget_${id}_auto_update"),
dao.getSettingFlow("widget_${id}_update_interval")
) { args: Array ->
- // Manually unpack the array
val shown = args[0]
val timeout = args[1]
val sizeStr = args[2]
@@ -268,12 +299,12 @@ class AppPreferences(context: Context) {
val autoStr = args[4]
val intervalStr = args[5]
- val sizeEnum = try { WidgetSize.valueOf(sizeStr ?: WidgetSize.MEDIUM.name) } catch (e: Exception) { WidgetSize.MEDIUM }
- val modeEnum = try { WidgetRenderMode.valueOf(modeStr ?: WidgetRenderMode.INTERACTIVE.name) } catch (e: Exception) { WidgetRenderMode.INTERACTIVE }
+ val sizeEnum = try { WidgetSize.valueOf(sizeStr ?: WidgetSize.MEDIUM.name) } catch (_: Exception) { WidgetSize.MEDIUM }
+ val modeEnum = try { WidgetRenderMode.valueOf(modeStr ?: WidgetRenderMode.INTERACTIVE.name) } catch (_: Exception) { WidgetRenderMode.INTERACTIVE }
WidgetConfig(
isShowShade = shown.toBoolean(true),
- timeout = timeout.toInt(5),
+ timeout = timeout.toInt(10),
size = sizeEnum,
renderMode = modeEnum,
autoUpdate = autoStr.toBoolean(false),
@@ -282,9 +313,6 @@ class AppPreferences(context: Context) {
}
}
- /**
- * Saves configuration for a specific widget ID.
- */
suspend fun saveWidgetConfig(
id: Int,
config: WidgetConfig
@@ -328,4 +356,106 @@ class AppPreferences(context: Context) {
val newSet = if (isFavorite) currentSet + packageName else currentSet - packageName
save("favorite_widget_apps", newSet.serialize())
}
+
+ // ========================================================================
+ // Global Notification Types
+ // ========================================================================
+
+ val GLOBAL_NOTIFICATION_TYPES_KEY = "global_notification_types"
+
+ val globalNotificationTypesFlow: Flow> = dao.getSettingFlow(GLOBAL_NOTIFICATION_TYPES_KEY).map { str ->
+ str?.deserializeSet() ?: NotificationType.entries.map { it.name }.toSet()
+ }
+
+ suspend fun updateGlobalNotificationType(type: NotificationType, isEnabled: Boolean) {
+ val currentStr = dao.getSetting(GLOBAL_NOTIFICATION_TYPES_KEY)
+ val currentSet = currentStr?.deserializeSet() ?: NotificationType.entries.map { it.name }.toSet()
+ val newSet = if (isEnabled) currentSet + type.name else currentSet - type.name
+ save(GLOBAL_NOTIFICATION_TYPES_KEY, newSet.serialize())
+ }
+
+ // --- APP-SPECIFIC NOTIFICATION TYPES ---
+
+ fun getAppConfigFlow(packageName: String): Flow?> {
+ val legacyKey = "config_$packageName"
+ return dao.getSettingFlow(legacyKey).map { str ->
+ str?.deserializeSet()
+ }
+ }
+
+ suspend fun updateAppConfig(packageName: String, type: NotificationType, isEnabled: Boolean) {
+ val key = "config_$packageName"
+ val currentStr = dao.getSetting(key)
+ val currentSet = currentStr?.deserializeSet() ?: NotificationType.entries.map { it.name }.toSet()
+ val newSet = if (isEnabled) currentSet + type.name else currentSet - type.name
+ save(key, newSet.serialize())
+ }
+
+ // ========================================================================
+ // THEME ENGINE CONFIGURATION
+ // ========================================================================
+
+ private val USE_NATIVE_ENGINE = "use_native_live_updates"
+ private val IS_SHIZUKU_WORKAROUND_ENABLED = "is_shizuku_workaround_enabled"
+
+ val useNativeLiveUpdates: Flow = dao.getSettingFlow(USE_NATIVE_ENGINE)
+ .map { it?.toBoolean() ?: false }
+
+ val isShizukuWorkaroundEnabled: Flow = dao.getSettingFlow(IS_SHIZUKU_WORKAROUND_ENABLED)
+ .map { it?.toBoolean() ?: false }
+
+ suspend fun setUseNativeLiveUpdates(value: Boolean) {
+ save(USE_NATIVE_ENGINE, value.toString())
+ }
+
+ suspend fun setShizukuWorkaroundEnabled(value: Boolean) {
+ save(IS_SHIZUKU_WORKAROUND_ENABLED, value.toString())
+ }
+
+ // ========================================================================
+ // DND / GAME MODE CONFIGURATION
+ // ========================================================================
+
+ val isDndModeEnabledFlow: Flow = dao.getSettingFlow("dnd_mode_enabled").map { it.toBoolean(false) }
+ suspend fun setDndModeEnabled(isEnabled: Boolean) = save("dnd_mode_enabled", isEnabled.toString())
+
+ val autoDetectDndFlow: Flow = dao.getSettingFlow("auto_detect_dnd").map { it.toBoolean(true) }
+ suspend fun setAutoDetectDnd(autoDetect: Boolean) = save("auto_detect_dnd", autoDetect.toString())
+
+ // --- APP-SPECIFIC ENGINE OVERRIDES ---
+
+ fun getAppEnginePreferenceFlow(packageName: String): Flow {
+ val key = "config_${packageName}_use_native"
+ return dao.getSettingFlow(key).map { it?.toBooleanStrictOrNull() }
+ }
+
+ suspend fun updateAppEnginePreference(packageName: String, useNative: Boolean?) {
+ val key = "config_${packageName}_use_native"
+ if (useNative != null) {
+ save(key, useNative.toString())
+ } else {
+ remove(key)
+ }
+ }
+
+ // ========================================================================
+ // PERMANENT ISLAND CONFIGURATION
+ // ========================================================================
+
+ private val SHOW_PERMANENT_ISLAND = "show_permanent_island"
+ private val PERMANENT_ISLAND_WIDTH = "permanent_island_width"
+
+ val isPermanentIslandEnabledFlow: Flow = dao.getSettingFlow(SHOW_PERMANENT_ISLAND)
+ .map { it?.toBoolean() ?: false }
+
+ val permanentIslandWidthFlow: Flow = dao.getSettingFlow(PERMANENT_ISLAND_WIDTH)
+ .map { it?.toIntOrNull() ?: 0 }
+
+ suspend fun setPermanentIslandEnabled(value: Boolean) {
+ save(SHOW_PERMANENT_ISLAND, value.toString())
+ }
+
+ suspend fun setPermanentIslandWidth(value: Int) {
+ save(PERMANENT_ISLAND_WIDTH, value.toString())
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/data/ConfigResolver.kt b/app/src/main/java/com/d4viddf/hyperbridge/data/ConfigResolver.kt
new file mode 100644
index 0000000..afce313
--- /dev/null
+++ b/app/src/main/java/com/d4viddf/hyperbridge/data/ConfigResolver.kt
@@ -0,0 +1,68 @@
+package com.d4viddf.hyperbridge.data
+
+import android.content.Context
+import com.d4viddf.hyperbridge.data.theme.ThemeRepository
+import com.d4viddf.hyperbridge.models.NavContent
+import com.d4viddf.hyperbridge.models.theme.HyperTheme
+import com.d4viddf.hyperbridge.models.theme.NavigationModule
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * A fast, in-memory resolver for Translators to look up effective
+ * configurations without blocking the main thread.
+ */
+class ConfigResolver(context: Context) {
+
+ private val preferences = AppPreferences(context)
+ private val themeRepo = ThemeRepository(context)
+
+ // Keep a hot flow of the active theme in memory
+ private val activeTheme: StateFlow = themeRepo.activeTheme
+
+ /**
+ * Data class holding the exact UI parameters a Translator needs to render the Island
+ */
+ data class ResolvedNavConfig(
+ val useNativeEngine: Boolean,
+ val leftContent: NavContent,
+ val rightContent: NavContent,
+ val themeVisuals: NavigationModule?
+ )
+
+ /**
+ * Call this from your NavTranslator when a notification arrives.
+ * It instantly calculates the result based on in-memory state.
+ */
+ suspend fun resolveNavConfig(packageName: String): ResolvedNavConfig {
+ val theme = activeTheme.value
+ val themeAppOverride = theme?.apps?.get(packageName)
+
+ // 1. Resolve Engine
+ val effectiveEngine = themeAppOverride?.useNativeLiveUpdates
+ ?: preferences.useNativeLiveUpdates.stateIn(CoroutineScope(Dispatchers.IO), SharingStarted.Eagerly, true).value
+
+ // 2. Resolve Visuals
+ val effectiveVisuals = themeAppOverride?.navigation
+
+ // 3. Resolve Content (A bit tricky as it requires reading DB, but we do it fast)
+ // Note: For absolute zero-latency, you might want to cache globalNavLayoutFlow in memory too!
+ var leftContent = NavContent.DISTANCE_ETA
+ var rightContent = NavContent.INSTRUCTION
+
+ preferences.getEffectiveNavLayout(packageName).collect { (l, r) ->
+ leftContent = l
+ rightContent = r
+ }
+
+ return ResolvedNavConfig(
+ useNativeEngine = effectiveEngine,
+ leftContent = leftContent,
+ rightContent = rightContent,
+ themeVisuals = effectiveVisuals
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/data/db/AppSettings.kt b/app/src/main/java/com/d4viddf/hyperbridge/data/db/AppSettings.kt
index 1c43a10..0247162 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/data/db/AppSettings.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/data/db/AppSettings.kt
@@ -24,6 +24,8 @@ object SettingsKeys {
const val GLOBAL_FLOAT = "global_float"
const val GLOBAL_SHADE = "global_shade"
const val GLOBAL_TIMEOUT = "global_timeout"
+ const val GLOBAL_FLOAT_TIMEOUT = "global_float_timeout"
+ const val GLOBAL_REMOVE_NOTIF = "global_remove_original_notif"
const val GLOBAL_BLOCKED_TERMS = "global_blocked_terms"
// Nav
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/data/model/BackupModels.kt b/app/src/main/java/com/d4viddf/hyperbridge/data/model/BackupModels.kt
index 622d642..e9fe3c2 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/data/model/BackupModels.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/data/model/BackupModels.kt
@@ -1,15 +1,18 @@
package com.d4viddf.hyperbridge.data.model
import com.google.gson.annotations.SerializedName
+import kotlinx.serialization.Serializable
/**
* Root Container
*/
+@Serializable
data class HyperBridgeBackup(
@SerializedName("metadata") val metadata: BackupMetadata,
@SerializedName("settings") val settings: List
)
+@Serializable
data class BackupMetadata(
@SerializedName("package_name") val packageName: String,
@SerializedName("version_code") val versionCode: Int,
@@ -18,7 +21,8 @@ data class BackupMetadata(
@SerializedName("device_model") val deviceModel: String
)
+@Serializable
data class AppSettingBackup(
@SerializedName("key") val key: String,
@SerializedName("value") val value: String
-)
\ No newline at end of file
+)
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/data/theme/RulesEngine.kt b/app/src/main/java/com/d4viddf/hyperbridge/data/theme/RulesEngine.kt
index 318c9b7..391e133 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/data/theme/RulesEngine.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/data/theme/RulesEngine.kt
@@ -70,7 +70,7 @@ class RulesEngine {
// 4. External State (Plugins) - Placeholder for now
if (!cond.externalStateKey.isNullOrEmpty()) {
- // In future: Check against a StateManager.currentState map
+ // In the future: Check against a StateManager.currentState map
// For now, if a rule requires external state, we fail it safely
return false
}
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/data/theme/ThemeRepository.kt b/app/src/main/java/com/d4viddf/hyperbridge/data/theme/ThemeRepository.kt
index e8f52e5..c951ba8 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/data/theme/ThemeRepository.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/data/theme/ThemeRepository.kt
@@ -23,7 +23,7 @@ import java.util.zip.ZipInputStream
class ThemeRepository(private val context: Context) {
- private val TAG = "HyperBridgeTheme"
+ private val tag = "HyperBridgeTheme"
private val json = Json { ignoreUnknownKeys = true; prettyPrint = true }
private val _activeTheme = MutableStateFlow(null)
@@ -46,13 +46,13 @@ class ThemeRepository(private val context: Context) {
val content = themeFile.readText()
val theme = json.decodeFromString(content)
_activeTheme.value = theme
- Log.i(TAG, "Theme Activated: ${theme.meta.name}")
+ Log.i(tag, "Theme Activated: ${theme.meta.name}")
} else {
- Log.w(TAG, "Theme file not found: $themeId")
+ Log.w(tag, "Theme file not found: $themeId")
_activeTheme.value = null
}
} catch (e: Exception) {
- Log.e(TAG, "Failed to load theme", e)
+ Log.e(tag, "Failed to load theme", e)
_activeTheme.value = null
}
}
@@ -108,13 +108,13 @@ class ThemeRepository(private val context: Context) {
val content = configFile.readText()
val theme = try {
json.decodeFromString(content)
- } catch (e: Exception) {
+ } catch (_: Exception) {
throw IllegalArgumentException("Invalid JSON structure")
}
// 3. INSTALLATION
// We use the ID defined in JSON, or generate one if missing (shouldn't happen with strict spec)
- val finalId = if (theme.id.isNotEmpty()) theme.id else tempId
+ val finalId = theme.id.ifEmpty { tempId }
val targetDir = File(themesDir, finalId)
// If updating, clear old version
@@ -129,11 +129,11 @@ class ThemeRepository(private val context: Context) {
tempDir.deleteRecursively()
}
- Log.i(TAG, "Theme Installed Successfully: $finalId")
+ Log.i(tag, "Theme Installed Successfully: $finalId")
return@withContext finalId
} catch (e: Exception) {
- Log.e(TAG, "Installation failed", e)
+ Log.e(tag, "Installation failed", e)
tempDir.deleteRecursively() // Cleanup
throw e
}
@@ -195,7 +195,7 @@ class ThemeRepository(private val context: Context) {
ResourceType.PRESET_DRAWABLE -> null
}
} catch (e: Exception) {
- Log.w(TAG, "Error loading bitmap resource: ${resource.value}", e)
+ Log.w(tag, "Error loading bitmap resource: ${resource.value}", e)
null
}
}
@@ -219,8 +219,8 @@ class ThemeRepository(private val context: Context) {
val theme = json.decodeFromString(content)
list.add(theme)
}
- } catch (e: Exception) {
- Log.e(TAG, "Corrupt theme found in: ${themeFolder.name}")
+ } catch (_: Exception) {
+ Log.e(tag, "Corrupt theme found in: ${themeFolder.name}")
}
}
}
@@ -285,7 +285,7 @@ class ThemeRepository(private val context: Context) {
}
return@withContext zipFile
} catch (e: Exception) {
- Log.e(TAG, "Export failed", e)
+ Log.e(tag, "Export failed", e)
return@withContext null
}
}
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/data/widget/WidgetManager.kt b/app/src/main/java/com/d4viddf/hyperbridge/data/widget/WidgetManager.kt
index a372279..5160144 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/data/widget/WidgetManager.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/data/widget/WidgetManager.kt
@@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
+import androidx.core.graphics.createBitmap
@SuppressLint("StaticFieldLeak")
object WidgetManager {
@@ -28,13 +29,24 @@ object WidgetManager {
private val _widgetUpdates = MutableSharedFlow(replay = 0, extraBufferCapacity = 10)
val widgetUpdates: SharedFlow = _widgetUpdates.asSharedFlow()
+ private var isListening = false
+
fun init(ctx: Context) {
- if (context != null) return
- context = ctx.applicationContext
- appWidgetManager = AppWidgetManager.getInstance(context)
+ if (context == null) {
+ context = ctx.applicationContext
+ appWidgetManager = AppWidgetManager.getInstance(context)
+ appWidgetHost = HyperAppWidgetHost(context!!, HOST_ID)
+ }
- appWidgetHost = HyperAppWidgetHost(context!!, HOST_ID)
- appWidgetHost?.startListening()
+ if (!isListening) {
+ try {
+ appWidgetHost?.startListening()
+ isListening = true
+ } catch (e: IllegalStateException) {
+ // Thrown if the device is not unlocked yet
+ e.printStackTrace()
+ }
+ }
}
// --- ID MANAGEMENT ---
@@ -98,7 +110,7 @@ object WidgetManager {
hostView.layout(0, 0, hostView.measuredWidth, hostView.measuredHeight)
try {
- val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
+ val bitmap = createBitmap(width, height)
val canvas = Canvas(bitmap)
hostView.draw(canvas)
return bitmap
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/integration/shizuku/PrivilegedServiceImpl.kt b/app/src/main/java/com/d4viddf/hyperbridge/integration/shizuku/PrivilegedServiceImpl.kt
new file mode 100644
index 0000000..0987cd6
--- /dev/null
+++ b/app/src/main/java/com/d4viddf/hyperbridge/integration/shizuku/PrivilegedServiceImpl.kt
@@ -0,0 +1,178 @@
+package com.d4viddf.hyperbridge.integration.shizuku
+
+import androidx.annotation.Keep
+import com.d4viddf.hyperbridge.IPrivilegedService
+import com.d4viddf.hyperbridge.IPrivilegedLogCallback
+import android.util.Log
+import android.os.IBinder
+import android.os.IInterface
+import java.lang.reflect.InvocationTargetException
+import android.os.Handler
+import android.os.HandlerThread
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicReference
+
+@Keep
+class PrivilegedServiceImpl : IPrivilegedService.Stub() {
+
+ companion object {
+ private const val TAG = "PrivilegedServiceImpl"
+ private const val OP_TIMEOUT_MS = 3000L
+ private val workerThread: HandlerThread by lazy {
+ HandlerThread("PrivilegedServiceWorker").apply { start() }
+ }
+ private val workerHandler: Handler by lazy { Handler(workerThread.looper) }
+
+ init {
+ try {
+ Log.d(TAG, "⚡ PrivilegedServiceImpl class loaded")
+ } catch (ignored: Exception) {}
+ }
+ }
+
+ init {
+ try {
+ logD("⚡ PrivilegedServiceImpl instance created")
+ } catch (ignored: Exception) {}
+ }
+
+ @Volatile private var logCallback: IPrivilegedLogCallback? = null
+
+ override fun setLogCallback(callback: IPrivilegedLogCallback?) {
+ logCallback = callback
+ logD("Log callback set: ${callback != null}")
+ }
+
+ override fun setPackageNetworkingEnabled(uid: Int, enabled: Boolean): Boolean {
+ logD("🚀 ENTRY: setPackageNetworkingEnabled(uid=$uid, enabled=$enabled)")
+
+ try {
+ val resultRef = AtomicReference?>(null)
+ val latch = CountDownLatch(1)
+
+ workerHandler.post {
+ val result = runCatching {
+ logD("Step 1: Getting ConnectivityManager...")
+ val realCm = getConnectivityManagerInstance()
+ logD("Step 2: Got ConnectivityManager: ${realCm.javaClass.name}")
+
+ // Chain IDs: 9 = FILTER_CHAIN_NAME_STANDBY_ALLOWLIST or similar on some ROMs
+ // On some vendors, 2 or 1 might be used. 9 is most common for firewall.
+ val chain = 9
+
+ logD("Step 3: Calling setFirewallChainEnabled($chain, true)...")
+ // Pass Boolean instead of Integer to match expected 'boolean' type
+ callMethodResilient(realCm, "setFirewallChainEnabled", chain, true)
+ logD("Step 4: setFirewallChainEnabled succeeded")
+
+ val rule = if (enabled) 0 else 2 // 0 = ALLOW, 2 = DENY
+ logD("Step 5: Calling setUidFirewallRule($chain, $uid, $rule)...")
+ callMethodResilient(realCm, "setUidFirewallRule", chain, uid, rule)
+
+ logD("✅ SUCCESS: Firewall rules updated for $uid")
+ true
+ }
+
+ resultRef.set(result)
+ latch.countDown()
+ }
+
+ val completed = latch.await(OP_TIMEOUT_MS, TimeUnit.MILLISECONDS)
+ if (!completed) {
+ logE("❌ FAILURE in setPackageNetworkingEnabled: timeout after ${OP_TIMEOUT_MS}ms")
+ return false
+ }
+
+ val result = resultRef.get() ?: return false
+
+ if (result.isFailure) {
+ val e = result.exceptionOrNull()
+ logE("❌ FAILURE in setPackageNetworkingEnabled: ${e?.javaClass?.name}: ${e?.message}")
+ e?.cause?.let { cause ->
+ logE("❌ Root cause: ${cause.javaClass.name}: ${cause.message}")
+ }
+ e?.printStackTrace()
+ return false
+ }
+
+ return result.getOrDefault(false)
+
+ } catch (e: Throwable) {
+ // ABSOLUTE guard against any crash in the privileged process
+ logE("🔥 CRITICAL ERROR in PrivilegedServiceImpl: ${e.message}")
+ return false
+ } finally {
+ logD("🏁 EXIT: setPackageNetworkingEnabled")
+ }
+ }
+
+ private fun getConnectivityManagerInstance(): Any {
+ val smClass = Class.forName("android.os.ServiceManager")
+ val getService = smClass.getMethod("getService", String::class.java)
+ val binder = getService.invoke(null, "connectivity") as? IBinder
+ ?: throw RuntimeException("connectivity service not found")
+
+ val stubClass = Class.forName("android.net.IConnectivityManager\$Stub")
+ val asInterface = stubClass.getMethod("asInterface", IBinder::class.java)
+ return asInterface.invoke(null, binder) ?: throw RuntimeException("asInterface returned null")
+ }
+
+ /**
+ * More resilient method call that searches for the best matching method
+ */
+ private fun callMethodResilient(obj: Any, methodName: String, vararg args: Any) {
+ val clazz = obj.javaClass
+ val methods = clazz.methods
+
+ // Find a method that matches by name and parameter count
+ val targetMethod = methods.find { it.name == methodName && it.parameterCount == args.size }
+ ?: throw NoSuchMethodException("Could not find method $methodName with ${args.size} params on ${clazz.name}")
+
+ targetMethod.isAccessible = true
+
+ // Ensure arguments match primitive types if needed
+ val finalArgs = Array(args.size) { i ->
+ val paramType = targetMethod.parameterTypes[i]
+ val arg = args[i]
+
+ when {
+ paramType == Int::class.javaPrimitiveType && arg is Int -> arg
+ paramType == Boolean::class.javaPrimitiveType && arg is Boolean -> arg
+ // Force conversion if there's a mismatch (common in reflection)
+ paramType == Boolean::class.javaPrimitiveType && arg is Number -> arg.toInt() != 0
+ paramType == Int::class.javaPrimitiveType && arg is Boolean -> if (arg) 1 else 0
+ else -> arg
+ }
+ }
+
+ try {
+ targetMethod.invoke(obj, *finalArgs)
+ } catch (e: InvocationTargetException) {
+ val cause = e.targetException ?: e.cause
+ if (cause != null) {
+ logW("InvocationTargetException cause: ${cause.javaClass.name}: ${cause.message}")
+ } else {
+ logW("InvocationTargetException with null cause")
+ }
+ throw e
+ }
+ }
+
+ private fun logD(message: String) {
+ Log.d(TAG, message)
+ logCallback?.let { runCatching { it.log(0, TAG, message) } }
+ }
+
+ private fun logW(message: String) {
+ Log.w(TAG, message)
+ logCallback?.let { runCatching { it.log(2, TAG, message) } }
+ }
+
+ private fun logE(message: String) {
+ Log.e(TAG, message)
+ logCallback?.let { runCatching { it.log(3, TAG, message) } }
+ }
+}
+
+
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/integration/shizuku/ShizukuHook.kt b/app/src/main/java/com/d4viddf/hyperbridge/integration/shizuku/ShizukuHook.kt
new file mode 100644
index 0000000..3dcb729
--- /dev/null
+++ b/app/src/main/java/com/d4viddf/hyperbridge/integration/shizuku/ShizukuHook.kt
@@ -0,0 +1,241 @@
+package com.d4viddf.hyperbridge.integration.shizuku
+
+import android.content.Context
+import android.os.IBinder
+import android.util.Log
+import java.lang.reflect.InvocationTargetException
+import java.util.concurrent.ConcurrentHashMap
+import rikka.shizuku.ShizukuBinderWrapper
+import rikka.shizuku.SystemServiceHelper
+
+/**
+ * Minimal "hook mode" helper adapted from InstallerX Revived.
+ *
+ * Instead of hopping through our own Shizuku user service, we wrap the original
+ * system Connectivity binder with [ShizukuBinderWrapper] and invoke the hidden
+ * firewall APIs directly through that hooked binder.
+ */
+object ShizukuHook {
+ private const val TAG = "ShizukuHook"
+ private const val OEM_DENY_CHAIN = 9
+
+ private data class ServiceBackend(
+ val serviceName: String,
+ val stubClassName: String,
+ val label: String
+ )
+
+ private val serviceBackends = listOf(
+ ServiceBackend(
+ serviceName = Context.CONNECTIVITY_SERVICE,
+ stubClassName = "android.net.IConnectivityManager\$Stub",
+ label = "ConnectivityManager"
+ ),
+ ServiceBackend(
+ serviceName = "network_management",
+ stubClassName = "android.os.INetworkManagementService\$Stub",
+ label = "NetworkManagementService"
+ )
+ )
+
+ private val hookedServiceCache = ConcurrentHashMap()
+
+ fun setPackageNetworkingEnabled(uid: Int, enabled: Boolean) {
+ val rule = if (enabled) 0 else 2
+ val failures = mutableListOf()
+
+ try {
+ val cm = getHookedConnectivityManager()
+ if (!enabled) {
+ cm.javaClass.getMethod("setFirewallChainEnabled", Int::class.javaPrimitiveType, Boolean::class.javaPrimitiveType).invoke(cm, OEM_DENY_CHAIN, true)
+ Log.d(TAG, "Enabled firewall chain $OEM_DENY_CHAIN via typed IConnectivityManager before blocking uid=$uid")
+ }
+ cm.javaClass.getMethod("setUidFirewallRule", Int::class.javaPrimitiveType, Int::class.javaPrimitiveType, Int::class.javaPrimitiveType).invoke(cm, OEM_DENY_CHAIN, uid, rule)
+ Log.d(TAG, "Network ${if (enabled) "restored" else "blocked"} for uid=$uid via typed IConnectivityManager")
+ return
+ } catch (t: Throwable) {
+ val detail = "Typed IConnectivityManager failed: ${t.message}"
+ failures += detail
+ Log.w(TAG, detail, t)
+ }
+
+ try {
+ val nm = getHookedNetworkManagementService()
+ if (!enabled) {
+ nm.javaClass.getMethod("setFirewallChainEnabled", Int::class.javaPrimitiveType, Boolean::class.javaPrimitiveType).invoke(nm, OEM_DENY_CHAIN, true)
+ Log.d(TAG, "Enabled firewall chain $OEM_DENY_CHAIN via typed INetworkManagementService before blocking uid=$uid")
+ }
+ try {
+ nm.javaClass.getMethod("setUidFirewallRule", Int::class.javaPrimitiveType, Int::class.javaPrimitiveType, Int::class.javaPrimitiveType).invoke(nm, OEM_DENY_CHAIN, uid, rule)
+ Log.d(TAG, "Network ${if (enabled) "restored" else "blocked"} for uid=$uid via typed INetworkManagementService.setUidFirewallRule")
+ } catch (_: Throwable) {
+ nm.javaClass.getMethod("setFirewallUidRule", Int::class.javaPrimitiveType, Int::class.javaPrimitiveType, Int::class.javaPrimitiveType).invoke(nm, OEM_DENY_CHAIN, uid, rule)
+ Log.d(TAG, "Network ${if (enabled) "restored" else "blocked"} for uid=$uid via typed INetworkManagementService.setFirewallUidRule")
+ }
+ return
+ } catch (t: Throwable) {
+ val detail = "Typed INetworkManagementService failed: ${t.message}"
+ failures += detail
+ Log.w(TAG, detail, t)
+ }
+
+ for (backend in serviceBackends) {
+ try {
+ val service = getHookedService(backend)
+ Log.d(TAG, "Trying ${backend.label} backend for uid=$uid, enabled=$enabled")
+
+ if (!enabled) {
+ callMethodResilient(
+ service,
+ listOf("setFirewallChainEnabled"),
+ OEM_DENY_CHAIN,
+ true
+ )
+ Log.d(TAG, "Enabled firewall chain $OEM_DENY_CHAIN via ${backend.label} before blocking uid=$uid")
+ }
+
+ val methodUsed = callMethodResilient(
+ service,
+ listOf("setUidFirewallRule", "setFirewallUidRule"),
+ OEM_DENY_CHAIN,
+ uid,
+ rule
+ )
+ Log.d(
+ TAG,
+ "Network ${if (enabled) "restored" else "blocked"} for uid=$uid via ${backend.label}.$methodUsed"
+ )
+ return
+ } catch (t: Throwable) {
+ val available = summarizeFirewallMethods(backend)
+ val detail = buildString {
+ append("${backend.label} failed: ${t.message}")
+ if (available.isNotBlank()) {
+ append(" | available=$available")
+ }
+ }
+ failures += detail
+ Log.w(TAG, detail, t)
+ }
+ }
+
+ throw IllegalStateException(
+ "No compatible firewall backend found for uid=$uid. ${failures.joinToString(" || ")}"
+ )
+ }
+
+ private fun getHookedConnectivityManager(): Any {
+ return getHookedService(
+ ServiceBackend(
+ serviceName = Context.CONNECTIVITY_SERVICE,
+ stubClassName = "android.net.IConnectivityManager\$Stub",
+ label = "ConnectivityManager"
+ )
+ )
+ }
+
+ private fun getHookedNetworkManagementService(): Any {
+ return getHookedService(
+ ServiceBackend(
+ serviceName = "network_management",
+ stubClassName = "android.os.INetworkManagementService\$Stub",
+ label = "NetworkManagementService"
+ )
+ )
+ }
+
+ private fun getHookedService(backend: ServiceBackend): Any {
+ hookedServiceCache[backend.stubClassName]?.let { return it }
+
+ return synchronized(this) {
+ hookedServiceCache[backend.stubClassName]?.let { return@synchronized it }
+
+ val originalBinder = SystemServiceHelper.getSystemService(backend.serviceName)
+ ?: throw IllegalStateException("${backend.label} binder is null")
+
+ val wrapper: IBinder = ShizukuBinderWrapper(originalBinder)
+ val stubClass = Class.forName(backend.stubClassName)
+ val asInterface = stubClass.getMethod("asInterface", IBinder::class.java)
+ val hooked = asInterface.invoke(null, wrapper) ?: throw IllegalStateException(
+ "${backend.label}.Stub.asInterface returned null"
+ )
+
+ hookedServiceCache[backend.stubClassName] = hooked
+ Log.i(TAG, "Created hooked ${backend.label} binder wrapper")
+ hooked
+ }
+ }
+
+ private fun callMethodResilient(target: Any, methodNames: List, vararg args: Any): String {
+ val methods = target.javaClass.methods.filter {
+ it.name in methodNames && it.parameterCount == args.size
+ }
+ if (methods.isEmpty()) {
+ throw NoSuchMethodException(
+ "Could not find any of ${methodNames.joinToString()} with ${args.size} args on ${target.javaClass.name}"
+ )
+ }
+
+ var lastError: Throwable? = null
+ for (method in methods) {
+ try {
+ invokeMethod(target, method, args)
+ return method.name
+ } catch (t: Throwable) {
+ lastError = t
+ }
+ }
+
+ throw lastError ?: NoSuchMethodException(
+ "Methods ${methodNames.joinToString()} exist on ${target.javaClass.name} but none accepted the arguments"
+ )
+ }
+
+ private fun invokeMethod(target: Any, method: java.lang.reflect.Method, args: Array) {
+ method.isAccessible = true
+ val adaptedArgs = Array(args.size) { index ->
+ val arg = args[index]
+ when (val expected = method.parameterTypes[index]) {
+ Int::class.javaPrimitiveType -> when (arg) {
+ is Int -> arg
+ is Boolean -> if (arg) 1 else 0
+ is Number -> arg.toInt()
+ else -> throw IllegalArgumentException("Unsupported arg $arg for int parameter")
+ }
+
+ Boolean::class.javaPrimitiveType -> when (arg) {
+ is Boolean -> arg
+ is Number -> arg.toInt() != 0
+ else -> throw IllegalArgumentException("Unsupported arg $arg for boolean parameter")
+ }
+
+ else -> if (expected.isInstance(arg)) arg
+ else throw IllegalArgumentException("Arg $arg does not match ${expected.name}")
+ }
+ }
+
+ try {
+ method.invoke(target, *adaptedArgs)
+ } catch (e: InvocationTargetException) {
+ throw e.targetException ?: e
+ }
+ }
+
+ private fun summarizeFirewallMethods(backend: ServiceBackend): String {
+ return runCatching {
+ val service = getHookedService(backend)
+ service.javaClass.methods
+ .filter { method ->
+ method.name.contains("Firewall", ignoreCase = true) ||
+ method.name.contains("firewall", ignoreCase = true)
+ }
+ .sortedWith(compareBy({ it.name }, { it.parameterCount }))
+ .joinToString("; ") { method ->
+ val params = method.parameterTypes.joinToString(",") { it.simpleName }
+ "${method.name}($params)"
+ }
+ }.getOrElse { "" }
+ }
+}
+
+
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/integration/shizuku/ShizukuUserServiceRecycler.kt b/app/src/main/java/com/d4viddf/hyperbridge/integration/shizuku/ShizukuUserServiceRecycler.kt
new file mode 100644
index 0000000..6ee652b
--- /dev/null
+++ b/app/src/main/java/com/d4viddf/hyperbridge/integration/shizuku/ShizukuUserServiceRecycler.kt
@@ -0,0 +1,181 @@
+package com.d4viddf.hyperbridge.integration.shizuku
+
+import android.content.ComponentName
+import com.d4viddf.hyperbridge.BuildConfig
+import android.content.Context
+import android.content.ServiceConnection
+import android.os.IBinder
+import com.d4viddf.hyperbridge.IPrivilegedService
+import com.d4viddf.hyperbridge.IPrivilegedLogCallback
+import android.util.Log
+import rikka.shizuku.Shizuku
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withTimeoutOrNull
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+
+object ShizukuUserServiceRecycler {
+
+ private val serviceMutex = Mutex()
+ private var cachedService: IPrivilegedService? = null
+ private var serviceConnection: ServiceConnection? = null
+ private var serviceArgs: Shizuku.UserServiceArgs? = null
+ private var lastPingAttempt = 0L
+ private const val TAG = "ShizukuRecycler"
+ private const val PING_INTERVAL_MS = 5000L // Only ping every 5 seconds to avoid overhead
+ private const val BIND_TIMEOUT_MS = 10000L // 10 second timeout for service binding
+
+
+ @Volatile private var logCallbackEnabled = false
+ private val logCallback = object : IPrivilegedLogCallback.Stub() {
+ override fun log(level: Int, tag: String?, message: String?) {
+ val safeTag = tag ?: "PrivilegedService"
+ val safeMsg = message ?: ""
+ when (level) {
+ 0 -> Log.d(safeTag, safeMsg)
+ 1 -> Log.i(safeTag, safeMsg)
+ 2 -> Log.w(safeTag, safeMsg)
+ 3 -> Log.e(safeTag, safeMsg)
+ else -> Log.d(safeTag, safeMsg)
+ }
+ }
+ }
+
+ /**
+ * Gets or creates a persistent connection to the privileged service.
+ * Uses caching to avoid repeated bind/unbind cycles.
+ * Only validates connection periodically to minimize Binder calls and avoid throttling.
+ */
+ suspend fun getPrivilegedService(): IPrivilegedService {
+ serviceMutex.withLock {
+ cachedService?.let { cached ->
+ val now = System.currentTimeMillis()
+ // Only ping periodically, not on every call, to avoid Binder overhead
+ if (now - lastPingAttempt > PING_INTERVAL_MS) {
+ try {
+ if (cached.asBinder().pingBinder()) {
+ lastPingAttempt = now
+ Log.d(TAG, "Service cache still valid (ping successful)")
+ return cached
+ }
+ } catch (e: Exception) {
+ // Binder is dead, will reinitialize below
+ Log.w(TAG, "Service ping failed, rebinding: ${e.message}")
+ }
+ lastPingAttempt = now
+ cachedService = null
+ } else {
+ // Recent ping succeeded or not yet time to check, use cached
+ Log.d(TAG, "Using cached service (${now - lastPingAttempt}ms since last ping)")
+ return cached
+ }
+ }
+
+ // Need to establish new connection
+ Log.d(TAG, "Establishing new service connection...")
+ return establishServiceConnection().also {
+ lastPingAttempt = System.currentTimeMillis()
+ Log.d(TAG, "Service connection established successfully")
+ }
+ }
+ }
+
+ fun setLogCallbackEnabled(enabled: Boolean) {
+ logCallbackEnabled = enabled
+ val service = cachedService ?: return
+ try {
+ if (enabled) {
+ service.setLogCallback(logCallback)
+ Log.d(TAG, "Log callback registered with privileged service (toggle)")
+ } else {
+ service.setLogCallback(null)
+ Log.d(TAG, "Log callback unregistered from privileged service (toggle)")
+ }
+ } catch (e: Exception) {
+ Log.w(TAG, "Failed to toggle log callback: ${e.message}")
+ }
+ }
+
+ private suspend fun establishServiceConnection(): IPrivilegedService {
+ return withTimeoutOrNull(BIND_TIMEOUT_MS) {
+ suspendCancellableCoroutine { continuation ->
+ val connection = object : ServiceConnection {
+ override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
+ Log.d(TAG, "onServiceConnected called with service=$service")
+ if (service != null) {
+ val privileged = IPrivilegedService.Stub.asInterface(service)
+ if (logCallbackEnabled) {
+ try {
+ privileged.setLogCallback(logCallback)
+ Log.d(TAG, "Log callback registered with privileged service")
+ } catch (e: Exception) {
+ Log.w(TAG, "Failed to register log callback: ${e.message}")
+ }
+ }
+ cachedService = privileged
+ serviceConnection = this
+ continuation.resume(privileged)
+ } else {
+ Log.e(TAG, "onServiceConnected but binder is null!")
+ continuation.resumeWithException(Exception("Shizuku UserService bound but returned null binder"))
+ }
+ }
+
+ override fun onServiceDisconnected(name: ComponentName?) {
+ Log.w(TAG, "Service disconnected unexpectedly")
+ cachedService = null
+ }
+ }
+
+ val args = Shizuku.UserServiceArgs(
+ ComponentName(BuildConfig.APPLICATION_ID, PrivilegedServiceImpl::class.java.name)
+ )
+ .daemon(true) // Keep as daemon to avoid repeated reconnections
+ .processNameSuffix("privileged")
+ .debuggable(BuildConfig.DEBUG)
+ .version(2)
+
+ serviceArgs = args
+
+ try {
+ Log.d(TAG, "Calling Shizuku.bindUserService()...")
+ Shizuku.bindUserService(args, connection)
+ } catch (e: Exception) {
+ Log.e(TAG, "bindUserService threw exception: ${e.message}", e)
+ continuation.resumeWithException(e)
+ }
+
+ continuation.invokeOnCancellation {
+ Log.d(TAG, "Service binding cancelled, unbinding...")
+ try {
+ Shizuku.unbindUserService(args, connection, true)
+ } catch (ignored: Exception) {
+ Log.w(TAG, "Error during unbind: ${ignored.message}")
+ }
+ }
+ }
+ } ?: throw Exception("Service binding timed out after ${BIND_TIMEOUT_MS}ms")
+ }
+
+ /**
+ * Executes an action with the privileged service, using cached connection.
+ */
+ suspend fun executeWithService(action: suspend (IPrivilegedService) -> T): T {
+ Log.d(TAG, "executeWithService called")
+ return try {
+ action(getPrivilegedService()).also {
+ Log.d(TAG, "executeWithService completed successfully")
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "executeWithService failed: ${e.message}", e)
+ throw e
+ }
+ }
+}
+
+
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/integration/shizuku/ShizukuUtil.kt b/app/src/main/java/com/d4viddf/hyperbridge/integration/shizuku/ShizukuUtil.kt
new file mode 100644
index 0000000..ea88750
--- /dev/null
+++ b/app/src/main/java/com/d4viddf/hyperbridge/integration/shizuku/ShizukuUtil.kt
@@ -0,0 +1,46 @@
+package com.d4viddf.hyperbridge.integration.shizuku
+
+import android.content.pm.PackageManager
+import com.d4viddf.hyperbridge.BuildConfig
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.first
+import rikka.shizuku.Shizuku
+
+
+class ShizukuNotWorkException(cause: Throwable? = null) : Exception("Shizuku is not working or permission denied.", cause)
+
+suspend fun requireShizukuPermissionGranted(action: suspend () -> T): T {
+ callbackFlow {
+ try {
+ rikka.sui.Sui.init(BuildConfig.APPLICATION_ID)
+ } catch (e: Exception) {}
+
+ if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) {
+ if (Shizuku.pingBinder()) {
+ send(Unit)
+ } else {
+ close(ShizukuNotWorkException())
+ }
+ awaitClose()
+ } else {
+ val requestCode = (Int.MIN_VALUE..Int.MAX_VALUE).random()
+ val listener =
+ Shizuku.OnRequestPermissionResultListener { _requestCode, grantResult ->
+ if (_requestCode != requestCode) return@OnRequestPermissionResultListener
+ if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED)
+ trySend(Unit)
+ else close(Exception("sui/shizuku permission denied"))
+ }
+ Shizuku.addRequestPermissionResultListener(listener)
+ Shizuku.requestPermission(requestCode)
+ awaitClose { Shizuku.removeRequestPermissionResultListener(listener) }
+ }
+ }.catch {
+ throw ShizukuNotWorkException(it)
+ }.first()
+
+ return action()
+}
+
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/integration/shizuku/XmsfNetworkHelper.kt b/app/src/main/java/com/d4viddf/hyperbridge/integration/shizuku/XmsfNetworkHelper.kt
new file mode 100644
index 0000000..dbdb740
--- /dev/null
+++ b/app/src/main/java/com/d4viddf/hyperbridge/integration/shizuku/XmsfNetworkHelper.kt
@@ -0,0 +1,92 @@
+package com.d4viddf.hyperbridge.integration.shizuku
+
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.DeadObjectException
+import android.util.Log
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.delay
+
+object XmsfNetworkHelper {
+
+ private const val TAG = "XmsfNetworkHelper"
+ private const val XMSF_PACKAGE = "com.xiaomi.xmsf"
+ private const val MAX_RETRIES = 2
+ private const val RETRY_DELAY_MS = 500L
+
+ suspend fun setXmsfNetworkingEnabled(context: Context, enabled: Boolean): Boolean {
+
+ try {
+ val pm = context.packageManager
+ val uid = try {
+ pm.getPackageUid(XMSF_PACKAGE, 0)
+ } catch (e: Exception) {
+ Log.w(TAG, "XMSF package not found (UID lookup failed)")
+ return false
+ }
+
+ Log.d(TAG, "🚀 setXmsfNetworkingEnabled called: enabled=$enabled, uid=$uid")
+
+ // Use the utility to ensure permission and then perform the action
+ return try {
+ requireShizukuPermissionGranted {
+ Log.d(TAG, "✓ Shizuku permission granted, entering retry loop")
+ var lastError: Exception? = null
+
+ for (attempt in 0 until MAX_RETRIES) {
+ try {
+ Log.d(TAG, "📡 Attempt ${attempt + 1}/$MAX_RETRIES: Getting privileged service...")
+ val service = ShizukuUserServiceRecycler.getPrivilegedService()
+ Log.d(TAG, "✓ Got privileged service, calling setPackageNetworkingEnabled...")
+
+ val success = service.setPackageNetworkingEnabled(uid, enabled)
+ if (!success) {
+ throw IllegalStateException("Privileged service returned failure for uid=$uid")
+ }
+
+ Log.d(TAG, "✓ Successfully set XMSF networking to $enabled via privileged service")
+ return@requireShizukuPermissionGranted true
+ } catch (e: CancellationException) {
+ Log.w(TAG, "⚠️ Operation cancelled")
+ return@requireShizukuPermissionGranted false
+ } catch (e: DeadObjectException) {
+ lastError = e
+ Log.w(TAG, "⚠️ DeadObjectException on attempt ${attempt + 1}")
+ if (attempt + 1 < MAX_RETRIES) {
+ delay(RETRY_DELAY_MS)
+ }
+ } catch (e: Exception) {
+ lastError = e
+ Log.w(TAG, "⚠️ Privileged service path failed on attempt ${attempt + 1}: ${e.message}")
+ try {
+ Log.d(TAG, "🪝 Attempt ${attempt + 1}/$MAX_RETRIES: Falling back to hooked binder...")
+ ShizukuHook.setPackageNetworkingEnabled(uid, enabled)
+ Log.d(TAG, "✓ Successfully set XMSF networking to $enabled via hooked binder fallback")
+ return@requireShizukuPermissionGranted true
+ } catch (hookError: Exception) {
+ lastError = hookError
+ Log.e(
+ TAG,
+ "❌ Error on attempt ${attempt + 1}: service=${e.message}; hook=${hookError.message}"
+ )
+ }
+ if (attempt + 1 < MAX_RETRIES) {
+ delay(RETRY_DELAY_MS)
+ }
+ }
+ }
+ lastError?.let { Log.e(TAG, "❌ All XMSF networking paths failed after retries: ${it.message}") }
+ false
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "❌ Shizuku permission or logic failed: ${e.message}")
+ false
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "❌ Critical error in setXmsfNetworkingEnabled: ${e.message}")
+ return false
+ }
+ }
+}
+
+
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/models/IslandConfig.kt b/app/src/main/java/com/d4viddf/hyperbridge/models/IslandConfig.kt
index 3841912..7483cd2 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/models/IslandConfig.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/models/IslandConfig.kt
@@ -4,15 +4,18 @@ data class IslandConfig(
val isFloat: Boolean? = null,
val isShowShade: Boolean? = null,
val timeout: Int? = null,
+ val floatTimeout: Int? = null,
+ val removeOriginalNotification: Boolean? = null,
) {
// Merges this config (App) with a default config (Global)
fun mergeWith(global: IslandConfig): IslandConfig {
return IslandConfig(
isFloat = this.isFloat ?: global.isFloat ?: false,
- isShowShade = this.isShowShade ?: global.isShowShade ?: true,
+ isShowShade = this.isShowShade ?: global.isShowShade ?: false,
timeout = this.timeout ?: global.timeout ?: 10,
-
+ floatTimeout = this.floatTimeout ?: global.floatTimeout ?: 5,
+ removeOriginalNotification = this.removeOriginalNotification ?: global.removeOriginalNotification ?: false,
)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/models/NotificationType.kt b/app/src/main/java/com/d4viddf/hyperbridge/models/NotificationType.kt
index 83be563..75ff37c 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/models/NotificationType.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/models/NotificationType.kt
@@ -6,6 +6,7 @@ import com.d4viddf.hyperbridge.R
enum class NotificationType(@StringRes val labelRes: Int) {
STANDARD(R.string.type_standard),
PROGRESS(R.string.type_progress),
+ DOWNLOAD(R.string.type_download),
MEDIA(R.string.type_media),
NAVIGATION(R.string.type_nav),
CALL(R.string.type_call),
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/models/theme/ThemeModels.kt b/app/src/main/java/com/d4viddf/hyperbridge/models/theme/ThemeModels.kt
index 32e5c9f..ad1a626 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/models/theme/ThemeModels.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/models/theme/ThemeModels.kt
@@ -32,6 +32,7 @@ data class ThemeMetadata(
data class GlobalConfig(
@SerialName("highlight_color") val highlightColor: String? = null,
@SerialName("background_color") val backgroundColor: String? = null,
+ @SerialName("use_native_live_updates") val useNativeLiveUpdates: Boolean? = null,
@SerialName("text_color") val textColor: String? = "#FFFFFF",
@Deprecated("Use colorMode instead. Kept for backward compatibility with v0.4.x themes.")
@@ -77,12 +78,17 @@ data class AppThemeOverride(
@SerialName("call_config") val callConfig: CallModule? = null,
val actions: Map? = null,
val progress: ProgressModule? = null,
- val navigation: NavigationModule? = null
+ val navigation: NavigationModule? = null,
+
+ // [NEW] App-specific behavior overrides
+ @SerialName("use_native_live_updates") val useNativeLiveUpdates: Boolean? = null,
+ @SerialName("active_notification_types") val activeNotificationTypes: Set? = null
) {
// Safe getter: reads new enum, falls back to legacy boolean if present, else returns null (uses global)
val activeColorMode: ColorMode?
get() = colorMode ?: useAppColors?.let { if (it) ColorMode.APP_ICON else ColorMode.CUSTOM }
}
+
@Serializable
data class ActionConfig(
val mode: ActionButtonMode = ActionButtonMode.ICON,
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/receiver/BootReceiver.kt b/app/src/main/java/com/d4viddf/hyperbridge/receiver/BootReceiver.kt
index 9963228..86523f9 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/receiver/BootReceiver.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/receiver/BootReceiver.kt
@@ -12,6 +12,8 @@ class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
// Check for both standard boot and quick boot (some ROMs use quick)
if (intent.action == Intent.ACTION_BOOT_COMPLETED ||
+ intent.action == Intent.ACTION_LOCKED_BOOT_COMPLETED ||
+ intent.action == Intent.ACTION_MY_PACKAGE_REPLACED ||
intent.action == "android.intent.action.QUICKBOOT_POWERON") {
Log.d("HyperBridge", "Boot completed detected.")
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/service/NotificationReaderService.kt b/app/src/main/java/com/d4viddf/hyperbridge/service/NotificationReaderService.kt
index 0be670b..226f300 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/service/NotificationReaderService.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/service/NotificationReaderService.kt
@@ -5,8 +5,10 @@ import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
+import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
+import android.content.IntentFilter
import android.os.Bundle
import android.service.notification.NotificationListenerService
import android.service.notification.StatusBarNotification
@@ -22,7 +24,9 @@ import com.d4viddf.hyperbridge.data.theme.ThemeRepository
import com.d4viddf.hyperbridge.data.widget.WidgetManager
import com.d4viddf.hyperbridge.models.ActiveIsland
import com.d4viddf.hyperbridge.models.HyperIslandData
+import com.d4viddf.hyperbridge.models.IslandConfig
import com.d4viddf.hyperbridge.models.IslandLimitMode
+import com.d4viddf.hyperbridge.models.NavContent
import com.d4viddf.hyperbridge.models.NotificationType
import com.d4viddf.hyperbridge.models.WidgetConfig
import com.d4viddf.hyperbridge.models.WidgetRenderMode
@@ -31,6 +35,7 @@ import com.d4viddf.hyperbridge.service.translators.LiveUpdateTranslator
import com.d4viddf.hyperbridge.service.translators.MediaTranslator
import com.d4viddf.hyperbridge.service.translators.NavTranslator
import com.d4viddf.hyperbridge.service.translators.ProgressTranslator
+import com.d4viddf.hyperbridge.service.translators.DownloadTranslator
import com.d4viddf.hyperbridge.service.translators.StandardTranslator
import com.d4viddf.hyperbridge.service.translators.TimerTranslator
import com.d4viddf.hyperbridge.service.translators.WidgetTranslator
@@ -56,7 +61,7 @@ class NotificationReaderService : NotificationListenerService() {
// --- CHANNELS ---
private val NOTIFICATION_CHANNEL_ID = "hyper_bridge_notification_channel"
private val WIDGET_CHANNEL_ID = "hyper_bridge_widget_channel"
- private val LIVE_UPDATE_CHANNEL_ID = "hyper_bridge_live_update_channel" // [NEW]
+ private val LIVE_UPDATE_CHANNEL_ID = "hyper_bridge_live_update_channel"
private val serviceScope = CoroutineScope(Dispatchers.Default + Job())
// --- STATE & CONFIG ---
@@ -64,14 +69,21 @@ class NotificationReaderService : NotificationListenerService() {
private var currentMode = IslandLimitMode.MOST_RECENT
private var appPriorityList = emptyList()
private var globalBlockedTerms: Set = emptySet()
+
+ private var isDndModeEnabled = false
+ private var autoDetectDnd = true
// --- CACHES ---
private val activeIslands = ConcurrentHashMap()
private val activeTranslations = ConcurrentHashMap()
private val reverseTranslations = ConcurrentHashMap()
private val processingJobs = ConcurrentHashMap()
+ private val timeoutJobs = ConcurrentHashMap()
+ private lateinit var permanentIslandManager: PermanentIslandManager
+ private val intentionallyRemovedKeys = ConcurrentHashMap.newKeySet()
private val widgetUpdateDebouncer = ConcurrentHashMap()
private val dismissedWidgetIds = ConcurrentHashMap.newKeySet()
+ private val activeWidgets = ConcurrentHashMap.newKeySet()
private val appLabelCache = ConcurrentHashMap()
private val MAX_ISLANDS = 9
@@ -88,14 +100,27 @@ class NotificationReaderService : NotificationListenerService() {
private lateinit var navTranslator: NavTranslator
private lateinit var timerTranslator: TimerTranslator
private lateinit var progressTranslator: ProgressTranslator
+ private lateinit var downloadTranslator: DownloadTranslator
private lateinit var standardTranslator: StandardTranslator
private lateinit var mediaTranslator: MediaTranslator
private lateinit var widgetTranslator: WidgetTranslator
- private lateinit var liveUpdateTranslator: LiveUpdateTranslator // [NEW]
+ private lateinit var liveUpdateTranslator: LiveUpdateTranslator
+
+ private val userUnlockedReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ if (intent.action == Intent.ACTION_USER_UNLOCKED) {
+ WidgetManager.init(this@NotificationReaderService)
+ }
+ }
+ }
@RequiresPermission(Manifest.permission.POST_NOTIFICATIONS)
override fun onCreate() {
super.onCreate()
+
+ val filter = IntentFilter(Intent.ACTION_USER_UNLOCKED)
+ registerReceiver(userUnlockedReceiver, filter)
+
preferences = AppPreferences(applicationContext)
createChannels()
@@ -108,28 +133,34 @@ class NotificationReaderService : NotificationListenerService() {
navTranslator = NavTranslator(this, themeRepository)
timerTranslator = TimerTranslator(this, themeRepository)
progressTranslator = ProgressTranslator(this, themeRepository)
+ downloadTranslator = DownloadTranslator(this, themeRepository)
standardTranslator = StandardTranslator(this, themeRepository)
- liveUpdateTranslator = LiveUpdateTranslator(this, themeRepository) // [NEW]
+ liveUpdateTranslator = LiveUpdateTranslator(this, themeRepository)
- // Media and Widget translators don't necessarily need the repo yet
mediaTranslator = MediaTranslator(this)
widgetTranslator = WidgetTranslator(this)
- WidgetManager.init(this)
+ val userManager = getSystemService(USER_SERVICE) as android.os.UserManager
+ if (userManager.isUserUnlocked) {
+ WidgetManager.init(this)
+ }
+
+ permanentIslandManager = PermanentIslandManager(this, serviceScope, preferences)
serviceScope.launch { preferences.allowedPackagesFlow.collectLatest { allowedPackageSet = it } }
serviceScope.launch { preferences.limitModeFlow.collectLatest { currentMode = it } }
serviceScope.launch { preferences.appPriorityListFlow.collectLatest { appPriorityList = it } }
serviceScope.launch { preferences.globalBlockedTermsFlow.collectLatest { globalBlockedTerms = it } }
+ serviceScope.launch { preferences.isDndModeEnabledFlow.collectLatest { isDndModeEnabled = it } }
+ serviceScope.launch { preferences.autoDetectDndFlow.collectLatest { autoDetectDnd = it } }
- // Listen for Theme Changes and update the Repository
+ // Listen for Theme Changes
serviceScope.launch {
preferences.activeThemeIdFlow.collectLatest { themeId ->
Log.d(TAG, "Service detected theme change: $themeId")
if (themeId != null) {
themeRepository.activateTheme(themeId)
} else {
- // Reset to defaults if theme is removed/disabled
themeRepository.activateTheme("")
}
}
@@ -164,7 +195,6 @@ class NotificationReaderService : NotificationListenerService() {
}
}
} else if (intent?.action == ACTION_RELOAD_THEME) {
- // Force reload current theme from disk
serviceScope.launch {
val themeId = preferences.activeThemeIdFlow.first()
if (themeId != null) {
@@ -176,6 +206,50 @@ class NotificationReaderService : NotificationListenerService() {
return START_STICKY
}
+ // =========================================================================
+ // EFFECTIVE BEHAVIOR RESOLUTION (Theme > App > Global)
+ // =========================================================================
+
+ private suspend fun getEffectiveTypes(pkg: String): Set {
+ val themeOverride = themeRepository.activeTheme.value?.apps?.get(pkg)
+ val rawTypes = if (themeOverride?.activeNotificationTypes != null) {
+ themeOverride.activeNotificationTypes
+ } else {
+ val localPref = preferences.getAppConfigFlow(pkg).first()
+ localPref ?: preferences.globalNotificationTypesFlow.first()
+ }
+
+ // Fallback: if PROGRESS is enabled but DOWNLOAD is missing, implicitly enable DOWNLOAD
+ return if (rawTypes.contains("PROGRESS") && !rawTypes.contains("DOWNLOAD")) {
+ rawTypes + "DOWNLOAD"
+ } else {
+ rawTypes
+ }
+ }
+
+ private suspend fun getEffectiveEngine(pkg: String): Boolean {
+ val activeTheme = themeRepository.activeTheme.value
+
+ // 1. Theme App Override (Creator explicitly configured this app)
+ val themeAppOverride = activeTheme?.apps?.get(pkg)?.useNativeLiveUpdates
+ if (themeAppOverride != null) return themeAppOverride
+
+ // 2. User App Override (User explicitly configured this app via Home Screen)
+ val userAppOverride = preferences.getAppEnginePreferenceFlow(pkg).first()
+ if (userAppOverride != null) return userAppOverride
+
+ // 3. Theme Global Override (Creator explicitly forced an engine for the whole theme)
+ val themeGlobalOverride = activeTheme?.global?.useNativeLiveUpdates
+ if (themeGlobalOverride != null) return themeGlobalOverride
+
+ // 4. User Global Fallback (The main Engine Setting on the Home Screen!)
+ return preferences.useNativeLiveUpdates.first()
+ }
+
+ private suspend fun getEffectiveNav(pkg: String): Pair {
+ return preferences.getEffectiveNavLayout(pkg).first()
+ }
+
// =========================================================================
// NOTIFICATION REMOVAL LOGIC
// =========================================================================
@@ -186,13 +260,22 @@ class NotificationReaderService : NotificationListenerService() {
val notifId = it.id
val notifKey = it.key
+ if (intentionallyRemovedKeys.remove(notifKey)) {
+ return
+ }
+
processingJobs[notifKey]?.cancel()
processingJobs.remove(notifKey)
+ timeoutJobs[notifKey]?.cancel()
+ timeoutJobs.remove(notifKey)
+
if (isOurApp) {
if (notifId >= WIDGET_ID_BASE) {
val widgetId = notifId - WIDGET_ID_BASE
dismissedWidgetIds.add(widgetId)
+ activeWidgets.remove(widgetId)
+ permanentIslandManager.onActiveNotificationsChanged(activeIslands.size + activeWidgets.size)
return
}
@@ -202,10 +285,8 @@ class NotificationReaderService : NotificationListenerService() {
}
if (originalKey != null) {
- Log.d(TAG, "Dismissing source notification for ID $notifId -> Key: $originalKey")
- serviceScope.launch {
- cancelSourceNotification(originalKey)
- }
+ Log.d(TAG, "Our notification $notifId removed. Cleaning up cache for $originalKey")
+ // [FIX] We no longer kill the source notification when our Island is dismissed or timed out
cleanupCache(originalKey)
}
return
@@ -216,7 +297,7 @@ class NotificationReaderService : NotificationListenerService() {
try {
NotificationManagerCompat.from(this).cancel(hyperId)
- } catch (e: Exception) {}
+ } catch (_: Exception) {}
cleanupCache(notifKey)
}
@@ -263,10 +344,36 @@ class NotificationReaderService : NotificationListenerService() {
val hyperId = activeTranslations[originalKey]
activeIslands.remove(originalKey)
activeTranslations.remove(originalKey)
+ timeoutJobs[originalKey]?.cancel()
+ timeoutJobs.remove(originalKey)
if (hyperId != null) {
reverseTranslations.remove(hyperId)
}
+ permanentIslandManager.onActiveNotificationsChanged(activeIslands.size + activeWidgets.size)
+ }
+
+ private fun handlePostNotificationSideEffects(originalKey: String, bridgeId: Int, config: IslandConfig, type: NotificationType, isLiveUpdate: Boolean) {
+ // 1. Remove original if enabled (EXCEPT for Media)
+ if (config.removeOriginalNotification == true && type != NotificationType.MEDIA) {
+ intentionallyRemovedKeys.add(originalKey)
+ cancelNotification(originalKey)
+ }
+
+ // 2. Schedule timeout ONLY for Live Update notifications
+ if (isLiveUpdate) {
+ val timeoutSeconds = config.timeout ?: 0
+ timeoutJobs[originalKey]?.cancel()
+ if (timeoutSeconds > 0) {
+ timeoutJobs[originalKey] = serviceScope.launch {
+ delay(timeoutSeconds * 1000L)
+ Log.d(TAG, "Timeout reached for $originalKey, removing translated notification $bridgeId")
+ NotificationManagerCompat.from(this@NotificationReaderService).cancel(bridgeId)
+ cleanupCache(originalKey)
+ timeoutJobs.remove(originalKey)
+ }
+ }
+ }
}
// =========================================================================
@@ -293,81 +400,64 @@ class NotificationReaderService : NotificationListenerService() {
@RequiresPermission(Manifest.permission.POST_NOTIFICATIONS)
private suspend fun processStandardNotification(rawSbn: StatusBarNotification) {
+ val manager = getSystemService(NotificationManager::class.java)
+ val isSystemDndActive = manager.currentInterruptionFilter != NotificationManager.INTERRUPTION_FILTER_ALL
+ val dndActive = isDndModeEnabled || (autoDetectDnd && isSystemDndActive)
+
+ if (dndActive) {
+ Log.d(TAG, "DND active. Skipping notification ${rawSbn.packageName}")
+ return
+ }
+
val sbn = ensureValidSbn(rawSbn)
try {
val extras = sbn.notification.extras
- val rawTitle = extras.getCharSequence(Notification.EXTRA_TITLE)?.toString()
- val rawText = extras.getCharSequence(Notification.EXTRA_TEXT)?.toString()
- val rawBigTitle = extras.getCharSequence(Notification.EXTRA_TITLE_BIG)?.toString()
- val rawBigText = extras.getCharSequence(Notification.EXTRA_BIG_TEXT)?.toString()
- val rawProgress = extras.getInt(Notification.EXTRA_PROGRESS, -1)
-
- Log.d(TAG, "--------------------------------------------------")
- Log.d(TAG, "START PROCESSING: ${sbn.packageName}")
- Log.d(TAG, " RAW Title : '$rawTitle'")
- Log.d(TAG, " RAW Text : '$rawText'")
- Log.d(TAG, " RAW BigTitle : '$rawBigTitle'")
- Log.d(TAG, " RAW BigText : '$rawBigText'")
- Log.d(TAG, " RAW Progress : $rawProgress")
-
// [LOGIC] 1. Resolve Info intelligently
var effectiveTitle = resolveTitle(sbn)
val effectiveText = resolveText(sbn.notification.extras)
- Log.d(TAG, " RESOLVED Initial Title: '$effectiveTitle'")
-
// [LOGIC] 2. State Preservation
val key = sbn.key
val previous = activeIslands[key]
if (effectiveTitle.isEmpty()) {
- Log.w(TAG, " Title invalid. Attempting fallback...")
if (previous != null && previous.title.isNotEmpty() && previous.title != sbn.packageName) {
effectiveTitle = previous.title
- Log.d(TAG, " >>> Restored from Cache: '$effectiveTitle'")
} else {
effectiveTitle = getCachedAppLabel(sbn.packageName)
- Log.d(TAG, " >>> Used App Label Fallback: '$effectiveTitle'")
}
}
// [LOGIC] 3. Hard Stop
- val hasProgress = extras.getInt(Notification.EXTRA_PROGRESS_MAX, 0) > 0 ||
- extras.getBoolean(Notification.EXTRA_PROGRESS_INDETERMINATE)
+ val hasProgress = hasProgressNotification(sbn, effectiveTitle, effectiveText)
- if (effectiveTitle.isEmpty() && !hasProgress) {
- Log.w(TAG, " ABORTING: Title still empty and no progress bar.")
- return
- }
+ if (effectiveTitle.isEmpty() && !hasProgress) return
val appBlockedTerms = preferences.getAppBlockedTerms(sbn.packageName).first()
if (appBlockedTerms.isNotEmpty()) {
val content = "$effectiveTitle $effectiveText"
- if (appBlockedTerms.any { term -> content.contains(term, ignoreCase = true) }) {
- Log.d(TAG, " ABORTING: Blocked term found.")
- return
- }
+ if (appBlockedTerms.any { term -> content.contains(term, ignoreCase = true) }) return
}
- // [UPDATED] 4. Theme & Rules Interception
+ // [LOGIC] 4. Theme & Rules Interception
val activeTheme = themeRepository.activeTheme.value
val ruleMatch = rulesEngine.match(sbn, effectiveTitle, effectiveText, activeTheme)
val type = if (ruleMatch?.targetLayout != null) {
- try {
- NotificationType.valueOf(ruleMatch.targetLayout)
- } catch (_: Exception) {
- Log.e(TAG, "Invalid rule layout: ${ruleMatch.targetLayout}")
- detectNotificationType(sbn)
- }
+ try { NotificationType.valueOf(ruleMatch.targetLayout) }
+ catch (_: Exception) { detectNotificationType(sbn) }
} else {
detectNotificationType(sbn)
}
- val config = preferences.getAppConfig(sbn.packageName).first()
- if (!config.contains(type.name)) return
+ // --- LAYERED TRIGGERS LOGIC ---
+ val effectiveTypes = getEffectiveTypes(sbn.packageName)
+ if (!effectiveTypes.contains(type.name)) {
+ Log.d(TAG, " ABORTING: Type $type disabled by user/theme for ${sbn.packageName}")
+ return
+ }
val isUpdate = activeIslands.containsKey(key)
@@ -383,103 +473,158 @@ class NotificationReaderService : NotificationListenerService() {
val bridgeId = sbn.key.hashCode()
val picKey = "pic_${bridgeId}"
- // ===================================================================
- // [NEW LOGIC] NATIVE LIVE UPDATES FALLBACK
- // ===================================================================
- val useLiveUpdates = getSharedPreferences("hyperbridge_settings", Context.MODE_PRIVATE)
- .getBoolean("use_native_live_updates", false)
+ // --- LAYERED ENGINE LOGIC ---
+ val useLiveUpdates = getEffectiveEngine(sbn.packageName)
if (useLiveUpdates) {
Log.i(TAG, " POSTING Native Live Update -> ID: $bridgeId, Type: $type")
- // [FIXED] Pass the dedicated Live Update Channel ID
- val builder = liveUpdateTranslator.translateToLiveUpdate(sbn, finalConfig, LIVE_UPDATE_CHANNEL_ID)
+ // [FIX] Fetch the user's custom layout so the Live Update can use it!
+ val navLayout = if (type == NotificationType.NAVIGATION) getEffectiveNav(sbn.packageName) else null
+
+ // [FIX] Pass the type and the right layout to the translator
+ val builder = liveUpdateTranslator.translateToLiveUpdate(
+ sbn = sbn,
+ channelId = LIVE_UPDATE_CHANNEL_ID,
+ type = type,
+ navRight = navLayout?.second
+ )
- // Track original notification key for dismissal synchronization
builder.extras.putString(EXTRA_ORIGINAL_KEY, sbn.key)
+ val shouldAlertOnce = isUpdate && (type == NotificationType.PROGRESS || type == NotificationType.DOWNLOAD || type == NotificationType.MEDIA)
+ builder.setOnlyAlertOnce(shouldAlertOnce)
+
val notification = builder.build()
- // [FIXED] Dynamic Hash that accounts for Text, Progress, AND Button Actions (Play/Pause)
val actualProgress = extras.getInt(Notification.EXTRA_PROGRESS, 0)
val actualMax = extras.getInt(Notification.EXTRA_PROGRESS_MAX, 0)
val isIndeterminate = extras.getBoolean(Notification.EXTRA_PROGRESS_INDETERMINATE, false)
-
- // Track the button titles so we know when Play becomes Pause
val actionState = sbn.notification.actions?.joinToString { it.title?.toString() ?: "" } ?: ""
val newContentHash = effectiveTitle.hashCode() * 31 +
- effectiveText.hashCode() +
- actualProgress +
- actualMax +
- isIndeterminate.hashCode() +
- actionState.hashCode()
-
- if (isUpdate && previous != null && previous.lastContentHash == newContentHash) {
- Log.d(TAG, " ABORTING: Content hash duplicate (Live Update).")
- return
- }
+ effectiveText.hashCode() + actualProgress + actualMax +
+ isIndeterminate.hashCode() + actionState.hashCode()
+
+ if (isUpdate && previous != null && previous.lastContentHash == newContentHash) return
- NotificationManagerCompat.from(this).notify(bridgeId, notification)
+ com.d4viddf.hyperbridge.util.ShizukuManager.notify(this, bridgeId, notification)
activeTranslations[sbn.key] = bridgeId
reverseTranslations[bridgeId] = sbn.key
-
activeIslands[key] = ActiveIsland(
id = bridgeId, type = type, postTime = System.currentTimeMillis(),
- packageName = sbn.packageName,
- title = effectiveTitle,
- text = effectiveText,
- subText = "LiveUpdate",
- lastContentHash = newContentHash
+ packageName = sbn.packageName, title = effectiveTitle, text = effectiveText,
+ subText = "LiveUpdate", lastContentHash = newContentHash
)
+ permanentIslandManager.onActiveNotificationsChanged(activeIslands.size + activeWidgets.size)
+
+ handlePostNotificationSideEffects(key, bridgeId, finalConfig, type, true)
return
}
- // ===================================================================
- // [ORIGINAL LOGIC] Xiaomi Custom Island Injection
+ // --- LAYERED CUSTOM ISLAND LOGIC ---
val data: HyperIslandData = when (type) {
NotificationType.CALL -> callTranslator.translate(sbn, picKey, finalConfig, activeTheme)
NotificationType.NAVIGATION -> {
- val navLayout = preferences.getEffectiveNavLayout(sbn.packageName).first()
+ // --- LAYERED NAVIGATION LOGIC ---
+ val navLayout = getEffectiveNav(sbn.packageName)
navTranslator.translate(sbn, picKey, finalConfig, navLayout.first, navLayout.second, activeTheme)
}
NotificationType.TIMER -> timerTranslator.translate(sbn, picKey, finalConfig, activeTheme)
- NotificationType.PROGRESS -> progressTranslator.translate(sbn, effectiveTitle, picKey, finalConfig, activeTheme)
+ NotificationType.PROGRESS -> progressTranslator.translate(sbn, effectiveTitle, picKey, finalConfig, activeTheme, isUpdate)
+ NotificationType.DOWNLOAD -> downloadTranslator.translate(sbn, effectiveTitle, picKey, finalConfig, activeTheme, isUpdate)
NotificationType.MEDIA -> mediaTranslator.translate(sbn, picKey, finalConfig)
else -> standardTranslator.translate(sbn, effectiveTitle, effectiveText, picKey, finalConfig, activeTheme)
}
val newContentHash = data.jsonParam.hashCode()
+ if (isUpdate && previous != null && previous.lastContentHash == newContentHash) return
- if (isUpdate && previous != null && previous.lastContentHash == newContentHash) {
- Log.d(TAG, " ABORTING: Content hash duplicate.")
- return
- }
-
- try {
- val currentNotifs = activeNotifications
- val exists = currentNotifs.any { it.key == key }
- if (!exists) return
- } catch (e: Exception) { }
+ val shouldAlertOnce = isUpdate && (type == NotificationType.PROGRESS || type == NotificationType.DOWNLOAD || type == NotificationType.MEDIA)
Log.i(TAG, " POSTING Island -> ID: $bridgeId, Type: $type, FinalTitle: '$effectiveTitle', FinalText: '$effectiveText'")
- postStandardNotification(sbn, bridgeId, data)
+ postStandardNotification(sbn, bridgeId, data, shouldAlertOnce)
activeIslands[key] = ActiveIsland(
id = bridgeId, type = type, postTime = System.currentTimeMillis(),
- packageName = sbn.packageName,
- title = effectiveTitle,
- text = effectiveText,
- subText = "",
- lastContentHash = newContentHash
+ packageName = sbn.packageName, title = effectiveTitle, text = effectiveText,
+ subText = "", lastContentHash = newContentHash
)
+ permanentIslandManager.onActiveNotificationsChanged(activeIslands.size + activeWidgets.size)
+
+ handlePostNotificationSideEffects(key, bridgeId, finalConfig, type, false)
} catch (e: Exception) {
Log.e(TAG, "💥 Error processing standard notification", e)
}
}
+ private fun isDownloadNotification(sbn: StatusBarNotification, title: String, text: String): Boolean {
+ val pkg = sbn.packageName.lowercase()
+ val titleLower = title.lowercase()
+ val textLower = text.lowercase()
+ val channelId = sbn.notification.channelId?.lowercase() ?: ""
+
+ val isMatch = if (pkg.contains("download") || pkg.contains("downloader") || pkg.contains("chrome") ||
+ pkg.contains("browser") || pkg.contains("firefox") || pkg.contains("market") ||
+ pkg.contains("vending") || pkg.contains("play.store") || pkg.contains("playstore") ||
+ pkg.contains("store") || pkg.contains("fdroid") || pkg.contains("samsungapps") ||
+ pkg.contains("mipicks") || pkg.contains("venezia") || pkg.contains("packageinstaller") ||
+ pkg.contains("installer") || pkg.contains("gms") || channelId.contains("download") ||
+ channelId.contains("install")) {
+ true
+ } else {
+ val extras = sbn.notification.extras
+ val subText = extras.getCharSequence(Notification.EXTRA_SUB_TEXT)?.toString()?.lowercase() ?: ""
+ val infoText = extras.getCharSequence(Notification.EXTRA_INFO_TEXT)?.toString()?.lowercase() ?: ""
+
+ val downloadKeywords = listOf(
+ // English
+ "download", "install", "update", "updat", "upload", "transfer",
+ // Spanish / Portuguese / Italian / French
+ "descarg", "baix", "telecharg", "instal", "actuali", "carg", "subi", "transf",
+ // German
+ "laden", "gelad", "aktualis",
+ // Polish
+ "pobier", "pobran", "aktual",
+ // Russian / Ukrainian
+ "скач", "загруз", "устан", "обнов"
+ )
+ downloadKeywords.any {
+ titleLower.contains(it) ||
+ textLower.contains(it) ||
+ subText.contains(it) ||
+ infoText.contains(it)
+ }
+ }
+
+ Log.d(TAG, "🔍 isDownloadNotification check: pkg=$pkg, channelId='$channelId', title='$title', text='$text', resolved=$isMatch")
+ return isMatch
+ }
+
+ private fun hasProgressNotification(sbn: StatusBarNotification, title: String, text: String): Boolean {
+ val extras = sbn.notification.extras
+ val isDownload = isDownloadNotification(sbn, title, text)
+ return extras.getInt(Notification.EXTRA_PROGRESS_MAX, 0) > 0 ||
+ extras.getBoolean(Notification.EXTRA_PROGRESS_INDETERMINATE) ||
+ (isDownload && extractTextPercentage(title, text) != null)
+ }
+
+ private fun extractTextPercentage(title: String?, text: String?): Int? {
+ val pattern = Regex("""\b(\d{1,3})\s*%""")
+ val textMatch = text?.let { pattern.find(it) }
+ val titleMatch = title?.let { pattern.find(it) }
+ val match = textMatch ?: titleMatch
+ if (match != null) {
+ val value = match.groupValues[1].toIntOrNull()
+ if (value != null && value in 0..100) {
+ return value
+ }
+ }
+ return null
+ }
+
private fun resolveTitle(sbn: StatusBarNotification): String {
val extras = sbn.notification.extras
val title = extras.getCharSequence(Notification.EXTRA_TITLE)?.toString()?.trim() ?: ""
@@ -489,9 +634,7 @@ class NotificationReaderService : NotificationListenerService() {
if ((title.isEmpty() || title.equals(pkg, ignoreCase = true)) && !bigTitle.isNullOrEmpty()) {
return bigTitle
}
-
if (title.equals(pkg, ignoreCase = true)) return ""
-
return title
}
@@ -505,12 +648,11 @@ class NotificationReaderService : NotificationListenerService() {
private suspend fun ensureValidSbn(sbn: StatusBarNotification): StatusBarNotification {
val extras = sbn.notification.extras
- val hasProgress = extras.getInt(Notification.EXTRA_PROGRESS_MAX, 0) > 0 ||
- extras.getBoolean(Notification.EXTRA_PROGRESS_INDETERMINATE)
- if (hasProgress) return sbn
-
val title = resolveTitle(sbn)
val text = resolveText(extras)
+ val hasProgress = hasProgressNotification(sbn, title, text)
+ if (hasProgress) return sbn
+
val pkg = sbn.packageName
val isSuspicious = title.isEmpty() || text.equals(pkg, ignoreCase = true)
@@ -520,10 +662,8 @@ class NotificationReaderService : NotificationListenerService() {
try {
val activeList = activeNotifications
val updatedSbn = activeList?.firstOrNull { it.key == sbn.key }
- if (updatedSbn != null) {
- return updatedSbn
- }
- } catch (e: Exception) { }
+ if (updatedSbn != null) return updatedSbn
+ } catch (_: Exception) { }
}
return sbn
}
@@ -536,27 +676,37 @@ class NotificationReaderService : NotificationListenerService() {
val isNav = n.category == Notification.CATEGORY_NAVIGATION || sbn.packageName.let { it.contains("maps") || it.contains("waze") }
val isTimer = (extras.getBoolean(Notification.EXTRA_SHOW_CHRONOMETER) || n.category == Notification.CATEGORY_ALARM) && n.`when` > 0
val isMedia = template.contains("MediaStyle") || n.category == Notification.CATEGORY_TRANSPORT
- val hasProgress = extras.getInt(Notification.EXTRA_PROGRESS_MAX, 0) > 0
+
+ val title = resolveTitle(sbn)
+ val text = resolveText(extras)
+ val isDownload = isDownloadNotification(sbn, title, text)
+ val hasProgress = hasProgressNotification(sbn, title, text)
return when {
isCall -> NotificationType.CALL
isNav -> NotificationType.NAVIGATION
isTimer -> NotificationType.TIMER
isMedia -> NotificationType.MEDIA
- hasProgress -> NotificationType.PROGRESS
+ hasProgress -> {
+ if (isDownload) {
+ NotificationType.DOWNLOAD
+ } else {
+ NotificationType.PROGRESS
+ }
+ }
else -> NotificationType.STANDARD
}
}
@RequiresPermission(Manifest.permission.POST_NOTIFICATIONS)
- private fun postStandardNotification(sbn: StatusBarNotification, bridgeId: Int, data: HyperIslandData) {
+ private fun postStandardNotification(sbn: StatusBarNotification, bridgeId: Int, data: HyperIslandData, shouldAlertOnce: Boolean) {
val builder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setContentTitle(getString(R.string.app_name))
- .setContentText("Active Island")
+ .setContentText(getString(R.string.notification_went_wrong))
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setOngoing(true)
- .setOnlyAlertOnce(true)
+ .setOnlyAlertOnce(shouldAlertOnce)
val extras = Bundle()
extras.putString(EXTRA_ORIGINAL_KEY, sbn.key)
@@ -568,7 +718,11 @@ class NotificationReaderService : NotificationListenerService() {
val notification = builder.build()
notification.extras.putString("miui.focus.param", data.jsonParam)
- NotificationManagerCompat.from(this).notify(bridgeId, notification)
+ if (!shouldAlertOnce) {
+ com.d4viddf.hyperbridge.util.ShizukuManager.notifyWithCancel(this, bridgeId, notification)
+ } else {
+ com.d4viddf.hyperbridge.util.ShizukuManager.notify(this, bridgeId, notification)
+ }
activeTranslations[sbn.key] = bridgeId
reverseTranslations[bridgeId] = sbn.key
@@ -590,11 +744,8 @@ class NotificationReaderService : NotificationListenerService() {
}
manager.createNotificationChannel(widgetChannel)
- // [NEW] Channel specifically for Native Android Live Updates
val liveUpdateChannel = NotificationChannel(LIVE_UPDATE_CHANNEL_ID, getString(R.string.channel_live_updates), NotificationManager.IMPORTANCE_DEFAULT).apply {
- setSound(null, null)
- enableVibration(false)
- setShowBadge(false)
+ setSound(null, null); enableVibration(false); setShowBadge(false)
}
manager.createNotificationChannel(liveUpdateChannel)
}
@@ -613,6 +764,8 @@ class NotificationReaderService : NotificationListenerService() {
try {
val data = widgetTranslator.translate(widgetId)
postWidgetNotification(WIDGET_ID_BASE + widgetId, data)
+ activeWidgets.add(widgetId)
+ permanentIslandManager.onActiveNotificationsChanged(activeIslands.size + activeWidgets.size)
} catch (e: Exception) { Log.e(TAG, "Failed widget $widgetId", e) }
}
@@ -620,7 +773,7 @@ class NotificationReaderService : NotificationListenerService() {
private fun postWidgetNotification(notificationId: Int, data: HyperIslandData) {
val builder = NotificationCompat.Builder(this, WIDGET_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_launcher_foreground)
- .setContentTitle("Widget Overlay").setContentText("Active")
+ .setContentTitle("Widget Overlay").setContentText(getString(R.string.widget_went_wrong))
.setPriority(NotificationCompat.PRIORITY_LOW).setOngoing(true)
.setOnlyAlertOnce(true).addExtras(data.resources)
@@ -630,15 +783,50 @@ class NotificationReaderService : NotificationListenerService() {
val notification = builder.build()
notification.extras.putString("miui.focus.param", data.jsonParam)
- NotificationManagerCompat.from(this).notify(notificationId, notification)
+ com.d4viddf.hyperbridge.util.ShizukuManager.notify(this, notificationId, notification)
}
private fun handleLimitReached(newType: NotificationType, newPkg: String) {
- if (currentMode == IslandLimitMode.MOST_RECENT) {
- val oldest = activeIslands.minByOrNull { it.value.postTime }
- oldest?.let {
- NotificationManagerCompat.from(this).cancel(it.value.id)
- cleanupCache(it.key)
+ val oldest = activeIslands.minByOrNull { it.value.postTime } ?: return
+
+ when (currentMode) {
+ IslandLimitMode.FIRST_COME -> {
+ // Ignore the new notification by removing it immediately (or simply returning, but returning here means the caller won't add it)
+ // The logic in the caller says:
+ // if (!isUpdate && activeIslands.size >= MAX_ISLANDS) {
+ // handleLimitReached(type, sbn.packageName)
+ // if (activeIslands.size >= MAX_ISLANDS) return
+ // }
+ // So if we do nothing here, the size remains >= MAX_ISLANDS, and the caller will return.
+ return
+ }
+ IslandLimitMode.MOST_RECENT -> {
+ NotificationManagerCompat.from(this).cancel(oldest.value.id)
+ cleanupCache(oldest.key)
+ }
+ IslandLimitMode.PRIORITY -> {
+ // Check if newPkg has higher priority than existing ones.
+ // Priority is determined by its index in appPriorityList (lower index = higher priority).
+ // If it's not in the list, it has the lowest priority (Int.MAX_VALUE).
+ val newPriority = appPriorityList.indexOf(newPkg).let { if (it == -1) Int.MAX_VALUE else it }
+
+ // Find the existing active island with the lowest priority (highest index value)
+ val lowestPriorityIsland = activeIslands.maxByOrNull {
+ appPriorityList.indexOf(it.value.packageName).let { idx -> if (idx == -1) Int.MAX_VALUE else idx }
+ }
+
+ if (lowestPriorityIsland != null) {
+ val lowestPriority = appPriorityList.indexOf(lowestPriorityIsland.value.packageName).let { if (it == -1) Int.MAX_VALUE else it }
+ if (newPriority <= lowestPriority) {
+ // The new notification has equal or higher priority than the lowest existing one.
+ // Remove the lowest priority existing notification.
+ NotificationManagerCompat.from(this).cancel(lowestPriorityIsland.value.id)
+ cleanupCache(lowestPriorityIsland.key)
+ } else {
+ // The new notification has lower priority than all existing ones. Do nothing, which will ignore it.
+ return
+ }
+ }
}
}
}
@@ -651,28 +839,35 @@ class NotificationReaderService : NotificationListenerService() {
val title = extras.getCharSequence(Notification.EXTRA_TITLE)?.toString()?.trim() ?: ""
val text = extras.getCharSequence(Notification.EXTRA_TEXT)?.toString()?.trim() ?: ""
- val hasProgress = extras.getInt(Notification.EXTRA_PROGRESS_MAX, 0) > 0 || extras.getBoolean(Notification.EXTRA_PROGRESS_INDETERMINATE)
+ val hasProgress = hasProgressNotification(sbn, title, text)
val isSpecial = notification.category == Notification.CATEGORY_TRANSPORT || notification.category == Notification.CATEGORY_CALL ||
notification.category == Notification.CATEGORY_NAVIGATION || extras.getString(Notification.EXTRA_TEMPLATE)?.contains("MediaStyle") == true
if (hasProgress || isSpecial) return false
-
if (title.isEmpty() && text.isEmpty()) return true
-
if (title.equals(pkg, ignoreCase = true) || text.equals(pkg, ignoreCase = true)) return true
-
if (globalBlockedTerms.any { "$title $text".contains(it, true) }) return true
- if ((notification.flags and Notification.FLAG_GROUP_SUMMARY) != 0) return true
+
+ if ((notification.flags and Notification.FLAG_GROUP_SUMMARY) != 0) {
+ // We previously blocked GROUP_ALERT_CHILDREN, but some apps like Telegram
+ // use it while silencing their actual children, leading to no alerts at all.
+ // Let's just allow group summaries if they have actual text.
+ if (text.isEmpty() || title.isEmpty()) return true
+ }
return false
}
private fun getCachedAppLabel(pkg: String): String = appLabelCache.getOrPut(pkg) {
- try { packageManager.getApplicationLabel(packageManager.getApplicationInfo(pkg, 0)).toString() } catch (e: Exception) { "" }
+ try { packageManager.getApplicationLabel(packageManager.getApplicationInfo(pkg, 0)).toString() } catch (_: Exception) { "" }
}
private fun shouldIgnore(packageName: String): Boolean = packageName == this.packageName || packageName == "android" || packageName.contains("miui.notification")
private fun isAppAllowed(packageName: String): Boolean = allowedPackageSet.contains(packageName)
override fun onListenerConnected() { Log.i(TAG, "HyperBridge Service Connected") }
- override fun onDestroy() { super.onDestroy(); serviceScope.cancel() }
+ override fun onDestroy() {
+ super.onDestroy()
+ unregisterReceiver(userUnlockedReceiver)
+ serviceScope.cancel()
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/service/PermanentIslandManager.kt b/app/src/main/java/com/d4viddf/hyperbridge/service/PermanentIslandManager.kt
new file mode 100644
index 0000000..b5708ad
--- /dev/null
+++ b/app/src/main/java/com/d4viddf/hyperbridge/service/PermanentIslandManager.kt
@@ -0,0 +1,119 @@
+package com.d4viddf.hyperbridge.service
+
+import android.Manifest
+import android.content.Context
+import android.util.Log
+import androidx.annotation.RequiresPermission
+import androidx.core.app.NotificationCompat
+import com.d4viddf.hyperbridge.R
+import com.d4viddf.hyperbridge.data.AppPreferences
+import com.d4viddf.hyperbridge.models.HyperIslandData
+import com.d4viddf.hyperbridge.util.ShizukuManager
+import io.github.d4viddf.hyperisland_kit.HyperIslandNotification
+import io.github.d4viddf.hyperisland_kit.models.ImageTextInfoLeft
+import io.github.d4viddf.hyperisland_kit.models.TextInfo
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+
+
+class PermanentIslandManager(
+ private val context: Context,
+ private val scope: CoroutineScope,
+ private val preferences: AppPreferences
+) {
+ private val TAG = "HyperBridgeDebug"
+ private val PERMANENT_BRIDGE_ID = 9999
+
+ private var isPermanentIslandEnabled = false
+ private var isIslandActive = false
+ private var currentRealNotifications = 0
+ private var currentWidth = 0
+
+ init {
+ scope.launch {
+ preferences.isPermanentIslandEnabledFlow.collectLatest { enabled ->
+ isPermanentIslandEnabled = enabled
+ updateState()
+ }
+ }
+ scope.launch {
+ preferences.permanentIslandWidthFlow.collectLatest { width ->
+ currentWidth = width
+ if (isIslandActive) {
+ dispatchPermanentIsland()
+ }
+ }
+ }
+ }
+
+ fun onActiveNotificationsChanged(count: Int) {
+ currentRealNotifications = count
+ updateState()
+ }
+
+ private fun updateState() {
+ if (isPermanentIslandEnabled && currentRealNotifications == 0) {
+ if (!isIslandActive) {
+ dispatchPermanentIsland()
+ isIslandActive = true
+ }
+ } else {
+ if (isIslandActive) {
+ removePermanentIsland()
+ isIslandActive = false
+ }
+ }
+ }
+
+ @RequiresPermission(Manifest.permission.POST_NOTIFICATIONS)
+ private fun dispatchPermanentIsland() {
+ try {
+ Log.d(TAG, "Dispatching permanent island")
+
+ val builder = HyperIslandNotification.Builder(context, "permanent_island", "Permanent Island")
+
+ // Should not be dismissible and shouldn't show in shade
+ builder.setEnableFloat(false)
+ builder.setIslandConfig(timeout = 0, dismissible = false, highlightColor = "#FFFFFF", expandedTimeMs = 0)
+ builder.setShowNotification(false)
+ builder.setReopen(false)
+ builder.setIslandFirstFloat(false)
+
+ // Only big paramislands with empty values for textonleft and picKey = ""
+ // Use width spaces to change width
+ val emptyString = "\u00A0".repeat(currentWidth)
+ builder.setBigIslandInfo(
+ left = ImageTextInfoLeft(1, null, TextInfo(emptyString, emptyString)),
+ right = null
+ )
+
+ val data = HyperIslandData(builder.buildResourceBundle(), builder.buildJsonParam())
+
+ val notifBuilder = NotificationCompat.Builder(context, "hyper_bridge_notification_channel")
+ .setSmallIcon(R.drawable.ic_launcher_foreground)
+ .setContentTitle("Permanent Island")
+ .setContentText("Empty Island")
+ .setPriority(NotificationCompat.PRIORITY_MIN)
+ .setOngoing(true)
+
+ notifBuilder.addExtras(data.resources)
+
+ val notification = notifBuilder.build()
+ notification.extras.putString("miui.focus.param", data.jsonParam)
+
+ ShizukuManager.notify(context, PERMANENT_BRIDGE_ID, notification)
+ } catch (e: Exception) {
+ Log.e(TAG, "Error dispatching permanent island", e)
+ }
+ }
+
+ private fun removePermanentIsland() {
+ try {
+ Log.d(TAG, "Removing permanent island")
+ ShizukuManager.cancel(context, PERMANENT_BRIDGE_ID)
+ } catch (e: Exception) {
+ Log.e(TAG, "Error removing permanent island", e)
+ }
+ }
+}
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/service/WidgetOverlayService.kt b/app/src/main/java/com/d4viddf/hyperbridge/service/WidgetOverlayService.kt
index c4e4d63..483fb19 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/service/WidgetOverlayService.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/service/WidgetOverlayService.kt
@@ -173,7 +173,7 @@ class WidgetOverlayService : Service() {
// Pass JSON param for the Island UI
notification.extras.putString("miui.focus.param", data.jsonParam)
- NotificationManagerCompat.from(this).notify(notificationId, notification)
+ com.d4viddf.hyperbridge.util.ShizukuManager.notify(this, notificationId, notification)
}
private fun createWidgetChannel() {
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/service/translators/BaseTranslator.kt b/app/src/main/java/com/d4viddf/hyperbridge/service/translators/BaseTranslator.kt
index a63c9b8..bee0bf2 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/service/translators/BaseTranslator.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/service/translators/BaseTranslator.kt
@@ -16,7 +16,6 @@ import android.graphics.RectF
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.Icon
-import android.os.Build
import android.os.Bundle
import android.os.Parcelable
import android.service.notification.StatusBarNotification
@@ -50,21 +49,11 @@ abstract class BaseTranslator(
private val appColorCache = ConcurrentHashMap()
protected inline fun Bundle.getParcelableCompat(key: String): T? {
- return if (Build.VERSION.SDK_INT >= 33) {
- getParcelable(key, T::class.java)
- } else {
- @Suppress("DEPRECATION")
- getParcelable(key)
- }
+ return getParcelable(key, T::class.java)
}
protected inline fun Bundle.getParcelableArrayListCompat(key: String): ArrayList? {
- return if (Build.VERSION.SDK_INT >= 33) {
- getParcelableArrayList(key, T::class.java)
- } else {
- @Suppress("DEPRECATION")
- getParcelableArrayList(key)
- }
+ return getParcelableArrayList(key, T::class.java)
}
// --- THEME HELPERS ---
@@ -178,10 +167,52 @@ abstract class BaseTranslator(
}
protected fun resolveIcon(sbn: StatusBarNotification, picKey: String): HyperPicture {
- val originalBitmap = getNotificationBitmap(sbn) ?: createFallbackBitmap()
+ var originalBitmap = getNotificationBitmap(sbn) ?: createFallbackBitmap()
+ if (isBitmapDarkAndMonochrome(originalBitmap)) {
+ originalBitmap = tintBitmap(originalBitmap, Color.WHITE)
+ }
return HyperPicture(picKey, originalBitmap)
}
+ protected fun isBitmapDarkAndMonochrome(bitmap: Bitmap): Boolean {
+ val width = bitmap.width
+ val height = bitmap.height
+ if (width == 0 || height == 0) return false
+
+ var darkPixels = 0
+ var totalPixels = 0
+ var isMonochrome = true
+
+ val stepX = maxOf(1, width / 20)
+ val stepY = maxOf(1, height / 20)
+
+ for (x in 0 until width step stepX) {
+ for (y in 0 until height step stepY) {
+ val pixel = bitmap.getPixel(x, y)
+ val alpha = Color.alpha(pixel)
+ if (alpha > 50) {
+ totalPixels++
+ val r = Color.red(pixel)
+ val g = Color.green(pixel)
+ val b = Color.blue(pixel)
+
+ val diff = abs(r - g) + abs(g - b) + abs(b - r)
+ if (diff > 45) {
+ isMonochrome = false
+ }
+
+ val luminance = (0.299 * r + 0.587 * g + 0.114 * b)
+ if (luminance < 80) {
+ darkPixels++
+ }
+ }
+ }
+ }
+
+ if (totalPixels == 0 || !isMonochrome) return false
+ return (darkPixels.toFloat() / totalPixels) > 0.7f
+ }
+
// --- THEME APPLICATION LOGIC ---
protected fun applyThemeToActionIcon(source: Bitmap, shapeId: String, paddingPercent: Int, bgColor: Int): Bitmap {
@@ -356,6 +387,21 @@ abstract class BaseTranslator(
val picture = extras.getParcelableCompat(Notification.EXTRA_PICTURE)
if (picture != null) return picture
+ val template = extras.getString(Notification.EXTRA_TEMPLATE)
+ if (template == "android.app.Notification\$MessagingStyle") {
+ val messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES)
+ if (messages != null && messages.isNotEmpty()) {
+ val lastMessage = messages.last() as? Bundle
+ if (lastMessage != null) {
+ val senderPerson = lastMessage.getParcelableCompat("sender_person")
+ if (senderPerson?.icon != null) {
+ val bitmap = loadIconBitmap(senderPerson.icon!!, pkg)
+ if (bitmap != null) return bitmap
+ }
+ }
+ }
+ }
+
if (sbn.notification.category == Notification.CATEGORY_CALL) {
val person = extras.getParcelableCompat(Notification.EXTRA_MESSAGING_PERSON)
?: extras.getParcelableArrayListCompat(Notification.EXTRA_PEOPLE_LIST)?.firstOrNull()
@@ -453,6 +499,20 @@ abstract class BaseTranslator(
}
}
+ protected fun extractTextPercentage(title: String?, text: String?): Int? {
+ val pattern = Regex("""\b(\d{1,3})\s*%""")
+ val textMatch = text?.let { pattern.find(it) }
+ val titleMatch = title?.let { pattern.find(it) }
+ val match = textMatch ?: titleMatch
+ if (match != null) {
+ val value = match.groupValues[1].toIntOrNull()
+ if (value != null && value in 0..100) {
+ return value
+ }
+ }
+ return null
+ }
+
protected fun createFallbackBitmap(): Bitmap = createBitmap(1, 1)
protected fun Drawable.toBitmap(width: Int? = null, height: Int? = null): Bitmap {
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/service/translators/CallTranslator.kt b/app/src/main/java/com/d4viddf/hyperbridge/service/translators/CallTranslator.kt
index 5c78c8a..ccce3dc 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/service/translators/CallTranslator.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/service/translators/CallTranslator.kt
@@ -36,7 +36,7 @@ class CallTranslator(
theme: HyperTheme?
): HyperIslandData {
val extras = sbn.notification.extras
- val title = extras.getString(Notification.EXTRA_TITLE) ?: "Call"
+ val title = extras.getCharSequence(Notification.EXTRA_TITLE)?.toString() ?: "Call"
val isChronometerShown = extras.getBoolean(Notification.EXTRA_SHOW_CHRONOMETER)
val baseTime = sbn.notification.`when`
@@ -92,7 +92,7 @@ class CallTranslator(
builder.setSmallIsland(picKey)
val highlight = resolveColor(theme, sbn.packageName, "#FFFFFF")
- builder.setIslandConfig(highlightColor = highlight)
+ builder.setIslandConfig(highlightColor = highlight, expandedTimeMs = config.floatTimeout)
if (isIncoming) {
builder.setBigIslandInfo(
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/service/translators/DownloadTranslator.kt b/app/src/main/java/com/d4viddf/hyperbridge/service/translators/DownloadTranslator.kt
new file mode 100644
index 0000000..d31ce6b
--- /dev/null
+++ b/app/src/main/java/com/d4viddf/hyperbridge/service/translators/DownloadTranslator.kt
@@ -0,0 +1,123 @@
+package com.d4viddf.hyperbridge.service.translators
+
+import android.app.Notification
+import android.content.Context
+import android.service.notification.StatusBarNotification
+import com.d4viddf.hyperbridge.R
+import com.d4viddf.hyperbridge.data.theme.ThemeRepository
+import com.d4viddf.hyperbridge.models.HyperIslandData
+import com.d4viddf.hyperbridge.models.IslandConfig
+import com.d4viddf.hyperbridge.models.theme.HyperTheme
+import io.github.d4viddf.hyperisland_kit.HyperIslandNotification
+import io.github.d4viddf.hyperisland_kit.HyperPicture
+import io.github.d4viddf.hyperisland_kit.models.ImageTextInfoLeft
+import io.github.d4viddf.hyperisland_kit.models.ImageTextInfoRight
+import io.github.d4viddf.hyperisland_kit.models.PicInfo
+import io.github.d4viddf.hyperisland_kit.models.TextInfo
+
+class DownloadTranslator(context: Context, repo: ThemeRepository) : BaseTranslator(context, repo) {
+
+ private val finishKeywords by lazy {
+ context.resources.getStringArray(R.array.progress_finish_keywords).toList()
+ }
+
+ fun translate(
+ sbn: StatusBarNotification,
+ title: String,
+ picKey: String,
+ config: IslandConfig,
+ theme: HyperTheme?,
+ isUpdate: Boolean
+ ): HyperIslandData {
+
+ val themeProgressColor = theme?.defaultProgress?.activeColor
+ ?: resolveColor(theme, sbn.packageName, "#007AFF")
+
+ val themeFinishColor = theme?.defaultProgress?.finishedColor
+ ?: resolveColor(theme, sbn.packageName, "#34C759")
+
+ val customTick = getThemeBitmap(theme, "tick_icon")
+
+ val builder = HyperIslandNotification.Builder(context, "bridge_${sbn.packageName}", title)
+
+ builder.setShowNotification(config.isShowShade ?: true)
+
+ val isFloatEnabled = config.isFloat ?: false
+ builder.setEnableFloat(isFloatEnabled && !isUpdate)
+ builder.setIslandFirstFloat(config.isFloat ?: false)
+
+ val extras = sbn.notification.extras
+ val max = extras.getInt(Notification.EXTRA_PROGRESS_MAX, 0)
+ val current = extras.getInt(Notification.EXTRA_PROGRESS, 0)
+ val indeterminate = extras.getBoolean(Notification.EXTRA_PROGRESS_INDETERMINATE)
+ val textContent = (extras.getCharSequence(Notification.EXTRA_TEXT)?.toString() ?: "")
+
+ val textPercent = extractTextPercentage(title, textContent)
+ val percent = if (max > 0) {
+ ((current.toFloat() / max.toFloat()) * 100).toInt()
+ } else {
+ textPercent ?: 0
+ }
+ val isIndeterminate = indeterminate && textPercent == null
+ val isTextFinished = finishKeywords.any { textContent.contains(it, ignoreCase = true) }
+ val isFinished = percent >= 100 || isTextFinished
+
+ val tickKey = "${picKey}_tick"
+ val hiddenKey = "hidden_pixel"
+
+ builder.addPicture(resolveIcon(sbn, picKey))
+ builder.addPicture(getTransparentPicture(hiddenKey))
+
+ if (isFinished) {
+ if (customTick != null) {
+ builder.addPicture(HyperPicture(tickKey, customTick))
+ } else {
+ builder.addPicture(getColoredPicture(tickKey, R.drawable.rounded_check_circle_24, themeFinishColor))
+ }
+ }
+
+ val actions = extractBridgeActions(sbn, theme)
+
+ builder.setChatInfo(
+ title = title,
+ content = if (isFinished) context.getString(R.string.download_complete) else textContent,
+ pictureKey = picKey,
+ appPkg = sbn.packageName
+ )
+
+ if (!isFinished && !isIndeterminate) {
+ builder.setProgressBar(percent, themeProgressColor)
+ }
+
+ if (isFinished) {
+ builder.setBigIslandInfo(
+ left = ImageTextInfoLeft(1, PicInfo(1, hiddenKey)),
+ right = ImageTextInfoRight(2, PicInfo(1, tickKey))
+ )
+ builder.setSmallIsland(tickKey)
+ builder.setIslandConfig(timeout = config.timeout , dismissible = true, expandedTimeMs = if (isFloatEnabled) config.floatTimeout else null)
+ } else {
+ if (isIndeterminate) {
+ builder.setBigIslandInfo(
+ left = ImageTextInfoLeft(1, PicInfo(1, picKey), TextInfo("", "")),
+ right = ImageTextInfoRight(1, PicInfo(1, hiddenKey), TextInfo(title, context.getString(R.string.downloading)))
+ )
+ builder.setSmallIsland(picKey)
+ } else {
+ builder.setBigIslandProgressCircle(picKey, "", percent, themeProgressColor, true)
+ builder.setSmallIslandCircularProgress(picKey, percent, themeProgressColor, isCCW = true)
+ }
+ }
+
+ val highlight = resolveColor(theme, sbn.packageName, themeProgressColor)
+ builder.setIslandConfig(timeout = config.timeout, highlightColor = highlight, expandedTimeMs = config.floatTimeout)
+ actions.forEach { it.actionImage?.let { pic -> builder.addPicture(pic) } }
+ val hyperActions = actions.map { it.action }.toTypedArray()
+ hyperActions.forEach {
+ builder.addAction(it)
+ }
+ hyperActions.forEach { builder.addHiddenAction(it) }
+
+ return HyperIslandData(builder.buildResourceBundle(), builder.buildJsonParam())
+ }
+}
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/service/translators/LiveUpdateTranslator.kt b/app/src/main/java/com/d4viddf/hyperbridge/service/translators/LiveUpdateTranslator.kt
index e83e514..f2e1d79 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/service/translators/LiveUpdateTranslator.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/service/translators/LiveUpdateTranslator.kt
@@ -2,13 +2,14 @@ package com.d4viddf.hyperbridge.service.translators
import android.app.Notification
import android.content.Context
-import android.graphics.Bitmap
import android.service.notification.StatusBarNotification
import androidx.core.app.NotificationCompat
import androidx.core.graphics.drawable.IconCompat
+import androidx.core.graphics.toColorInt
import com.d4viddf.hyperbridge.R
import com.d4viddf.hyperbridge.data.theme.ThemeRepository
-import com.d4viddf.hyperbridge.models.IslandConfig
+import com.d4viddf.hyperbridge.models.NavContent
+import com.d4viddf.hyperbridge.models.NotificationType
class LiveUpdateTranslator(
context: Context,
@@ -17,8 +18,9 @@ class LiveUpdateTranslator(
fun translateToLiveUpdate(
sbn: StatusBarNotification,
- config: IslandConfig,
- channelId: String
+ channelId: String,
+ type: NotificationType,
+ navRight: NavContent? = null
): NotificationCompat.Builder {
val original = sbn.notification
val extras = original.extras
@@ -26,55 +28,50 @@ class LiveUpdateTranslator(
val title = extras.getCharSequence(Notification.EXTRA_TITLE)?.toString() ?: ""
val text = extras.getCharSequence(Notification.EXTRA_TEXT)?.toString() ?: ""
- var progressMax = extras.getInt(Notification.EXTRA_PROGRESS_MAX, 0)
- var progress = extras.getInt(Notification.EXTRA_PROGRESS, 0)
- var indeterminate = extras.getBoolean(Notification.EXTRA_PROGRESS_INDETERMINATE, false)
-
- // Identify if it's media so we can use the title for the Island chip
- val isMedia = extras.containsKey(Notification.EXTRA_MEDIA_SESSION) ||
- extras.getString(Notification.EXTRA_TEMPLATE)?.contains("MediaStyle") == true
-
- // [FIXED] Media apps often leave random progress flags in the background.
- // Force them off so it doesn't show an indeterminate loading bar!
- if (isMedia) {
- progressMax = 0
- progress = 0
- indeterminate = false
- }
- // Force category for Android 16 promotion limits
- val validCategory = if (original.category.isNullOrEmpty() || original.category == NotificationCompat.CATEGORY_SERVICE) {
- if (progressMax > 0 || indeterminate) NotificationCompat.CATEGORY_PROGRESS else NotificationCompat.CATEGORY_TRANSPORT
- } else {
- original.category
- }
+ val progressMax = extras.getInt(Notification.EXTRA_PROGRESS_MAX, 0)
+ val progress = extras.getInt(Notification.EXTRA_PROGRESS, 0)
+ val indeterminate = extras.getBoolean(Notification.EXTRA_PROGRESS_INDETERMINATE, false)
val builder = NotificationCompat.Builder(context, channelId)
- .setSmallIcon(original.smallIcon?.let { IconCompat.createFromIcon(context, it) } ?: IconCompat.createWithResource(context, R.drawable.ic_launcher_foreground))
.setContentTitle(title)
.setContentText(text)
.setOngoing(true)
- .setOnlyAlertOnce(true)
- .setCategory(validCategory)
- .setContentIntent(original.contentIntent)
+ .setCategory(original.category)
- // --- ADD PICTURE / LARGE ICON ---
- if (android.os.Build.VERSION.SDK_INT >= 23 && original.getLargeIcon() != null) {
- builder.setLargeIcon(original.getLargeIcon())
- } else {
- @Suppress("DEPRECATION")
- val picture = extras.getParcelable(Notification.EXTRA_PICTURE)
- if (picture != null) builder.setLargeIcon(picture)
- }
+ original.contentIntent?.let { builder.setContentIntent(it) }
+
+ // --- THEME COLOR & ICON INJECTION ---
+ val theme = repository?.activeTheme?.value
- // Carry over the original timestamp (Standard behavior)
- if (original.`when` > 0) {
- builder.setWhen(original.`when`)
- builder.setShowWhen(true)
+ if (type == NotificationType.NAVIGATION) {
+ // 1. Inject Theme Nav Color
+ val themeColorStr = theme?.defaultNavigation?.progressBarColor
+ ?: resolveColor(theme, sbn.packageName, "#34C759") // Green fallback
+
+ val themeColorInt = try {
+ themeColorStr.toColorInt()
+ } catch (_: Exception) {
+ original.color
+ }
+ builder.setColor(themeColorInt)
+
+ // 2. Inject Theme Nav Icon
+ val navStartBitmap = getThemeBitmap(theme, "nav_start")
+ if (navStartBitmap != null) {
+ builder.setSmallIcon(IconCompat.createWithBitmap(navStartBitmap))
+ } else {
+ builder.setSmallIcon(original.smallIcon?.let { IconCompat.createFromIcon(context, it) } ?: IconCompat.createWithResource(context, R.drawable.ic_launcher_foreground))
+ }
+ } else {
+ // Standard fallback for non-navigation
+ builder.setColor(original.color)
+ builder.setSmallIcon(original.smallIcon?.let { IconCompat.createFromIcon(context, it) } ?: IconCompat.createWithResource(context, R.drawable.ic_launcher_foreground))
}
- // --- COPY BUTTONS ---
- original.actions?.forEach { action ->
- val iconCompat = if ( action.getIcon() != null) {
+ // --- ACTIONS ---
+ val rawActions = original.actions ?: emptyArray()
+ rawActions.forEach { action ->
+ val iconCompat = if (action.getIcon() != null) {
IconCompat.createFromIcon(context, action.getIcon()!!)
} else {
IconCompat.createWithResource(context, action.icon)
@@ -83,31 +80,78 @@ class LiveUpdateTranslator(
}
// --- APPLY STYLES ---
+ // BigTextStyle ensures text isn't completely hidden by the progress bar
+ builder.setStyle(NotificationCompat.BigTextStyle().bigText(text).setBigContentTitle(title))
+
+ // Add back the progress bar to show the user where they are
if (progressMax > 0 || indeterminate) {
builder.setProgress(progressMax, progress, indeterminate)
- } else {
- // Standard notification: Allow expanding text
- builder.setStyle(NotificationCompat.BigTextStyle().bigText(text).setBigContentTitle(title))
}
// --- ANDROID 16 LIVE UPDATE INJECTION ---
- val shortAlertText = generateCriticalShortText(title, text, progress, progressMax, isMedia)
+ val shortAlertText = generateCriticalShortText(title, text, progress, progressMax, type, navRight, sbn)
+
builder.setRequestPromotedOngoing(true)
builder.setShortCriticalText(shortAlertText)
-
return builder
}
- private fun generateCriticalShortText(title: String, text: String, progress: Int, max: Int, isMedia: Boolean): String {
- if (isMedia) return title.ifBlank { "Media" }
+ private fun generateCriticalShortText(
+ title: String,
+ text: String,
+ progress: Int,
+ max: Int,
+ type: NotificationType,
+ navRight: NavContent?,
+ sbn: StatusBarNotification
+ ): String {
+
+ if (type == NotificationType.MEDIA) return title.ifBlank { "Media" }
+
+ // Advanced Extraction for Navigation Layouts
+ if (type == NotificationType.NAVIGATION) {
+ val extras = sbn.notification.extras
+ val bigText = extras.getCharSequence(Notification.EXTRA_BIG_TEXT)?.toString()?.replace("\n", " ")?.trim() ?: ""
+ val subText = extras.getCharSequence(Notification.EXTRA_SUB_TEXT)?.toString()?.replace("\n", " ")?.trim() ?: ""
+
+ val timeRegex = Regex("(\\d{1,2}:\\d{2})|(\\d+h\\s*\\d+m)", RegexOption.IGNORE_CASE)
+ val distanceRegex = Regex("^\\d+([,.]\\d+)?\\s*(m|km|ft|mi|yd|yards|miles|meters)", RegexOption.IGNORE_CASE)
+
+ var distance = ""
+ var eta = ""
+
+ // Extract ETA
+ if (timeRegex.containsMatchIn(subText)) eta = subText
+ else if (timeRegex.containsMatchIn(text) && !distanceRegex.containsMatchIn(text)) eta = text
+
+ // Extract Distance
+ val candidates = listOf(bigText, title, text).filter { it.isNotEmpty() }
+ val contentSource = candidates.firstOrNull { str -> distanceRegex.containsMatchIn(str) } ?: title.ifEmpty { text }
+
+ if (distanceRegex.containsMatchIn(contentSource)) {
+ distanceRegex.find(contentSource)?.let { distance = it.value }
+ }
+
+ // Return the value based on the user's customized Right Side layout!
+ return when (navRight) {
+ NavContent.ETA -> eta.ifEmpty { distance }
+ NavContent.DISTANCE -> distance.ifEmpty { eta }
+ NavContent.DISTANCE_ETA -> listOf(distance, eta).filter { it.isNotEmpty() }.joinToString(" • ")
+ NavContent.INSTRUCTION -> title
+ else -> eta.ifEmpty { distance }.ifEmpty { title } // Fallback
+ }
+ }
+ // Standard Progress Fallback (Only applied if NOT Navigation)
+ val textPercent = extractTextPercentage(title, text)
if (max > 0) return "${(progress * 100) / max}%"
+ if (textPercent != null) return "$textPercent%"
+ // Timer Fallback
val timeRegex = Regex("(\\d+\\s*(min|m))", RegexOption.IGNORE_CASE)
timeRegex.find(text)?.let { return it.groupValues[1] }
- timeRegex.find(title)?.let { return it.groupValues[1] }
- return title.ifBlank { text }.ifBlank { "Active" }
+ return title
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/service/translators/MediaTranslator.kt b/app/src/main/java/com/d4viddf/hyperbridge/service/translators/MediaTranslator.kt
index a5777dc..f405197 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/service/translators/MediaTranslator.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/service/translators/MediaTranslator.kt
@@ -152,7 +152,7 @@ class MediaTranslator(context: Context) : BaseTranslator(context) {
textInfo = TextInfo(title = "", content = "")
)
)
- builder.setIslandConfig(highlightColor = containerColorHex)
+ builder.setIslandConfig(highlightColor = containerColorHex, expandedTimeMs = config.floatTimeout)
builder.setHideDeco(true).setReopen(true).setShowSmallIcon(true)
return HyperIslandData(builder.buildResourceBundle(), builder.buildJsonParam())
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/service/translators/NavTranslator.kt b/app/src/main/java/com/d4viddf/hyperbridge/service/translators/NavTranslator.kt
index e4c691e..3f44506 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/service/translators/NavTranslator.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/service/translators/NavTranslator.kt
@@ -64,7 +64,7 @@ class NavTranslator(context: Context, repo: ThemeRepository) : BaseTranslator(co
else if (isTimeInfo(text) && !isDistanceInfo(text)) eta = text
val candidates = listOf(bigText, title, text).filter { it.isNotEmpty() }
- val contentSource = candidates.firstOrNull { str -> distanceRegex.containsMatchIn(str) } ?: if (title.isNotEmpty()) title else text
+ val contentSource = candidates.firstOrNull { str -> distanceRegex.containsMatchIn(str) } ?: title.ifEmpty { text }
if (isDistanceInfo(contentSource)) {
val match = distanceRegex.find(contentSource)
@@ -81,7 +81,6 @@ class NavTranslator(context: Context, repo: ThemeRepository) : BaseTranslator(co
// 4. Build Notification
val builder = HyperIslandNotification.Builder(context, "bridge_${sbn.packageName}", instruction)
builder.setEnableFloat(config.isFloat ?: false)
- builder.setIslandConfig(timeout = config.timeout)
builder.setShowNotification(config.isShowShade ?: true)
builder.setIslandFirstFloat(config.isFloat ?: false)
@@ -106,17 +105,12 @@ class NavTranslator(context: Context, repo: ThemeRepository) : BaseTranslator(co
}
// 5. Actions (Important: Nav needs Text Buttons like "Exit")
- // We use extractBridgeActions from BaseTranslator but customize the mode slightly if needed
val rawActions = sbn.notification.actions ?: emptyArray()
val actionKeys = mutableListOf()
rawActions.forEachIndexed { index, action ->
val uniqueKey = "act_${sbn.key.hashCode()}_$index"
- // For Navigation, we usually prefer Text buttons (e.g. "Exit Navigation")
- // So we don't necessarily need the fancy icon shape logic here unless user explicitly styles it.
- // We pass null for background to keep it standard pill style or text only.
-
val hyperAction = HyperAction(
key = uniqueKey,
title = action.title?.toString() ?: "",
@@ -131,8 +125,7 @@ class NavTranslator(context: Context, repo: ThemeRepository) : BaseTranslator(co
actionKeys.add(uniqueKey)
}
- // 6. Shade Layout (The Fix)
- // Revert to setBaseInfo (Type 1) which supports standard notifications with actions.
+ // 6. Shade Layout
val shadeContent = listOf(distance, eta).filter { it.isNotEmpty() }.joinToString(" • ")
builder.setBaseInfo(
@@ -153,7 +146,7 @@ class NavTranslator(context: Context, repo: ThemeRepository) : BaseTranslator(co
)
}
- // 8. Island Layout (Dynamic)
+ // 8. Island Layout (Dynamic from App Preference / Global)
fun getTextInfo(type: NavContent): TextInfo {
return when (type) {
NavContent.INSTRUCTION -> TextInfo(instruction, null)
@@ -170,7 +163,7 @@ class NavTranslator(context: Context, repo: ThemeRepository) : BaseTranslator(co
)
builder.setSmallIsland(picKey)
- builder.setIslandConfig(highlightColor = theme?.global?.highlightColor)
+ builder.setIslandConfig(highlightColor = theme?.global?.highlightColor, expandedTimeMs = config.floatTimeout)
return HyperIslandData(builder.buildResourceBundle(), builder.buildJsonParam())
}
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/service/translators/ProgressTranslator.kt b/app/src/main/java/com/d4viddf/hyperbridge/service/translators/ProgressTranslator.kt
index 517719b..2452a66 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/service/translators/ProgressTranslator.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/service/translators/ProgressTranslator.kt
@@ -26,7 +26,8 @@ class ProgressTranslator(context: Context, repo: ThemeRepository) : BaseTranslat
title: String,
picKey: String,
config: IslandConfig,
- theme: HyperTheme?
+ theme: HyperTheme?,
+ isUpdate: Boolean
): HyperIslandData {
// [FIX] Prioritize Progress Colors -> Global Highlight -> Default
@@ -39,18 +40,27 @@ class ProgressTranslator(context: Context, repo: ThemeRepository) : BaseTranslat
val customTick = getThemeBitmap(theme, "tick_icon")
val builder = HyperIslandNotification.Builder(context, "bridge_${sbn.packageName}", title)
- builder.setEnableFloat(config.isFloat ?: false)
- builder.setIslandConfig(timeout = config.timeout)
+
builder.setShowNotification(config.isShowShade ?: true)
+
+ // Always enable float if the user wants it, but only "First Float" (expand) on the initial appearance
+ val isFloatEnabled = config.isFloat ?: false
+ builder.setEnableFloat(isFloatEnabled && !isUpdate)
builder.setIslandFirstFloat(config.isFloat ?: false)
val extras = sbn.notification.extras
val max = extras.getInt(Notification.EXTRA_PROGRESS_MAX, 0)
val current = extras.getInt(Notification.EXTRA_PROGRESS, 0)
val indeterminate = extras.getBoolean(Notification.EXTRA_PROGRESS_INDETERMINATE)
- val textContent = (extras.getString(Notification.EXTRA_TEXT) ?: "")
+ val textContent = (extras.getCharSequence(Notification.EXTRA_TEXT)?.toString() ?: "")
- val percent = if (max > 0) ((current.toFloat() / max.toFloat()) * 100).toInt() else 0
+ val textPercent = extractTextPercentage(title, textContent)
+ val percent = if (max > 0) {
+ ((current.toFloat() / max.toFloat()) * 100).toInt()
+ } else {
+ textPercent ?: 0
+ }
+ val isIndeterminate = indeterminate && textPercent == null
val isTextFinished = finishKeywords.any { textContent.contains(it, ignoreCase = true) }
val isFinished = percent >= 100 || isTextFinished
@@ -78,32 +88,32 @@ class ProgressTranslator(context: Context, repo: ThemeRepository) : BaseTranslat
appPkg = sbn.packageName
)
- if (!isFinished && !indeterminate) {
+ if (!isFinished && !isIndeterminate) {
builder.setProgressBar(percent, themeProgressColor)
}
if (isFinished) {
builder.setBigIslandInfo(
- left = ImageTextInfoLeft(1, PicInfo(1, hiddenKey), TextInfo("", "")),
- right = ImageTextInfoRight(1, PicInfo(1, tickKey), TextInfo("Finished", title))
+ left = ImageTextInfoLeft(1, PicInfo(1, hiddenKey)),
+ right = ImageTextInfoRight(2, PicInfo(1, tickKey))
)
builder.setSmallIsland(tickKey)
+ builder.setIslandConfig(timeout = config.timeout , dismissible = true, expandedTimeMs = if (isFloatEnabled) config.floatTimeout else null)
} else {
- if (indeterminate) {
+ if (isIndeterminate) {
builder.setBigIslandInfo(
left = ImageTextInfoLeft(1, PicInfo(1, picKey), TextInfo("", "")),
right = ImageTextInfoRight(1, PicInfo(1, hiddenKey), TextInfo(title, "Processing..."))
)
builder.setSmallIsland(picKey)
} else {
- builder.setBigIslandProgressCircle(picKey, "$percent%", percent, themeProgressColor, true)
+ builder.setBigIslandProgressCircle(picKey, "", percent, themeProgressColor, true)
builder.setSmallIslandCircularProgress(picKey, percent, themeProgressColor, isCCW = true)
}
}
val highlight = resolveColor(theme, sbn.packageName, themeProgressColor)
- builder.setIslandConfig(highlightColor = highlight)
-
+ builder.setIslandConfig(timeout = config.timeout, highlightColor = highlight, expandedTimeMs = config.floatTimeout)
actions.forEach { it.actionImage?.let { pic -> builder.addPicture(pic) } }
val hyperActions = actions.map { it.action }.toTypedArray()
hyperActions.forEach {
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/service/translators/StandardTranslator.kt b/app/src/main/java/com/d4viddf/hyperbridge/service/translators/StandardTranslator.kt
index d225bbf..6f4664a 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/service/translators/StandardTranslator.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/service/translators/StandardTranslator.kt
@@ -35,7 +35,6 @@ class StandardTranslator(
val isMedia = template.contains("MediaStyle")
val isCall = sbn.notification.category == Notification.CATEGORY_CALL
- val displayTitle = title
val displayContent = when {
isMedia -> context.getString(R.string.status_now_playing)
isCall && subText.isNotEmpty() -> "$text • $subText"
@@ -45,12 +44,12 @@ class StandardTranslator(
val highlightColor = resolveColor(theme, sbn.packageName, "#FFFFFF")
- val builder = HyperIslandNotification.Builder(context, "bridge_${sbn.packageName}", displayTitle)
+ val builder = HyperIslandNotification.Builder(context, "bridge_${sbn.packageName}", title)
// --- CONFIGURATION ---
builder.setEnableFloat(config.isFloat ?: false)
- builder.setIslandConfig(timeout = config.timeout)
- builder.setShowNotification(config.isShowShade ?: true)
+ builder.setIslandConfig(timeout = config.timeout , dismissible = true, highlightColor = highlightColor, expandedTimeMs = config.floatTimeout)
+ builder.setShowNotification(config.isShowShade ?: false)
builder.setReopen(true)
builder.setIslandFirstFloat(config.isFloat ?: false)
@@ -68,12 +67,12 @@ class StandardTranslator(
// Base Info (Shade)
builder.setBaseInfo(
type = 2,
- title = displayTitle,
+ title = title,
content = displayContent
)
builder.setIconTextInfo(
picKey= picKey,
- title = displayTitle,
+ title = title,
content = displayContent
)
@@ -83,7 +82,7 @@ class StandardTranslator(
} else {
builder.setBigIslandInfo(
left = ImageTextInfoLeft(1, PicInfo(1, picKey), TextInfo("", "")),
- right = ImageTextInfoRight(1, PicInfo(1, hiddenKey), TextInfo(displayTitle, displayContent))
+ right = ImageTextInfoRight(1, PicInfo(1, hiddenKey), TextInfo(title, displayContent))
)
}
@@ -120,7 +119,6 @@ class StandardTranslator(
}
}
- builder.setIslandConfig(highlightColor = highlightColor)
return HyperIslandData(builder.buildResourceBundle(), builder.buildJsonParam())
}
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/service/translators/TimerTranslator.kt b/app/src/main/java/com/d4viddf/hyperbridge/service/translators/TimerTranslator.kt
index 6baeb06..347c906 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/service/translators/TimerTranslator.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/service/translators/TimerTranslator.kt
@@ -40,8 +40,6 @@ class TimerTranslator(context: Context, repo: ThemeRepository) : BaseTranslator(
builder.setIslandConfig(timeout = config.timeout)
builder.setShowNotification(config.isShowShade ?: true)
builder.setIslandFirstFloat(config.isFloat ?: false)
- // Apply Theme Color to Island text
- builder.setIslandConfig(highlightColor = themeHighlight)
val hiddenKey = "hidden_pixel"
builder.addPicture(resolveIcon(sbn, picKey))
@@ -71,7 +69,7 @@ class TimerTranslator(context: Context, repo: ThemeRepository) : BaseTranslator(
builder.addAction(it.action)
it.actionImage?.let { pic -> builder.addPicture(pic) }
}
- builder.setIslandConfig(highlightColor = theme?.global?.highlightColor)
+ builder.setIslandConfig(highlightColor = theme?.global?.highlightColor, expandedTimeMs = config.floatTimeout)
return HyperIslandData(builder.buildResourceBundle(), builder.buildJsonParam())
}
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/ui/AppListViewModel.kt b/app/src/main/java/com/d4viddf/hyperbridge/ui/AppListViewModel.kt
index 05275b3..2d65347 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/ui/AppListViewModel.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/ui/AppListViewModel.kt
@@ -12,14 +12,20 @@ import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.d4viddf.hyperbridge.data.AppCacheManager
import com.d4viddf.hyperbridge.data.AppPreferences
+import com.d4viddf.hyperbridge.data.theme.ThemeRepository
import com.d4viddf.hyperbridge.models.IslandConfig
+import com.d4viddf.hyperbridge.models.NavContent
import com.d4viddf.hyperbridge.models.NotificationType
+import com.d4viddf.hyperbridge.models.theme.HyperTheme
+import com.d4viddf.hyperbridge.models.theme.NavigationModule
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -46,6 +52,10 @@ class AppListViewModel(application: Application) : AndroidViewModel(application)
private val preferences = AppPreferences(application)
private val cacheManager = AppCacheManager(application)
+ // [NEW] Theme Repository to resolve behavior overrides
+ private val themeRepo = ThemeRepository(application)
+ val activeTheme: StateFlow = themeRepo.activeTheme
+
private val _installedApps = MutableStateFlow?>(null)
private val _isLoading = MutableStateFlow(true)
@@ -140,6 +150,59 @@ class AppListViewModel(application: Application) : AndroidViewModel(application)
}
}
+// ========================================================================
+ // EFFECTIVE BEHAVIOR LOGIC (THEME vs PREFS)
+ // ========================================================================
+
+ data class EffectiveAppConfig(
+ val isManagedByTheme: Boolean,
+ val activeTypes: Set,
+ val useNativeEngine: Boolean,
+ val navigationOverride: NavigationModule?,
+ val localNavContent: Pair // Added for the bottom sheet
+ )
+
+ /**
+ * Resolves the "true" settings for an app by layering:
+ * Active Theme > Local App Preferences > Global Fallbacks
+ */
+ fun getEffectiveAppConfigFlow(packageName: String): Flow {
+ return combine(
+ preferences.getAppConfigFlow(packageName),
+ preferences.globalNotificationTypesFlow,
+ preferences.getEffectiveNavLayout(packageName), // Gets the fallback-resolved NavContent
+ activeTheme
+ ) { appPrefTypes, globalTypes, effectiveNavContent, theme ->
+
+ val themeOverride = theme?.apps?.get(packageName)
+ val isManaged = themeOverride != null
+
+ // 1. Resolve Types (Theme -> AppPref -> Global)
+ val effectiveTypes = when {
+ themeOverride?.activeNotificationTypes != null -> themeOverride.activeNotificationTypes
+ appPrefTypes != null -> appPrefTypes
+ else -> globalTypes
+ }
+
+ // 2. Resolve Engine
+ val effectiveEngine = when {
+ themeOverride?.useNativeLiveUpdates != null -> themeOverride.useNativeLiveUpdates
+ else -> true // Global Default
+ }
+
+ // 3. Resolve Navigation Visuals (Theme completely overrides local nav preferences)
+ val effectiveNavVisuals = themeOverride?.navigation
+
+ EffectiveAppConfig(
+ isManagedByTheme = isManaged,
+ activeTypes = effectiveTypes,
+ useNativeEngine = effectiveEngine,
+ navigationOverride = effectiveNavVisuals,
+ localNavContent = effectiveNavContent
+ )
+ }
+ }
+
// --- PREFERENCE ACTIONS ---
fun toggleApp(packageName: String, isEnabled: Boolean) {
@@ -148,10 +211,44 @@ class AppListViewModel(application: Application) : AndroidViewModel(application)
}
}
- fun getAppConfig(packageName: String) = preferences.getAppConfig(packageName)
+ // Standard non-flow getter if needed by other parts of the app
+ suspend fun getAppConfig(packageName: String) = preferences.getAppConfigFlow(packageName).first()
+ /**
+ * Updates notification types. Intelligently routes the save to the
+ * Active Theme JSON if the theme is managing the app, otherwise saves locally.
+ */
fun updateAppConfig(pkg: String, type: NotificationType, enabled: Boolean) {
- viewModelScope.launch { preferences.updateAppConfig(pkg, type, enabled) }
+ viewModelScope.launch {
+ val currentTheme = activeTheme.value
+ val themeOverride = currentTheme?.apps?.get(pkg)
+
+ if (currentTheme != null && themeOverride != null) {
+ // The Theme is managing this app! Update the Theme JSON directly.
+ val currentTypes = themeOverride.activeNotificationTypes ?: emptySet()
+ val newTypes = if (enabled) currentTypes + type.name else currentTypes - type.name
+
+ val updatedOverride = themeOverride.copy(activeNotificationTypes = newTypes)
+ val updatedAppsMap = currentTheme.apps.toMutableMap().apply { put(pkg, updatedOverride) }
+ val updatedTheme = currentTheme.copy(apps = updatedAppsMap)
+
+ // Save to disk and reload the active theme state
+ themeRepo.saveTheme(updatedTheme)
+ themeRepo.activateTheme(updatedTheme.id)
+ } else {
+ // Not managed by theme. Save normally to local AppPreferences.
+ preferences.updateAppConfig(pkg, type, enabled)
+ }
+ }
+ }
+
+ /**
+ * Updates the per-app navigation content layout (Distance vs ETA, etc.)
+ */
+ fun updateAppNavLayout(pkg: String, left: NavContent?, right: NavContent?) {
+ viewModelScope.launch {
+ preferences.updateAppNavLayout(pkg, left, right)
+ }
}
// --- ISLAND CONFIG ---
@@ -224,4 +321,27 @@ class AppListViewModel(application: Application) : AndroidViewModel(application)
draw(canvas)
return bitmap
}
+
+ fun updateAppEngine(pkg: String, useNative: Boolean) {
+ viewModelScope.launch {
+ val currentTheme = activeTheme.value
+ val isCustomTheme = currentTheme != null && currentTheme.id.isNotEmpty()
+
+ if (isCustomTheme) {
+ // If a Custom Theme is active, we MUST patch the Theme JSON,
+ // even if it didn't have an override before!
+ val existingOverride = currentTheme.apps[pkg] ?: com.d4viddf.hyperbridge.models.theme.AppThemeOverride()
+ val updatedOverride = existingOverride.copy(useNativeLiveUpdates = useNative)
+
+ val updatedAppsMap = currentTheme.apps.toMutableMap().apply { put(pkg, updatedOverride) }
+ val updatedTheme = currentTheme.copy(apps = updatedAppsMap)
+
+ themeRepo.saveTheme(updatedTheme)
+ themeRepo.activateTheme(updatedTheme.id)
+ } else {
+ // If NO custom theme is active, save to normal AppPreferences
+ preferences.updateAppEnginePreference(pkg, useNative)
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/ui/components/ChangelogDialog.kt b/app/src/main/java/com/d4viddf/hyperbridge/ui/components/ChangelogDialog.kt
deleted file mode 100644
index 221b1b6..0000000
--- a/app/src/main/java/com/d4viddf/hyperbridge/ui/components/ChangelogDialog.kt
+++ /dev/null
@@ -1,63 +0,0 @@
-package com.d4viddf.hyperbridge.ui.components
-
-import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.AutoAwesome
-import androidx.compose.material3.*
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import com.d4viddf.hyperbridge.R
-import com.d4viddf.hyperbridge.util.parseBold // Import the extension
-
-@Composable
-fun ChangelogDialog(
- currentVersionName: String,
- changelogText: String,
- onDismiss: () -> Unit
-) {
- AlertDialog(
- onDismissRequest = onDismiss,
- icon = {
- Icon(
- Icons.Default.AutoAwesome,
- contentDescription = null,
- modifier = Modifier.size(32.dp),
- tint = MaterialTheme.colorScheme.primary
- )
- },
- title = {
- Text(
- text = stringResource(R.string.whats_new_title, currentVersionName),
- fontWeight = FontWeight.Bold,
- style = MaterialTheme.typography.headlineSmall
- )
- },
- text = {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .heightIn(max = 300.dp)
- .verticalScroll(rememberScrollState())
- ) {
- // FIX: Use parseBold() instead of .replace()
- Text(
- text = changelogText.parseBold(),
- style = MaterialTheme.typography.bodyLarge,
- lineHeight = 24.sp,
- color = MaterialTheme.colorScheme.onSurfaceVariant
- )
- }
- },
- confirmButton = {
- Button(onClick = onDismiss) {
- Text(stringResource(R.string.awesome))
- }
- }
- )
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/ui/components/CommonCards.kt b/app/src/main/java/com/d4viddf/hyperbridge/ui/components/CommonCards.kt
index e473868..61fe6e8 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/ui/components/CommonCards.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/ui/components/CommonCards.kt
@@ -1,23 +1,33 @@
package com.d4viddf.hyperbridge.ui.components
-import android.content.Context
import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Settings
+import androidx.compose.material.icons.automirrored.rounded.ArrowForwardIos
import androidx.compose.material.icons.filled.Warning
-import androidx.compose.material3.*
-import androidx.compose.runtime.*
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
-import com.d4viddf.hyperbridge.util.openAutoStartSettings // Correct Import
-import com.d4viddf.hyperbridge.util.openBatterySettings // Correct Import
-// ... (Keep WarningCard and ExpandableOptimizationCard code from previous step) ...
@Composable
fun WarningCard(
title: String,
@@ -47,54 +57,18 @@ fun WarningCard(
}
}
-@Composable
-fun ExpandableOptimizationCard(context: Context) {
- var expanded by remember { mutableStateOf(false) }
- val cardColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.5f)
- val contentColor = MaterialTheme.colorScheme.onSecondaryContainer
-
- Card(
- colors = CardDefaults.cardColors(containerColor = cardColor),
- shape = RoundedCornerShape(16.dp),
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp, vertical = 8.dp)
- .clickable { expanded = !expanded }
- ) {
- Column(modifier = Modifier.padding(16.dp)) {
- Row(verticalAlignment = Alignment.CenterVertically) {
- Icon(Icons.Default.Settings, null, tint = contentColor)
- Spacer(modifier = Modifier.width(16.dp))
- Text("Xiaomi System Setup", fontWeight = FontWeight.Bold, color = contentColor)
- Spacer(modifier = Modifier.weight(1f))
- Text(if (expanded) "Hide" else "Show", color = contentColor)
- }
- if (expanded) {
- Spacer(modifier = Modifier.height(16.dp))
- Text(
- "HyperOS kills background apps. Apply these settings:",
- style = MaterialTheme.typography.bodyMedium,
- color = contentColor
- )
- Spacer(modifier = Modifier.height(12.dp))
-
- Button(
- onClick = { openAutoStartSettings(context) },
- modifier = Modifier.fillMaxWidth(),
- colors = ButtonDefaults.buttonColors(containerColor = contentColor)
- ) {
- Text("1. Enable Autostart", color = MaterialTheme.colorScheme.surface)
- }
-
- OutlinedButton(
- onClick = { openBatterySettings(context) },
- modifier = Modifier.fillMaxWidth(),
- colors = ButtonDefaults.outlinedButtonColors(contentColor = contentColor)
- ) {
- Text("2. Set Battery 'No Restrictions'")
- }
+@Composable
+fun ListOptionCard(title: String, subtitle: String, icon: ImageVector, shape: Shape, onClick: () -> Unit, trailingContent: (@Composable () -> Unit)? = null) {
+ Card(onClick = onClick, colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer), shape = shape, modifier = Modifier.fillMaxWidth().heightIn(min = 88.dp)) {
+ Row(modifier = Modifier.fillMaxSize().padding(horizontal = 20.dp, vertical = 16.dp), verticalAlignment = Alignment.CenterVertically) {
+ Icon(icon, null, tint = MaterialTheme.colorScheme.onSurfaceVariant)
+ Spacer(Modifier.width(20.dp))
+ Column(modifier = Modifier.weight(1f)) {
+ Text(title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Medium)
+ Text(subtitle, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
+ if (trailingContent != null) trailingContent() else Icon(Icons.AutoMirrored.Rounded.ArrowForwardIos, null, modifier = Modifier.size(16.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f))
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/ui/components/Dialogs.kt b/app/src/main/java/com/d4viddf/hyperbridge/ui/components/Dialogs.kt
index 3cf7bdd..18db4de 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/ui/components/Dialogs.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/ui/components/Dialogs.kt
@@ -26,6 +26,7 @@ import androidx.compose.material.icons.filled.Block
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material.icons.filled.Palette
+import androidx.compose.material.icons.outlined.Info
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
@@ -72,10 +73,17 @@ fun AppConfigBottomSheet(
onDismiss: () -> Unit,
onNavConfigClick: () -> Unit
) {
- // Data Loading
- val typeConfig by viewModel.getAppConfig(app.packageName).collectAsState(initial = emptySet())
+ // --- UPDATED DATA LOADING ---
+ val effectiveConfig by viewModel.getEffectiveAppConfigFlow(app.packageName).collectAsState(initial = null)
+ val activeTypes = effectiveConfig?.activeTypes ?: emptySet()
+ val isManagedByTheme = effectiveConfig?.isManagedByTheme == true
+
val appIslandConfig by viewModel.getAppIslandConfig(app.packageName).collectAsState(initial = IslandConfig())
- val globalConfig by viewModel.globalConfigFlow.collectAsState(initial = IslandConfig(true, true, 5))
+ val globalConfig by viewModel.globalConfigFlow.collectAsState(initial = IslandConfig(
+ isFloat = true,
+ isShowShade = true,
+ timeout = 5
+ ))
val blockedTerms by viewModel.getAppBlockedTerms(app.packageName).collectAsState(initial = emptySet())
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
@@ -131,9 +139,36 @@ fun AppConfigBottomSheet(
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.2f))
Spacer(modifier = Modifier.height(24.dp))
+ // --- NEW: THEME OVERRIDE WARNING ---
+ if (isManagedByTheme) {
+ Surface(
+ color = MaterialTheme.colorScheme.primaryContainer,
+ shape = RoundedCornerShape(8.dp),
+ modifier = Modifier.padding(bottom = 16.dp).fillMaxWidth()
+ ) {
+ Row(
+ modifier = Modifier.padding(horizontal = 12.dp, vertical = 12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ Icons.Outlined.Info,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onPrimaryContainer,
+ modifier = Modifier.size(20.dp)
+ )
+ Spacer(Modifier.width(12.dp))
+ Text(
+ text = "Behaviors are currently managed by your active Theme.",
+ color = MaterialTheme.colorScheme.onPrimaryContainer,
+ style = MaterialTheme.typography.bodySmall
+ )
+ }
+ }
+ }
+
// --- CARD 1: NOTIFICATION TYPES (New Dropdown) ---
- // Count how many are active to show in subtitle
- val activeCount = NotificationType.entries.toTypedArray().count { typeConfig.contains(it.name) }
+ // Count how many are active from the effective config
+ val activeCount = NotificationType.entries.toTypedArray().count { activeTypes.contains(it.name) }
val activeSubtitle = stringResource(R.string.active_notifications_subtitle, activeCount)
ExpandableSettingCard(
@@ -143,7 +178,7 @@ fun AppConfigBottomSheet(
) {
NotificationTypesContent(
app = app,
- typeConfig = typeConfig,
+ activeTypes = activeTypes,
viewModel = viewModel,
onNavConfigClick = { onDismiss(); onNavConfigClick() },
navEditDesc = navEditDesc
@@ -210,14 +245,14 @@ fun AppConfigBottomSheet(
@Composable
fun NotificationTypesContent(
app: AppInfo,
- typeConfig: Set,
+ activeTypes: Set,
viewModel: AppListViewModel,
onNavConfigClick: () -> Unit,
navEditDesc: String
) {
Column {
NotificationType.entries.forEach { type ->
- val isChecked = typeConfig.contains(type.name)
+ val isChecked = activeTypes.contains(type.name)
val typeLabel = stringResource(type.labelRes)
val switchDesc = if (isChecked) stringResource(R.string.cd_disable_type, typeLabel)
else stringResource(R.string.cd_enable_type, typeLabel)
@@ -225,6 +260,7 @@ fun NotificationTypesContent(
Row(
modifier = Modifier
.fillMaxWidth()
+ // viewModel.updateAppConfig perfectly routes to Theme or Database depending on state!
.clickable { viewModel.updateAppConfig(app.packageName, type, !isChecked) }
.padding(vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/ui/components/EmptyState.kt b/app/src/main/java/com/d4viddf/hyperbridge/ui/components/EmptyState.kt
index 553c550..d7b4d99 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/ui/components/EmptyState.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/ui/components/EmptyState.kt
@@ -28,10 +28,10 @@ import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun EmptyState(
+ modifier: Modifier = Modifier,
title: String,
description: String,
- icon: ImageVector = Icons.Outlined.NotificationsOff,
- modifier: Modifier = Modifier
+ icon: ImageVector = Icons.Outlined.NotificationsOff
) {
Column(
modifier = modifier
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/ui/components/EnginePreview.kt b/app/src/main/java/com/d4viddf/hyperbridge/ui/components/EnginePreview.kt
new file mode 100644
index 0000000..519f543
--- /dev/null
+++ b/app/src/main/java/com/d4viddf/hyperbridge/ui/components/EnginePreview.kt
@@ -0,0 +1,292 @@
+package com.d4viddf.hyperbridge.ui.components
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.EaseInOutQuart
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.animation.core.spring
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.BatteryFull
+import androidx.compose.material.icons.filled.Call
+import androidx.compose.material.icons.filled.CallEnd
+import androidx.compose.material.icons.filled.Group
+import androidx.compose.material.icons.filled.SignalCellular4Bar
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.RadioButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import kotlinx.coroutines.delay
+
+@Composable
+fun EnginePreview(isNative: Boolean) {
+ var isExpanded by remember { mutableStateOf(false) }
+
+ LaunchedEffect(isNative) {
+ while(true) {
+ isExpanded = false
+ delay(2500)
+ isExpanded = true
+ delay(4500)
+ }
+ }
+
+ val topPadding by animateDpAsState(
+ targetValue = if (isExpanded) 42.dp else 10.dp,
+ animationSpec = tween(500, easing = EaseInOutQuart),
+ label = "yOffset"
+ )
+
+ val height by animateDpAsState(
+ targetValue = if (isExpanded && isNative ) 130.dp else if (isExpanded && !isNative) 80.dp else 26.dp,
+ animationSpec = spring(0.8f, Spring.StiffnessLow),
+ label = "height"
+ )
+
+ val width by animateDpAsState(
+ targetValue = if (isExpanded) 340.dp else if (isNative) 120.dp else 200.dp,
+ animationSpec = spring(0.8f, Spring.StiffnessLow),
+ label = "width"
+ )
+
+ val containerColor by animateColorAsState(
+ if (isNative && isExpanded) Color(0xFF222222) else Color.Black,
+ label = "color"
+ )
+
+ Card(
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerHigh),
+ shape = RoundedCornerShape(28.dp),
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(200.dp)
+ ) {
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter) {
+
+ // Mock Status Bar
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 24.dp, vertical = 14.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text("12:00", style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.Bold)
+ Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
+ Icon(Icons.Default.SignalCellular4Bar, null, modifier = Modifier.size(14.dp))
+ Icon(Icons.Default.BatteryFull, null, modifier = Modifier.size(14.dp))
+ }
+ }
+
+ // The Pill
+ Box(
+ modifier = Modifier
+ .padding(top = topPadding)
+ .width(width)
+ .height(height)
+ .clip(RoundedCornerShape(if (isExpanded) 24.dp else 50.dp))
+ .background(containerColor),
+ contentAlignment = Alignment.Center
+ ) {
+ if (!isExpanded) {
+ // COLLAPSED MOCKUP
+ Row(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = 12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(Icons.Default.Group, null, tint = Color.White, modifier = Modifier.size(14.dp))
+ Spacer(Modifier.width(8.dp))
+ Text(
+ text = if (isNative) "" else "Alice",
+ color = Color.White,
+ fontSize = 10.sp,
+ fontWeight = FontWeight.Bold
+ )
+ Spacer(Modifier.weight(1f))
+ // Camera cutout mock
+ Box(Modifier
+ .size(14.dp)
+ .background(Color(0xFF1F1F1F), CircleShape))
+ Spacer(Modifier.weight(1f))
+ Text(
+ text = if (isNative) "Alice" else "Incoming",
+ color = Color.White,
+ fontSize = 10.sp
+ )
+ }
+ } else {
+ // EXPANDED MOCKUP
+ if (isNative) {
+ // Live Update Design
+ Column(Modifier
+ .fillMaxSize()
+ .padding(16.dp)) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Box(Modifier
+ .size(16.dp)
+ .background(Color(0xFF3DDA82), CircleShape), contentAlignment = Alignment.Center) {
+ Icon(Icons.Default.Call, null, tint = Color.Black, modifier = Modifier.size(12.dp))
+ }
+ Spacer(Modifier.width(8.dp))
+ Text("Hyper Bridge • now", color = Color.White.copy(0.6f), fontSize = 10.sp)
+ }
+ Spacer(Modifier.height(4.dp))
+ Column(Modifier.padding(start= 24.dp)) {
+ Text(
+ "Alice",
+ color = Color.White,
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Bold
+ )
+ Text(
+ "Incoming Call",
+ color = Color.White.copy(0.7f),
+ fontSize = 12.sp
+ )
+
+ Spacer(Modifier.height(4.dp))
+
+ Row(horizontalArrangement = Arrangement.spacedBy(14.dp)) {
+ Text(
+ "Reject",
+ color = Color(0xFF3DDA82),
+ fontSize = 12.sp,
+ fontWeight = FontWeight.Bold
+ )
+ Text(
+ "Answer",
+ color = Color(0xFF3DDA82),
+ fontSize = 12.sp,
+ fontWeight = FontWeight.Bold
+ )
+ }
+ }
+ }
+ } else {
+ // Xiaomi Featured Design
+ Row(Modifier
+ .fillMaxSize()
+ .padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically) {
+ Icon(Icons.Default.Group, null, tint = Color.White, modifier = Modifier.size(32.dp))
+ Spacer(Modifier.width(8.dp))
+ Column(Modifier.weight(1f)) {
+ Text("Alice", color = Color.White, fontSize = 18.sp, fontWeight = FontWeight.Bold)
+ Text("Incoming Call", color = Color.White.copy(0.7f), fontSize = 12.sp)
+
+ }
+ Box(Modifier
+ .size(46.dp)
+ .background(Color(0xFFFF3B30), CircleShape) , contentAlignment = Alignment.Center)
+ {
+ Icon(Icons.Default.CallEnd, null, tint = Color.White, modifier = Modifier.size(32.dp))
+ }
+ Spacer(Modifier.width(16.dp))
+ Box(Modifier
+ .size(46.dp)
+ .background(Color(0xFF34C759), CircleShape), contentAlignment = Alignment.Center)
+ {
+ Icon(Icons.Default.Call, null, tint = Color.White, modifier = Modifier.size(32.dp))
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun EngineOptionCard(title: String, description: String, isSelected: Boolean, onClick: () -> Unit) {
+ Card(
+ onClick = onClick,
+ shape = RoundedCornerShape(16.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = if (isSelected) MaterialTheme.colorScheme.primaryContainer.copy(0.3f) else MaterialTheme.colorScheme.surfaceContainerLow
+ ),
+ border = if (isSelected) androidx.compose.foundation.BorderStroke(2.dp, MaterialTheme.colorScheme.primary) else null,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Row(Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
+ RadioButton(selected = isSelected, onClick = null)
+ Spacer(Modifier.width(12.dp))
+ Column {
+ Text(title, fontWeight = FontWeight.Bold)
+ Text(description, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
+ }
+ }
+ }
+}
+
+
+@Preview(showBackground = true, name = "Xiaomi Featured (False)")
+@Composable
+fun PreviewXiaomiEngine() {
+ MaterialTheme {
+ Box(modifier = Modifier.padding(16.dp)) {
+ EnginePreview(isNative = false)
+ }
+ }
+}
+
+@Preview(showBackground = true, name = "Native Live Update (True)")
+@Composable
+fun PreviewNativeEngine() {
+ MaterialTheme {
+ Box(modifier = Modifier.padding(16.dp)) {
+ EnginePreview(isNative = true)
+ }
+ }
+}
+
+@Preview(showBackground = true, name = "Option Card Preview")
+@Composable
+fun PreviewOptionCard() {
+ MaterialTheme {
+ Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ EngineOptionCard(
+ title = "Selected Item",
+ description = "This is how it looks when selected",
+ isSelected = true,
+ onClick = {}
+ )
+ EngineOptionCard(
+ title = "Unselected Item",
+ description = "This is the default state",
+ isSelected = false,
+ onClick = {}
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/ui/components/HyperOSColorPicker.kt b/app/src/main/java/com/d4viddf/hyperbridge/ui/components/HyperOSColorPicker.kt
index a92f1ac..a25dee6 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/ui/components/HyperOSColorPicker.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/ui/components/HyperOSColorPicker.kt
@@ -1,14 +1,9 @@
package com.d4viddf.hyperbridge.ui.components
import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Slider
@@ -59,44 +54,3 @@ fun GradientSlider(
)
}
}
-
-// Reusable circular color bubble
-@Composable
-fun ColorBubble(
- color: Color,
- isSelected: Boolean,
- onClick: () -> Unit,
- backgroundBrush: Brush? = null,
- content: @Composable BoxScope.() -> Unit = {}
-) {
- Box(
- modifier = Modifier
- .size(56.dp)
- .clip(CircleShape)
- .clickable(onClick = onClick),
- contentAlignment = Alignment.Center
- ) {
- // Outer selection ring
- if (isSelected) {
- Box(
- modifier = Modifier
- .matchParentSize()
- .border(2.dp, Color(0xFF3B82F6), CircleShape) // HyperOS Blue ring
- )
- }
-
- // Inner color circle
- Box(
- modifier = Modifier
- .size(if (isSelected) 44.dp else 48.dp) // Shrinks slightly when selected
- .clip(CircleShape)
- .then(
- if (backgroundBrush != null) Modifier.background(backgroundBrush)
- else Modifier.background(color)
- ),
- contentAlignment = Alignment.Center,
- content = content
- )
- }
-}
-
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/ui/components/IslandSettingsControl.kt b/app/src/main/java/com/d4viddf/hyperbridge/ui/components/IslandSettingsControl.kt
index ac75b6f..0487250 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/ui/components/IslandSettingsControl.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/ui/components/IslandSettingsControl.kt
@@ -1,137 +1,312 @@
package com.d4viddf.hyperbridge.ui.components
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccessTime
+import androidx.compose.material.icons.filled.DeleteSweep
import androidx.compose.material.icons.filled.Layers
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.d4viddf.hyperbridge.R
import com.d4viddf.hyperbridge.models.IslandConfig
-import kotlin.math.roundToInt
+import com.d4viddf.hyperbridge.ui.theme.HyperBridgeTheme
+
+// Define our snap points (in seconds) for the auto-hide island
+val timeoutSteps = listOf(
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 20, 30, 45,
+ 60, 300, 900, 1800, 3600
+)
+private val timePopUpSteps = listOf(
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 20, 30
+)
@Composable
fun IslandSettingsControl(
config: IslandConfig,
- // If not null, represents Global Defaults to fall back on
defaultConfig: IslandConfig? = null,
onUpdate: (IslandConfig) -> Unit
) {
val isOverridden = config.isFloat != null
val displayConfig = if (isOverridden) config else (defaultConfig ?: config)
- Column {
- // 1. FLOAT TOGGLE
- SettingsSwitchRow(
- title = stringResource(R.string.setting_float),
- description = stringResource(R.string.setting_float_desc),
- icon = Icons.Default.Visibility,
- checked = displayConfig.isFloat ?: true,
- onCheckedChange = {
- onUpdate(config.copy(isFloat = it))
- }
+ // Timeout is "Enabled" if it's > 0
+ val currentTimeout = displayConfig.timeout ?: 10
+ val isTimeoutEnabled = currentTimeout > 0
+
+ Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
+
+ Text(
+ text = stringResource(R.string.global_behavior),
+ style = MaterialTheme.typography.labelLarge,
+ color = MaterialTheme.colorScheme.primary,
+ modifier = Modifier.padding(start = 4.dp, top = 8.dp)
)
+ Spacer(Modifier.height(8.dp))
+ // --- TIMEOUT CARD ---
+ Card(
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer),
+ shape = RoundedCornerShape(24.dp),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Column(modifier = Modifier.padding(horizontal = 20.dp, vertical = 16.dp)) {
+ // Header with Switch
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Icon(Icons.Default.AccessTime, null, tint = MaterialTheme.colorScheme.onSurfaceVariant)
+ Spacer(Modifier.width(20.dp))
+ Column(modifier = Modifier.weight(1f)) {
+ Text(stringResource(R.string.auto_hide_island), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Medium)
+ Text(
+ text = if (isTimeoutEnabled) stringResource(R.string.hides_after_a_set_time) else stringResource(
+ R.string.behavior_hide_desc
+ ),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ Switch(
+ checked = isTimeoutEnabled,
+ onCheckedChange = { enabled ->
+ // When changing this, we also ensure isFloat is set to track the override
+ val newTimeout = if (enabled) 5 else 0
+ val currentIsFloat = config.isFloat ?: defaultConfig?.isFloat ?: true
+ onUpdate(config.copy(timeout = newTimeout, isFloat = currentIsFloat))
+ }
+ )
+ }
+
+ // Expandable Slider Section
+ AnimatedVisibility(
+ visible = isTimeoutEnabled,
+ enter = expandVertically(),
+ exit = shrinkVertically()
+ ) {
+ Column {
+ Spacer(Modifier.height(16.dp))
+
+ // Time Display Label
+ Text(
+ text = formatSeconds(currentTimeout),
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.primary,
+ fontWeight = FontWeight.Bold
+ )
+
+ // Slider mapping to our steps list
+ val currentIndex = timeoutSteps.indexOf(currentTimeout).coerceAtLeast(0).toFloat()
+
+ Slider(
+ value = currentIndex,
+ onValueChange = { index ->
+ val selectedSeconds = timeoutSteps[index.toInt()]
+ val currentIsFloat = config.isFloat ?: defaultConfig?.isFloat ?: true
+ onUpdate(config.copy(timeout = selectedSeconds, isFloat = currentIsFloat))
+ },
+ valueRange = 0f..(timeoutSteps.size - 1).toFloat(),
+ steps = timeoutSteps.size - 2
+ )
+
+ Text(
+ text = stringResource(R.string.behavior_desc_hide_long),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
+ modifier = Modifier.padding(top = 4.dp)
+ )
+ }
+ }
+ }
+ }
+
+ Spacer(Modifier.height(14.dp))
- HorizontalDivider(
- modifier = Modifier.padding(vertical = 8.dp).padding(start = 56.dp),
- color = MaterialTheme.colorScheme.outlineVariant.copy(0.2f)
+ Text(
+ text = stringResource(R.string.xiaomi_featured_notifications),
+ style = MaterialTheme.typography.labelLarge,
+ color = MaterialTheme.colorScheme.primary,
+ modifier = Modifier.padding(start = 4.dp)
)
- // 2. SHADE TOGGLE
- SettingsSwitchRow(
+ Spacer(Modifier.height(8.dp))
+
+ // --- FLOAT SETTINGS (Heads-up Popup) ---
+ val isFloatEnabled = displayConfig.isFloat ?: true
+ val currentFloatTimeout = displayConfig.floatTimeout ?: 10
+
+ Card(
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer),
+ shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp, bottomStart = 4.dp, bottomEnd = 4.dp),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Column(modifier = Modifier.padding(horizontal = 20.dp, vertical = 16.dp)) {
+ Row(verticalAlignment = Alignment.Top) {
+ Icon(Icons.Default.Visibility, null, tint = MaterialTheme.colorScheme.onSurfaceVariant)
+ Spacer(Modifier.width(20.dp))
+ Column(modifier = Modifier.weight(1f)) {
+ Text(stringResource(R.string.setting_float), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Medium)
+ Text(stringResource(R.string.setting_float_desc), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
+ }
+ Switch(
+ checked = isFloatEnabled,
+ onCheckedChange = { onUpdate(config.copy(isFloat = it)) }
+ )
+ }
+
+ AnimatedVisibility(
+ visible = isFloatEnabled,
+ enter = expandVertically(),
+ exit = shrinkVertically()
+ ) {
+ Column {
+ Spacer(Modifier.height(16.dp))
+ Text(
+ text = stringResource(R.string.seconds_suffix, currentFloatTimeout),
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.primary,
+ fontWeight = FontWeight.Bold
+ )
+
+
+ // Slider mapping to our steps list
+ val currentIndexPop = timePopUpSteps.indexOf(currentFloatTimeout).coerceAtLeast(1).toFloat()
+
+ Slider(
+ value = currentIndexPop,
+ onValueChange = { index ->
+ val selectedSeconds = timePopUpSteps[index.toInt()]
+ onUpdate(config.copy(floatTimeout = selectedSeconds))
+ },
+ valueRange = 0f..(timePopUpSteps.size - 1).toFloat(),
+ steps = timePopUpSteps.size - 2,
+
+ )
+ Text(
+ text = stringResource(R.string.setting_float_timeout_desc),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
+ )
+ }
+ }
+ }
+ }
+
+ SettingsToggleCard(
title = stringResource(R.string.setting_shade),
- description = stringResource(R.string.setting_shade_desc),
+ subtitle = stringResource(R.string.setting_shade_desc),
icon = Icons.Default.Layers,
checked = displayConfig.isShowShade ?: true,
- onCheckedChange = {
- onUpdate(config.copy(isShowShade = it))
- }
+ onCheckedChange = {
+ val currentIsFloat = config.isFloat ?: defaultConfig?.isFloat ?: true
+ onUpdate(config.copy(isShowShade = it, isFloat = currentIsFloat))
+ },
+ shape = RoundedCornerShape(topStart = 4.dp, topEnd = 4.dp, bottomStart = 24.dp, bottomEnd = 24.dp)
)
- HorizontalDivider(
- modifier = Modifier.padding(vertical = 8.dp).padding(start = 56.dp),
- color = MaterialTheme.colorScheme.outlineVariant.copy(0.2f)
+ Spacer(Modifier.height(14.dp))
+
+ Text(
+ text = stringResource(R.string.live_updates),
+ style = MaterialTheme.typography.labelLarge,
+ color = MaterialTheme.colorScheme.primary,
+ modifier = Modifier.padding(start = 4.dp)
)
- // 3. TIMEOUT SLIDER
- // FIX: Treat values as SECONDS directly (Default 5s)
- val timeoutSec = (displayConfig.timeout ?: 5L).toFloat()
-
- Column(modifier = Modifier.padding(vertical = 8.dp)) {
- // Header Row
- Row(verticalAlignment = Alignment.CenterVertically) {
- Icon(
- Icons.Default.AccessTime,
- null,
- tint = MaterialTheme.colorScheme.primary,
- modifier = Modifier.size(24.dp)
- )
- Spacer(Modifier.width(16.dp))
-
- Text(
- text = stringResource(R.string.setting_timeout),
- style = MaterialTheme.typography.titleMedium,
- fontWeight = FontWeight.SemiBold,
- modifier = Modifier.weight(1f)
- )
-
- // Value Display (e.g. "5s")
- Text(
- text = stringResource(R.string.seconds_suffix, timeoutSec.roundToInt()),
- style = MaterialTheme.typography.titleMedium,
- color = MaterialTheme.colorScheme.primary,
- fontWeight = FontWeight.Bold
- )
- }
+ Spacer(Modifier.height(8.dp))
- // Full Width Slider
- Slider(
- value = timeoutSec,
- onValueChange = { seconds ->
- // FIX: Pass seconds directly as Long (No * 1000 conversion)
- onUpdate(config.copy(timeout = seconds.toInt()))
- },
- valueRange = 0f..10f,
- steps = 9,
- modifier = Modifier
- .fillMaxWidth()
- .padding(top = 4.dp, start = 40.dp)
- )
- }
+ SettingsToggleCard(
+ title = stringResource(R.string.remove_original_notification),
+ subtitle = stringResource(R.string.remove_original_notification_desc),
+ icon = Icons.Default.DeleteSweep,
+ checked = displayConfig.removeOriginalNotification ?: false,
+ onCheckedChange = {
+ val currentIsFloat = config.isFloat ?: defaultConfig?.isFloat ?: true
+ onUpdate(config.copy(removeOriginalNotification = it, isFloat = currentIsFloat))
+ },
+ shape = RoundedCornerShape(24.dp)
+ )
+ }
+}
+
+/**
+ * Formats seconds into a readable string (e.g. "10s", "5m", "1h")
+ */
+fun formatSeconds(seconds: Int): String {
+ return when {
+ seconds < 60 -> "${seconds}s"
+ seconds < 3600 -> "${seconds / 60}m"
+ else -> "${seconds / 3600}h"
}
}
@Composable
-fun SettingsSwitchRow(
- title: String, description: String, icon: ImageVector,
- checked: Boolean, onCheckedChange: (Boolean) -> Unit
+fun SettingsToggleCard(
+ title: String, subtitle: String, icon: ImageVector,
+ checked: Boolean, shape: Shape, onCheckedChange: (Boolean) -> Unit
) {
- Row(
- modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
- verticalAlignment = Alignment.CenterVertically
+ Card(
+ onClick = { onCheckedChange(!checked) },
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer),
+ shape = shape,
+ modifier = Modifier.fillMaxWidth()
) {
- Icon(icon, null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(24.dp))
- Spacer(Modifier.width(16.dp))
- Column(modifier = Modifier.weight(1f)) {
- Text(title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
- Text(description, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
+ Row(
+ modifier = Modifier.padding(horizontal = 20.dp, vertical = 16.dp),
+ verticalAlignment = Alignment.Top
+ ) {
+ Icon(icon, null, tint = MaterialTheme.colorScheme.onSurfaceVariant)
+ Spacer(Modifier.width(20.dp))
+ Column(modifier = Modifier.weight(1f)) {
+ Text(title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Medium)
+ Text(subtitle, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
+ }
+ Switch(checked = checked, onCheckedChange = onCheckedChange)
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun IslandSettingsControlPreview() {
+ HyperBridgeTheme {
+ Surface(modifier = Modifier.fillMaxSize()) {
+ IslandSettingsControl(
+ config = IslandConfig(
+ isFloat = true,
+ timeout = 5,
+ floatTimeout = 5,
+ isShowShade = true,
+ removeOriginalNotification = false
+ ),
+ onUpdate = {}
+ )
}
- Switch(
- checked = checked,
- onCheckedChange = onCheckedChange,
- colors = SwitchDefaults.colors(
- checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
- checkedTrackColor = MaterialTheme.colorScheme.primary,
- uncheckedThumbColor = MaterialTheme.colorScheme.outline,
- uncheckedTrackColor = MaterialTheme.colorScheme.surfaceContainerHighest
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun SettingsToggleCardPreview() {
+ HyperBridgeTheme {
+ Surface(modifier = Modifier.padding(16.dp)) {
+ SettingsToggleCard(
+ title = "Example Title",
+ subtitle = "Example subtitle for the toggle card",
+ icon = Icons.Default.Layers,
+ checked = true,
+ shape = RoundedCornerShape(24.dp),
+ onCheckedChange = {}
)
- )
+ }
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/ui/components/NavPreviewContent.kt b/app/src/main/java/com/d4viddf/hyperbridge/ui/components/NavPreviewContent.kt
new file mode 100644
index 0000000..56b029b
--- /dev/null
+++ b/app/src/main/java/com/d4viddf/hyperbridge/ui/components/NavPreviewContent.kt
@@ -0,0 +1,215 @@
+package com.d4viddf.hyperbridge.ui.components
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.TurnRight
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExposedDropdownMenuAnchorType
+import androidx.compose.material3.ExposedDropdownMenuBox
+import androidx.compose.material3.ExposedDropdownMenuDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.d4viddf.hyperbridge.R
+import com.d4viddf.hyperbridge.models.NavContent
+
+
+@Composable
+fun NavPreview(left: NavContent, right: NavContent) {
+ val leftLabel = stringResource(getNavContentLabelRes(left))
+ val rightLabel = stringResource(getNavContentLabelRes(right))
+ val cd = stringResource(R.string.cd_nav_preview, leftLabel, rightLabel)
+
+ Card(
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerHigh),
+ shape = RoundedCornerShape(24.dp),
+ modifier = Modifier
+ .fillMaxWidth()
+ .semantics(mergeDescendants = true) { contentDescription = cd }
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 32.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Box(
+ modifier = Modifier
+ .width(330.dp)
+ .height(46.dp)
+ .clip(RoundedCornerShape(50))
+ .background(Color.Black)
+ ) {
+ // Camera Cutout
+ Box(
+ modifier = Modifier
+ .align(Alignment.Center)
+ .size(30.dp)
+ .clip(CircleShape)
+ .background(Color(0xFF1F1F1F))
+ )
+
+ Row(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = 12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ // LEFT SIDE
+ Row(
+ modifier = Modifier.weight(1f),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ Icon(
+ imageVector = Icons.Default.TurnRight,
+ contentDescription = null,
+ modifier = Modifier.size(20.dp)
+ )
+ Spacer(Modifier.width(6.dp))
+ NavContentRenderer(left, Alignment.Start)
+ }
+
+ // SPACER
+ Spacer(modifier = Modifier.width(32.dp))
+
+ // RIGHT SIDE
+ Box(
+ modifier = Modifier.weight(1f),
+ contentAlignment = Alignment.CenterEnd
+ ) {
+ NavContentRenderer(right, Alignment.End)
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun NavContentRenderer(type: NavContent, align: Alignment.Horizontal) {
+
+ // Fade Logic: Only applies to long text (Instruction)
+ val fadeBrush = if (type == NavContent.INSTRUCTION) {
+ if (align == Alignment.Start) {
+ Brush.horizontalGradient(0.85f to Color.White, 1.0f to Color.Transparent)
+ } else {
+ Brush.horizontalGradient(0.0f to Color.Transparent, 0.15f to Color.White)
+ }
+ } else null
+
+ val textStyle = if (fadeBrush != null) {
+ TextStyle(brush = fadeBrush, fontWeight = FontWeight.Bold, fontSize = 14.sp)
+ } else {
+ TextStyle(color = Color.White, fontWeight = FontWeight.Bold, fontSize = 14.sp)
+ }
+
+ val timeStyle = TextStyle(color = Color.White, fontWeight = FontWeight.Normal, fontSize = 14.sp)
+
+ when (type) {
+ NavContent.INSTRUCTION -> {
+ Text(
+ text = stringResource(R.string.nav_preview_instruction),
+ style = textStyle,
+ maxLines = 1,
+ overflow = TextOverflow.Clip
+ )
+ }
+ NavContent.DISTANCE -> {
+ Text(
+ text = stringResource(R.string.nav_preview_distance),
+ style = textStyle // Solid White
+ )
+ }
+ NavContent.ETA -> {
+ Text(
+ text = stringResource(R.string.nav_preview_time),
+ style = timeStyle // Standard weight
+ )
+ }
+ NavContent.DISTANCE_ETA -> {
+ // Combined Row
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text(stringResource(R.string.nav_preview_distance), style = textStyle.copy(fontSize = 13.sp))
+ Spacer(Modifier.width(4.dp))
+ Text("•", color = Color.Gray, fontSize = 12.sp)
+ Spacer(Modifier.width(4.dp))
+ Text(stringResource(R.string.nav_preview_time), style = timeStyle.copy(fontSize = 13.sp))
+ }
+ }
+ NavContent.NONE -> { /* Empty */ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun NavDropdown(label: String, selected: NavContent, onSelect: (NavContent) -> Unit) {
+ var expanded by remember { mutableStateOf(false) }
+ Column {
+ Text(label, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.primary)
+ Spacer(Modifier.height(4.dp))
+ ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = { expanded = it }) {
+ OutlinedTextField(
+ value = stringResource(getNavContentLabelRes(selected)),
+ onValueChange = {},
+ readOnly = true,
+ trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
+ modifier = Modifier.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable, true).fillMaxWidth(),
+ textStyle = MaterialTheme.typography.bodyMedium
+ )
+ ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
+ NavContent.entries.forEach { option ->
+ DropdownMenuItem(
+ text = { Text(stringResource(getNavContentLabelRes(option))) },
+ onClick = { onSelect(option); expanded = false }
+ )
+ }
+ }
+ }
+ }
+}
+
+private fun getNavContentLabelRes(content: NavContent): Int {
+ return when(content) {
+ NavContent.INSTRUCTION -> R.string.nav_content_instruction
+ NavContent.DISTANCE -> R.string.nav_content_distance
+ NavContent.ETA -> R.string.nav_content_eta
+ NavContent.DISTANCE_ETA -> R.string.nav_content_distance_eta
+ NavContent.NONE -> R.string.nav_content_none
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/ui/components/PermanentIslandPreview.kt b/app/src/main/java/com/d4viddf/hyperbridge/ui/components/PermanentIslandPreview.kt
new file mode 100644
index 0000000..67d37bd
--- /dev/null
+++ b/app/src/main/java/com/d4viddf/hyperbridge/ui/components/PermanentIslandPreview.kt
@@ -0,0 +1,89 @@
+package com.d4viddf.hyperbridge.ui.components
+
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.animation.core.spring
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.BatteryFull
+import androidx.compose.material.icons.filled.SignalCellular4Bar
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun PermanentIslandPreview(islandWidthValue: Int) {
+ // Convert 0-20 value to dp width. Base width is maybe 120dp. Each step adds 5dp.
+ val targetWidth = 120.dp + (islandWidthValue * 5).dp
+
+ val width by animateDpAsState(
+ targetValue = targetWidth,
+ animationSpec = spring(0.8f, Spring.StiffnessLow),
+ label = "width"
+ )
+
+ Card(
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerHigh),
+ shape = RoundedCornerShape(28.dp),
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(160.dp)
+ ) {
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter) {
+
+ // Mock Status Bar
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 24.dp, vertical = 14.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text("12:00", style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.Bold)
+ Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
+ Icon(Icons.Default.SignalCellular4Bar, null, modifier = Modifier.size(14.dp))
+ Icon(Icons.Default.BatteryFull, null, modifier = Modifier.size(14.dp))
+ }
+ }
+
+ // The Pill
+ Box(
+ modifier = Modifier
+ .padding(top = 10.dp)
+ .width(width)
+ .height(26.dp)
+ .clip(RoundedCornerShape(50.dp))
+ .background(Color.Black),
+ contentAlignment = Alignment.Center
+ ) {
+ // Empty island, just the camera cutout mock
+ Box(
+ Modifier
+ .size(14.dp)
+ .background(Color(0xFF1F1F1F), CircleShape)
+ )
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/ui/components/PriorityEducationDialog.kt b/app/src/main/java/com/d4viddf/hyperbridge/ui/components/PriorityEducationDialog.kt
deleted file mode 100644
index 7d68813..0000000
--- a/app/src/main/java/com/d4viddf/hyperbridge/ui/components/PriorityEducationDialog.kt
+++ /dev/null
@@ -1,62 +0,0 @@
-package com.d4viddf.hyperbridge.ui.components
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.*
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.dp
-import com.d4viddf.hyperbridge.R
-import com.d4viddf.hyperbridge.models.IslandLimitMode
-
-@Composable
-fun PriorityEducationDialog(
- onDismiss: () -> Unit,
- onConfigure: () -> Unit
-) {
- AlertDialog(
- onDismissRequest = onDismiss,
- title = {
- Text(stringResource(R.string.priority_edu_title), fontWeight = FontWeight.Bold)
- },
- text = {
- Column(horizontalAlignment = Alignment.CenterHorizontally) {
- // Show the animation for the DEFAULT mode (Most Recent)
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .height(100.dp)
- .background(MaterialTheme.colorScheme.surfaceContainer, RoundedCornerShape(12.dp)),
- contentAlignment = Alignment.Center
- ) {
- BehaviorVisualizer(mode = IslandLimitMode.MOST_RECENT)
- }
-
- Spacer(modifier = Modifier.height(16.dp))
-
- // We manually parse bold tags since AlertDialog text doesn't support HTML natively in simple Text
- // For simplicity, we just show the raw string, or you can split it if you want styling.
- // Here we assume the string resource has text that looks good plain.
- Text(
- text = stringResource(R.string.priority_edu_desc)
- .replace("", "").replace("", ""), // Strip tags
- style = MaterialTheme.typography.bodyMedium
- )
- }
- },
- confirmButton = {
- Button(onClick = onConfigure) {
- Text(stringResource(R.string.configure_now))
- }
- },
- dismissButton = {
- TextButton(onClick = onDismiss) {
- Text(stringResource(R.string.keep_default))
- }
- }
- )
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/ui/navigation/NavGraph.kt b/app/src/main/java/com/d4viddf/hyperbridge/ui/navigation/NavGraph.kt
new file mode 100644
index 0000000..f3243b9
--- /dev/null
+++ b/app/src/main/java/com/d4viddf/hyperbridge/ui/navigation/NavGraph.kt
@@ -0,0 +1,150 @@
+package com.d4viddf.hyperbridge.ui.navigation
+
+import android.content.Context
+import android.widget.Toast
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import androidx.navigation3.runtime.entryProvider
+import com.d4viddf.hyperbridge.R
+import com.d4viddf.hyperbridge.data.AppPreferences
+import com.d4viddf.hyperbridge.ui.screens.home.HomeScreen
+import com.d4viddf.hyperbridge.ui.screens.onboarding.OnboardingScreen
+import com.d4viddf.hyperbridge.ui.screens.settings.AppPriorityScreen
+import com.d4viddf.hyperbridge.ui.screens.settings.BackupSettingsScreen
+import com.d4viddf.hyperbridge.ui.screens.settings.BlocklistAppListScreen
+import com.d4viddf.hyperbridge.ui.screens.settings.ChangelogHistoryScreen
+import com.d4viddf.hyperbridge.ui.screens.settings.EngineSettingsScreen
+import com.d4viddf.hyperbridge.ui.screens.settings.GlobalBlocklistScreen
+import com.d4viddf.hyperbridge.ui.screens.settings.GlobalSettingsScreen
+import com.d4viddf.hyperbridge.ui.screens.settings.ImportPreviewScreen
+import com.d4viddf.hyperbridge.ui.screens.settings.InfoScreen
+import com.d4viddf.hyperbridge.ui.screens.settings.IslandSettingsScreen
+import com.d4viddf.hyperbridge.ui.screens.settings.LicensesScreen
+import com.d4viddf.hyperbridge.ui.screens.settings.NavCustomizationScreen
+import com.d4viddf.hyperbridge.ui.screens.settings.PrioritySettingsScreen
+import com.d4viddf.hyperbridge.ui.screens.settings.SetupHealthScreen
+import com.d4viddf.hyperbridge.util.BackupManager
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+@Composable
+fun mainNavGraph(
+ context: Context,
+ scope: CoroutineScope,
+ preferences: AppPreferences,
+ navigator: Navigator,
+ backupManager: BackupManager,
+ currentVersionCode: Int,
+ onExit: () -> Unit
+) = entryProvider {
+ entry {
+ OnboardingScreen {
+ scope.launch {
+ preferences.setSetupComplete(true)
+ navigator.finishOnboarding(Screen.Home)
+ }
+ }
+ }
+ entry {
+ HomeScreen(
+ onSettingsClick = { navigator.navigate(Screen.Info) },
+ onNavConfigClick = { pkg -> navigator.navigate(Screen.NavCustomization(pkg)) }
+ )
+ }
+ entry {
+ InfoScreen(
+ onBack = { if (!navigator.goBack()) onExit() },
+ onSetupClick = { navigator.navigate(Screen.Setup) },
+ onLicensesClick = { navigator.navigate(Screen.Licenses) },
+ onBehaviorClick = { navigator.navigate(Screen.Behavior) },
+ onGlobalSettingsClick = { navigator.navigate(Screen.GlobalSettings) },
+ onHistoryClick = { navigator.navigate(Screen.History) },
+ onBlocklistClick = { navigator.navigate(Screen.GlobalBlocklist) },
+ onBackupClick = { navigator.navigate(Screen.Backup) }
+ )
+ }
+ entry {
+ GlobalSettingsScreen(
+ onBack = { navigator.goBack() },
+ onNavSettingsClick = { navigator.navigate(Screen.NavCustomization(null)) },
+ onIslandSettingsClick = { navigator.navigate(Screen.IslandSettings) },
+ onEngineSettingsClick = { navigator.navigate(Screen.EngineSettings) },
+ onDndSettingsClick = { navigator.navigate(Screen.DndSettings) },
+ onPermanentIslandClick = { navigator.navigate(Screen.PermanentIslandConfig) }
+ )
+ }
+ entry {
+ com.d4viddf.hyperbridge.ui.screens.settings.DndSettingsScreen(onBack = { navigator.goBack() })
+ }
+ entry {
+ com.d4viddf.hyperbridge.ui.screens.settings.PermanentIslandConfigScreen(onBack = { navigator.goBack() })
+ }
+ entry { key ->
+ NavCustomizationScreen(
+ onBack = { navigator.goBack() },
+ packageName = key.packageName
+ )
+ }
+ entry {
+ EngineSettingsScreen(onBack = { navigator.goBack() })
+ }
+ entry {
+ SetupHealthScreen(onBack = { navigator.goBack() })
+ }
+ entry {
+ LicensesScreen(onBack = { navigator.goBack() })
+ }
+ entry {
+ PrioritySettingsScreen(
+ onBack = { navigator.goBack() },
+ onNavigateToPriorityList = { navigator.navigate(Screen.AppPriority) }
+ )
+ }
+ entry {
+ AppPriorityScreen(onBack = { navigator.goBack() })
+ }
+ entry {
+ ChangelogHistoryScreen(onBack = { navigator.goBack() })
+ }
+ entry {
+ GlobalBlocklistScreen(
+ onBack = { navigator.goBack() },
+ onNavigateToAppList = { navigator.navigate(Screen.BlocklistApps) }
+ )
+ }
+ entry {
+ BlocklistAppListScreen(onBack = { navigator.goBack() })
+ }
+ entry {
+ BackupSettingsScreen(
+ onBack = { navigator.goBack() },
+ backupManager = backupManager,
+ onBackupFileLoaded = { backup ->
+ navigator.navigate(Screen.ImportPreview(backup))
+ }
+ )
+ }
+ entry { key ->
+ val importSuccessMsg = stringResource(R.string.import_success)
+ val importFailedMsg = stringResource(R.string.import_failed)
+ ImportPreviewScreen(
+ backupData = key.backup,
+ onBack = { navigator.goBack() },
+ onConfirmRestore = { selection ->
+ scope.launch {
+ val result = backupManager.restoreBackup(key.backup, selection)
+ if (result.isSuccess) {
+ Toast.makeText(context, importSuccessMsg, Toast.LENGTH_LONG).show()
+ navigator.navigate(Screen.Home)
+ } else {
+ val error = result.exceptionOrNull()?.message ?: ""
+ Toast.makeText(context, importFailedMsg.format(error), Toast.LENGTH_LONG).show()
+ }
+ }
+ }
+ )
+ }
+ entry {
+ IslandSettingsScreen(onBack = { navigator.goBack() })
+ }
+}
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/ui/navigation/NavigationState.kt b/app/src/main/java/com/d4viddf/hyperbridge/ui/navigation/NavigationState.kt
new file mode 100644
index 0000000..affd7c2
--- /dev/null
+++ b/app/src/main/java/com/d4viddf/hyperbridge/ui/navigation/NavigationState.kt
@@ -0,0 +1,94 @@
+package com.d4viddf.hyperbridge.ui.navigation
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSerializable
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.SnapshotStateList
+import androidx.compose.runtime.toMutableStateList
+import androidx.navigation3.runtime.NavBackStack
+import androidx.navigation3.runtime.NavEntry
+import androidx.navigation3.runtime.NavKey
+import androidx.navigation3.runtime.rememberDecoratedNavEntries
+import androidx.navigation3.runtime.rememberNavBackStack
+import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
+import androidx.navigation3.runtime.serialization.NavKeySerializer
+import androidx.savedstate.compose.serialization.serializers.MutableStateSerializer
+
+/**
+ * Create a navigation state that persists config changes and process death.
+ */
+@Composable
+fun rememberNavigationState(
+ startRoute: T,
+ topLevelRoutes: Set
+): NavigationState {
+
+ val topLevelRoute = rememberSerializable(
+ startRoute, topLevelRoutes,
+ serializer = MutableStateSerializer(NavKeySerializer())
+ ) {
+ mutableStateOf(startRoute)
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ val backStacks = topLevelRoutes.associateWith { key ->
+ rememberNavBackStack(key) as NavBackStack
+ }
+
+ return remember(startRoute, topLevelRoutes) {
+ NavigationState(
+ startRoute = startRoute,
+ topLevelRoute = topLevelRoute,
+ backStacks = backStacks
+ )
+ }
+}
+
+/**
+ * State holder for navigation state.
+ *
+ * @param startRoute - the start route. The user will exit the app through this route.
+ * @param topLevelRoute - the current top level route
+ * @param backStacks - the back stacks for each top level route
+ */
+class NavigationState(
+ val startRoute: T,
+ topLevelRoute: MutableState,
+ val backStacks: Map>
+) {
+ var topLevelRoute: T by topLevelRoute
+ val stacksInUse: List
+ get() = if (topLevelRoute == startRoute) {
+ listOf(startRoute)
+ } else {
+ listOf(startRoute, topLevelRoute)
+ }
+}
+
+/**
+ * Convert NavigationState into NavEntries.
+ */
+@Composable
+fun NavigationState.toEntries(
+ entryProvider: (T) -> NavEntry
+): SnapshotStateList> {
+
+ val decoratedEntries = backStacks.mapValues { (_, stack) ->
+ val decorators = listOf(
+ rememberSaveableStateHolderNavEntryDecorator(),
+ )
+ rememberDecoratedNavEntries(
+ backStack = stack,
+ entryDecorators = decorators,
+ entryProvider = entryProvider
+ )
+ }
+
+ return stacksInUse
+ .flatMap { decoratedEntries[it] ?: emptyList() }
+ .toMutableStateList()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/ui/navigation/Navigator.kt b/app/src/main/java/com/d4viddf/hyperbridge/ui/navigation/Navigator.kt
new file mode 100644
index 0000000..278c078
--- /dev/null
+++ b/app/src/main/java/com/d4viddf/hyperbridge/ui/navigation/Navigator.kt
@@ -0,0 +1,59 @@
+package com.d4viddf.hyperbridge.ui.navigation
+
+import androidx.navigation3.runtime.NavKey
+
+/**
+ * Handles navigation events (forward and back) by updating the navigation state.
+ */
+class Navigator(val state: NavigationState) {
+ fun navigate(route: T) {
+ if (route in state.backStacks.keys) {
+ // This is a top level route, just switch to it.
+ state.topLevelRoute = route
+ } else {
+ state.backStacks[state.topLevelRoute]?.add(route)
+ }
+ }
+
+ /**
+ * Returns true if the back event was handled, false if we're at the root of the start route.
+ */
+ fun goBack(): Boolean {
+ val currentStack = state.backStacks[state.topLevelRoute]
+ ?: error("Stack for ${state.topLevelRoute} not found")
+ val currentRoute = currentStack.lastOrNull() ?: return false
+
+ // If we're at the base of the current route, go back to the start route stack.
+ if (currentRoute == state.topLevelRoute) {
+ if (state.topLevelRoute != state.startRoute) {
+ state.topLevelRoute = state.startRoute
+ return true
+ } else {
+ return false
+ }
+ } else {
+ currentStack.removeLastOrNull()
+ return true
+ }
+ }
+
+ /**
+ * Specifically handles the transition from Onboarding to Home.
+ * Ensures the app doesn't close by adding the Home screen BEFORE clearing Onboarding.
+ */
+ fun finishOnboarding(homeRoute: T) {
+ val homeStack = state.backStacks[homeRoute]
+ ?: error("Home route $homeRoute not found in backStacks")
+
+
+ if (homeStack.isEmpty()) {
+ homeStack.add(homeRoute)
+ }
+
+ state.topLevelRoute = homeRoute
+
+ if (state.startRoute != homeRoute) {
+ state.backStacks[state.startRoute]?.clear()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/ui/navigation/Screen.kt b/app/src/main/java/com/d4viddf/hyperbridge/ui/navigation/Screen.kt
new file mode 100644
index 0000000..f711986
--- /dev/null
+++ b/app/src/main/java/com/d4viddf/hyperbridge/ui/navigation/Screen.kt
@@ -0,0 +1,27 @@
+package com.d4viddf.hyperbridge.ui.navigation
+
+import androidx.navigation3.runtime.NavKey
+import com.d4viddf.hyperbridge.data.model.HyperBridgeBackup
+import kotlinx.serialization.Serializable
+
+@Serializable
+sealed interface Screen : NavKey {
+ @Serializable data object Onboarding : Screen
+ @Serializable data object Home : Screen
+ @Serializable data object Info : Screen
+ @Serializable data object Setup : Screen
+ @Serializable data object Licenses : Screen
+ @Serializable data object Behavior : Screen
+ @Serializable data object GlobalSettings : Screen
+ @Serializable data object History : Screen
+ @Serializable data object Backup : Screen
+ @Serializable data class ImportPreview(val backup: HyperBridgeBackup) : Screen
+ @Serializable data class NavCustomization(val packageName: String?) : Screen
+ @Serializable data object EngineSettings : Screen
+ @Serializable data object AppPriority : Screen
+ @Serializable data object GlobalBlocklist : Screen
+ @Serializable data object BlocklistApps : Screen
+ @Serializable data object IslandSettings : Screen
+ @Serializable data object DndSettings : Screen
+ @Serializable data object PermanentIslandConfig : Screen
+}
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/design/DesignScreen.kt b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/design/DesignScreen.kt
index b076310..41334f9 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/design/DesignScreen.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/design/DesignScreen.kt
@@ -325,10 +325,14 @@ fun SectionHeader(title: String, onClick: () -> Unit) {
fun HeroSection() {
val uriHandler = LocalUriHandler.current
val items = listOf(
- HeroItem(stringResource(R.string.design_hero_customization_title), stringResource(R.string.design_hero_customization_subtitle), Color(0xFF4CAF50),{}),
- HeroItem(stringResource(R.string.design_hero_pro_title), stringResource(R.string.design_hero_pro_subtitle), Color(0xFF2196F3),{ uriHandler.openUri("https://github.com/D4vidDf/HyperBridge/discussions/78") }),
- HeroItem(stringResource(R.string.design_hero_community_title), stringResource(R.string.design_hero_community_subtitle), Color(0xFF9C27B0),
- { uriHandler.openUri("https://github.com/D4vidDf/HyperBridge/discussions") })
+ HeroItem(stringResource(R.string.design_hero_customization_title), stringResource(R.string.design_hero_customization_subtitle), Color(0xFF4CAF50)) {},
+ HeroItem(stringResource(R.string.design_hero_pro_title), stringResource(R.string.design_hero_pro_subtitle), Color(0xFF2196F3)) {
+ uriHandler.openUri(
+ "https://github.com/D4vidDf/HyperBridge/discussions/78"
+ )
+ },
+ HeroItem(stringResource(R.string.design_hero_community_title), stringResource(R.string.design_hero_community_subtitle), Color(0xFF9C27B0)
+ ) { uriHandler.openUri("https://github.com/D4vidDf/HyperBridge/discussions") }
)
val state = rememberCarouselState { items.size }
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/design/SavedAppWidgetsScreen.kt b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/design/SavedAppWidgetsScreen.kt
index e3160b5..0c1bf1e 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/design/SavedAppWidgetsScreen.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/design/SavedAppWidgetsScreen.kt
@@ -61,6 +61,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import androidx.core.content.edit
// --- DATA MODEL ---
data class SavedWidgetGroup(
@@ -92,7 +93,7 @@ fun SavedAppWidgetsScreen(
var tabIndex by remember { mutableIntStateOf(1) } // 0 = Favorites, 1 = All
var showPermissionReminder by remember { mutableStateOf(false) }
- val refreshTrigger = remember { mutableStateOf(0) }
+ val refreshTrigger = remember { mutableIntStateOf(0) }
val pullState = rememberPullToRefreshState()
val isRefreshing = isLoading && allGroups.isNotEmpty()
@@ -107,11 +108,11 @@ fun SavedAppWidgetsScreen(
if (lastVersion in 1 until currentVersion) {
showPermissionReminder = true
}
- sharedPrefs.edit().putInt("last_seen_version", currentVersion).apply()
+ sharedPrefs.edit { putInt("last_seen_version", currentVersion) }
}
// Fetch Saved Widgets (Reacts to new widgets being added dynamically)
- LaunchedEffect(savedIds, refreshTrigger.value) {
+ LaunchedEffect(savedIds, refreshTrigger.intValue) {
if (savedIds == null) return@LaunchedEffect
isLoading = true
@@ -127,11 +128,11 @@ fun SavedAppWidgetsScreen(
val appName = try {
val appInfo = context.packageManager.getApplicationInfo(pkg, 0)
context.packageManager.getApplicationLabel(appInfo).toString()
- } catch (e: Exception) { pkg }
+ } catch (_: Exception) { pkg }
val icon = try {
context.packageManager.getApplicationIcon(pkg)
- } catch (e: Exception) { null }
+ } catch (_: Exception) { null }
SavedWidgetGroup(pkg, appName, icon, ids)
}.sortedBy { it.appName }
@@ -318,7 +319,7 @@ fun SavedAppWidgetsScreen(
Box(modifier = Modifier.weight(1f).fillMaxWidth()) {
PullToRefreshBox(
isRefreshing = isRefreshing,
- onRefresh = { refreshTrigger.value++ },
+ onRefresh = { refreshTrigger.intValue++ },
state = pullState,
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.TopCenter,
@@ -368,7 +369,7 @@ fun SavedAppWidgetsScreen(
// 2. Remove from database and refresh UI
scope.launch {
preferences.removeWidgetId(widgetId)
- refreshTrigger.value++
+ refreshTrigger.intValue++
}
}
)
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/design/WidgetConfigScreen.kt b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/design/WidgetConfigScreen.kt
index 89252dc..6e55816 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/design/WidgetConfigScreen.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/design/WidgetConfigScreen.kt
@@ -91,6 +91,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
@@ -117,6 +118,7 @@ fun WidgetConfigScreen(
val scope = rememberCoroutineScope()
val appPreferences = remember { AppPreferences(context.applicationContext) }
val configuration = LocalConfiguration.current
+ val density = LocalDensity.current
val screenWidthDp = configuration.screenWidthDp
val previewWidthDp = screenWidthDp - 32
@@ -158,7 +160,7 @@ fun WidgetConfigScreen(
isShowShade = config.isShowShade
// Timeout Logic
- config.timeout?.let {
+ config.timeout.let {
if (it <= 0) {
isTimeoutEnabled = false
timeoutSeconds = 0
@@ -172,11 +174,14 @@ fun WidgetConfigScreen(
updateInterval = config.updateIntervalMinutes.toFloat()
}
+ val updatedToastText = stringResource(R.string.widget_updated_toast)
+ val settingsErrorText = stringResource(R.string.widget_settings_error)
+
val reconfigureLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
- Toast.makeText(context, context.getString(R.string.widget_updated_toast), Toast.LENGTH_SHORT).show()
+ Toast.makeText(context, updatedToastText, Toast.LENGTH_SHORT).show()
}
}
@@ -186,7 +191,7 @@ fun WidgetConfigScreen(
configureIntent.flags = 0
reconfigureLauncher.launch(configureIntent)
} catch (e: Exception) {
- Toast.makeText(context, context.getString(R.string.widget_settings_error), Toast.LENGTH_SHORT).show()
+ Toast.makeText(context, settingsErrorText, Toast.LENGTH_SHORT).show()
}
}
}
@@ -262,7 +267,7 @@ fun WidgetConfigScreen(
update = { wrapper ->
val hostView = wrapper.getChildAt(0)
if (hostView != null) {
- val density = context.resources.displayMetrics.density
+ val densityVal = density.density
val hDp = when (selectedSize) {
WidgetSize.SMALL -> 100
WidgetSize.MEDIUM -> 180
@@ -271,8 +276,8 @@ fun WidgetConfigScreen(
else -> 180
}
- val widthPx = (previewWidthDp * density).toInt()
- val heightPx = (hDp * density).toInt()
+ val widthPx = (previewWidthDp * densityVal).toInt()
+ val heightPx = (hDp * densityVal).toInt()
val widthSpec = View.MeasureSpec.makeMeasureSpec(widthPx, View.MeasureSpec.EXACTLY)
val heightSpec = View.MeasureSpec.makeMeasureSpec(heightPx, View.MeasureSpec.EXACTLY)
@@ -600,4 +605,4 @@ fun BehaviorSettings(
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/design/WidgetPickerScreen.kt b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/design/WidgetPickerScreen.kt
index a28d3af..21f7076 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/design/WidgetPickerScreen.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/design/WidgetPickerScreen.kt
@@ -116,7 +116,7 @@ fun WidgetPickerScreen(
var allGroups by remember { mutableStateOf>(emptyList()) }
var searchQuery by remember { mutableStateOf("") }
var tabIndex by remember { mutableIntStateOf(0) } // 0 = Recommended, 1 = All
- var pendingWidgetId by remember { mutableStateOf(-1) }
+ var pendingWidgetId by remember { mutableIntStateOf(-1) }
val bindLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
@@ -140,7 +140,7 @@ fun WidgetPickerScreen(
val appName = context.packageManager.getApplicationLabel(context.packageManager.getApplicationInfo(pkg, 0)).toString()
val icon = context.packageManager.getApplicationIcon(pkg)
WidgetAppGroup(pkg, appName, icon, list)
- } catch (e: Exception) { null }
+ } catch (_: Exception) { null }
}.sortedBy { it.appName }
allGroups = uiGroups
@@ -151,11 +151,7 @@ fun WidgetPickerScreen(
val filteredWidgets = if (tabIndex == 0) {
group.widgets.filter { w ->
// Filter specifically for 1x4 and 2x4 layouts (Compatible with Island sizes)
- if (android.os.Build.VERSION.SDK_INT >= 31) {
- w.targetCellWidth == 4 && (w.targetCellHeight == 1 || w.targetCellHeight == 2)
- } else {
- w.minWidth >= 200 && w.minHeight <= 150 // Rough estimation for older Android versions
- }
+ w.targetCellWidth == 4 && (w.targetCellHeight == 1 || w.targetCellHeight == 2)
}
} else group.widgets
@@ -408,17 +404,9 @@ fun WidgetChildItem(
val label = info.loadLabel(context.packageManager)
// Convert dp dimensions to Android Grid Proportions (e.g., 4x1, 2x2)
- val cols = if (android.os.Build.VERSION.SDK_INT >= 31) {
- info.targetCellWidth
- } else {
- maxOf(1, Math.ceil((info.minWidth + 30) / 70.0).toInt())
- }
+ val cols = info.targetCellWidth
- val rows = if (android.os.Build.VERSION.SDK_INT >= 31) {
- info.targetCellHeight
- } else {
- maxOf(1, Math.ceil((info.minHeight + 30) / 70.0).toInt())
- }
+ val rows = info.targetCellHeight
val dims = "$cols × $rows"
@@ -427,7 +415,7 @@ fun WidgetChildItem(
withContext(Dispatchers.IO) {
preview = try {
info.loadPreviewImage(context, 0) ?: info.loadIcon(context, 0)
- } catch (e: Exception) { null }
+ } catch (_: Exception) { null }
}
}
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/design/WidgetPickerViewModel.kt b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/design/WidgetPickerViewModel.kt
deleted file mode 100644
index 7109678..0000000
--- a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/design/WidgetPickerViewModel.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-package com.d4viddf.hyperbridge.ui.screens.design
-
-import android.appwidget.AppWidgetManager
-import android.content.Context
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-
-
-class WidgetPickerViewModel : ViewModel() {
-
- private val _widgetGroups = MutableStateFlow>(emptyList())
- val widgetGroups = _widgetGroups.asStateFlow()
-
- private val _isLoading = MutableStateFlow(true)
- val isLoading = _isLoading.asStateFlow()
-
- fun loadWidgets(context: Context) {
- viewModelScope.launch {
- _isLoading.value = true
- val groups = withContext(Dispatchers.IO) {
- val appWidgetManager = AppWidgetManager.getInstance(context)
- val packageManager = context.packageManager
-
- // 1. Get all providers
- val providers = appWidgetManager.getInstalledProviders()
-
- // 2. Group by Package Name
- val grouped = providers.groupBy { it.provider.packageName }
-
- // 3. Map to UI Model
- grouped.mapNotNull { (packageName, providerList) ->
- try {
- val appInfo = packageManager.getApplicationInfo(packageName, 0)
- val appName = packageManager.getApplicationLabel(appInfo).toString()
- val appIcon = packageManager.getApplicationIcon(appInfo)
-
- WidgetAppGroup(
- packageName = packageName,
- appName = appName,
- appIcon = appIcon,
- widgets = providerList
- )
- } catch (e: Exception) {
- null // Skip apps that can't be resolved
- }
- }.sortedBy { it.appName }
- }
- _widgetGroups.value = groups
- _isLoading.value = false
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/onboarding/OnboardingScreen.kt b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/onboarding/OnboardingScreen.kt
index 6c844b4..0b92db1 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/onboarding/OnboardingScreen.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/onboarding/OnboardingScreen.kt
@@ -1,6 +1,7 @@
package com.d4viddf.hyperbridge.ui.screens.onboarding
import android.Manifest
+import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Build
@@ -8,6 +9,11 @@ import android.provider.Settings
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.shrinkVertically
+import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@@ -21,6 +27,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@@ -32,32 +39,44 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.ArrowForward
+import androidx.compose.material.icons.filled.AccessTime
import androidx.compose.material.icons.filled.Architecture
import androidx.compose.material.icons.filled.BatteryStd
import androidx.compose.material.icons.filled.Bolt
-import androidx.compose.material.icons.filled.Cancel
-import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Construction
import androidx.compose.material.icons.filled.Info
+import androidx.compose.material.icons.filled.LowPriority
+import androidx.compose.material.icons.filled.NotificationAdd
import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material.icons.filled.NotificationsActive
+import androidx.compose.material.icons.filled.Palette
import androidx.compose.material.icons.filled.Security
+import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Smartphone
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material.icons.filled.WifiOff
+import androidx.compose.material.icons.rounded.Devices
+import androidx.compose.material.icons.rounded.Notifications
+import androidx.compose.material.icons.rounded.Timer
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
-import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Checkbox
+import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Slider
+import androidx.compose.material3.Switch
import androidx.compose.material3.Text
+import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -65,21 +84,33 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.net.toUri
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
+import com.d4viddf.hyperbridge.BuildConfig
import com.d4viddf.hyperbridge.R
+import com.d4viddf.hyperbridge.data.AppPreferences
+import com.d4viddf.hyperbridge.models.IslandConfig
+import com.d4viddf.hyperbridge.models.IslandLimitMode
+import com.d4viddf.hyperbridge.models.NotificationType
+import com.d4viddf.hyperbridge.ui.components.EngineOptionCard
+import com.d4viddf.hyperbridge.ui.components.EnginePreview
+import com.d4viddf.hyperbridge.ui.components.PermanentIslandPreview
+import com.d4viddf.hyperbridge.ui.components.formatSeconds
+import com.d4viddf.hyperbridge.ui.components.timeoutSteps
+import com.d4viddf.hyperbridge.ui.theme.HyperBridgeTheme
import com.d4viddf.hyperbridge.util.DeviceUtils
import com.d4viddf.hyperbridge.util.isNotificationServiceEnabled
import com.d4viddf.hyperbridge.util.isPostNotificationsEnabled
@@ -89,13 +120,25 @@ import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun OnboardingScreen(onFinish: () -> Unit) {
- // 7 Pages
- val pagerState = rememberPagerState(pageCount = { 7 })
- val scope = rememberCoroutineScope()
+ val isCN = remember { DeviceUtils.isCNRom }
+ val isXiaomi = remember { DeviceUtils.isXiaomi }
+ val isCompatibleOS = remember { DeviceUtils.isCompatibleOS() }
+ val canProceedCompat = isXiaomi && isCompatibleOS
+
val context = LocalContext.current
+ val prefs = remember { AppPreferences(context) }
+ val useNativeLiveUpdates by prefs.useNativeLiveUpdates.collectAsState(initial = false)
+ val needsShizuku = !useNativeLiveUpdates
+
+ val isShizukuWorkaroundEnabled by prefs.isShizukuWorkaroundEnabled.collectAsState(initial = false)
+ val isShizukuPermissionGranted by com.d4viddf.hyperbridge.util.ShizukuManager.isPermissionGranted.collectAsState()
+
+ val totalPages = if (needsShizuku) 15 else 14
+ val pagerState = rememberPagerState(pageCount = { totalPages })
+ val scope = rememberCoroutineScope()
// Handle Hardware Back Button
- BackHandler(enabled = pagerState.currentPage > 0) {
+ BackHandler(enabled = pagerState.currentPage > 1) {
scope.launch { pagerState.animateScrollToPage(pagerState.currentPage - 1) }
}
@@ -104,9 +147,7 @@ fun OnboardingScreen(onFinish: () -> Unit) {
var isPostGranted by remember { mutableStateOf(isPostNotificationsEnabled(context)) }
// --- Compatibility Logic ---
- val isXiaomi = remember { DeviceUtils.isXiaomi }
- val isCompatibleOS = remember { DeviceUtils.isCompatibleOS() }
- val canProceedCompat = isXiaomi && isCompatibleOS
+ // Moved up
// --- Permission Launcher ---
val postPermissionLauncher = rememberLauncherForActivityResult(
@@ -130,48 +171,85 @@ fun OnboardingScreen(onFinish: () -> Unit) {
Scaffold(
containerColor = MaterialTheme.colorScheme.background,
bottomBar = {
- if (pagerState.currentPage > 0) {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(24.dp)
- .navigationBarsPadding(),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically
- ) {
- // Pagination Dots
- Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
- repeat(6) { iteration ->
- val active = (pagerState.currentPage - 1) == iteration
- val width = if (active) 32.dp else 10.dp
- val color = if (active) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant
- Box(
- modifier = Modifier
- .height(10.dp)
- .width(width)
- .clip(CircleShape)
- .background(color)
- )
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(24.dp)
+ .navigationBarsPadding(),
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ val currentPage = pagerState.currentPage
+ val isLastPage = currentPage == totalPages - 1
+
+ if (currentPage == 0) {
+ Button(
+ onClick = { scope.launch { pagerState.animateScrollToPage(1) } },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(56.dp),
+ shape = RoundedCornerShape(16.dp),
+ colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary)
+ ) {
+ Text(
+ stringResource(R.string.get_started),
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold
+ )
+ }
+ } else {
+ val canProceed = if (needsShizuku) {
+ when (currentPage) {
+ 1 -> canProceedCompat || BuildConfig.DEBUG
+ 2 -> isPostGranted
+ 3 -> isListenerGranted
+ 10 -> {
+ // If they turn the workaround off, they can proceed without permission
+ !isShizukuWorkaroundEnabled || isShizukuPermissionGranted || BuildConfig.DEBUG
+ }
+ else -> true
+ }
+ } else {
+ when (currentPage) {
+ 1 -> canProceedCompat || BuildConfig.DEBUG
+ 2 -> isPostGranted
+ 3 -> isListenerGranted
+ else -> true
}
}
- // Blocking Logic
- val canProceed = when (pagerState.currentPage) {
- 3 -> canProceedCompat
- 4 -> isPostGranted
- 5 -> isListenerGranted
- else -> true
+ if (currentPage > 1) {
+ OutlinedButton(
+ onClick = {
+ scope.launch { pagerState.animateScrollToPage(currentPage - 1) }
+ },
+ modifier = Modifier
+ .weight(1f)
+ .height(56.dp),
+ shape = RoundedCornerShape(16.dp),
+ contentPadding = PaddingValues(0.dp),
+ ) {
+ Text(
+ text = stringResource(R.string.back),
+ style = MaterialTheme.typography.labelLarge,
+ fontSize = 16.sp
+ )
+ }
+ } else {
+ Spacer(modifier = Modifier.weight(1f))
}
- val isLastPage = pagerState.currentPage == 6
Button(
onClick = {
if (isLastPage) onFinish()
- else scope.launch { pagerState.animateScrollToPage(pagerState.currentPage + 1) }
+ else scope.launch { pagerState.animateScrollToPage(currentPage + 1) }
},
+ modifier = Modifier
+ .weight(1f)
+ .height(56.dp),
enabled = canProceed,
shape = RoundedCornerShape(16.dp),
- contentPadding = PaddingValues(horizontal = 24.dp, vertical = 16.dp),
+ contentPadding = PaddingValues(0.dp),
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary)
) {
Text(
@@ -179,8 +257,6 @@ fun OnboardingScreen(onFinish: () -> Unit) {
style = MaterialTheme.typography.labelLarge,
fontSize = 16.sp
)
- Spacer(modifier = Modifier.width(8.dp))
- Icon(Icons.AutoMirrored.Filled.ArrowForward, null, Modifier.size(18.dp))
}
}
}
@@ -193,19 +269,32 @@ fun OnboardingScreen(onFinish: () -> Unit) {
.fillMaxSize(),
userScrollEnabled = false
) { page ->
- when (page) {
- 0 -> WelcomePage(onStartClick = { scope.launch { pagerState.animateScrollToPage(1) } })
- 1 -> ExplanationPage()
- 2 -> PrivacyPage()
- 3 -> CompatibilityPage()
- 4 -> PostPermissionPage(
- isGranted = isPostGranted,
- onRequest = {
- postPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
- }
- )
- 5 -> ListenerPermissionPage(context, isListenerGranted)
- 6 -> OptimizationPage(context)
+ val adjustedPage = if (needsShizuku && page > 10) page - 1 else page
+
+ if (needsShizuku && page == 10) {
+ ShizukuPage(prefs)
+ } else {
+ when (adjustedPage) {
+ 0 -> WelcomePage()
+ 1 -> CompatibilityPage()
+ 2 -> PostPermissionPage(
+ isGranted = isPostGranted,
+ onRequest = {
+ postPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
+ }
+ )
+ 3 -> ListenerPermissionPage(context, isListenerGranted)
+ 4 -> OptimizationPage(context)
+ 5 -> ExplanationPage()
+ 6 -> PrivacyPage()
+ 7 -> CustomizationPage()
+ 8 -> TriggersConfigPage(prefs)
+ 9 -> EngineConfigPage(prefs)
+ 10 -> PriorityEducationPage(prefs)
+ 11 -> BehaviorConfigPage(prefs)
+ 12 -> AutoHideConfigPage(prefs)
+ 13 -> PermanentIslandConfigPage(prefs)
+ }
}
}
}
@@ -216,10 +305,10 @@ fun OnboardingScreen(onFinish: () -> Unit) {
// ==========================================
@Composable
-fun WelcomePage(onStartClick: () -> Unit) {
+fun WelcomePage() {
val context = LocalContext.current
val appIconBitmap = remember(context) {
- try { context.packageManager.getApplicationIcon(context.packageName).toBitmap().asImageBitmap() } catch (e: Exception) { null }
+ try { context.packageManager.getApplicationIcon(context.packageName).toBitmap().asImageBitmap() } catch (_: Exception) { null }
}
Column(
@@ -231,7 +320,6 @@ fun WelcomePage(onStartClick: () -> Unit) {
) {
Spacer(modifier = Modifier.weight(1f))
- // Icon (No Box Container)
if (appIconBitmap != null) {
Image(
bitmap = appIconBitmap,
@@ -267,22 +355,443 @@ fun WelcomePage(onStartClick: () -> Unit) {
)
Spacer(modifier = Modifier.weight(1f))
+ }
+}
+
+@Composable
+fun ExplanationPage() {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ .padding(24.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Spacer(modifier = Modifier.weight(0.5f))
+
+ Text(
+ text = stringResource(R.string.how_it_works),
+ style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.Bold),
+ textAlign = TextAlign.Center,
+ color = MaterialTheme.colorScheme.onBackground
+ )
+
+ Spacer(modifier = Modifier.height(24.dp))
+
+ EnginePreview(isNative = false)
+
+ Spacer(modifier = Modifier.height(32.dp))
+
+ Text(
+ text = stringResource(R.string.how_it_works_desc),
+ style = MaterialTheme.typography.bodyLarge,
+ textAlign = TextAlign.Center,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ lineHeight = 24.sp
+ )
+
+ Spacer(modifier = Modifier.height(24.dp))
+
+ Card(
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.secondaryContainer),
+ shape = RoundedCornerShape(24.dp)
+ ) {
+ Row(
+ modifier = Modifier.padding(20.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(Icons.Default.Construction, null, tint = MaterialTheme.colorScheme.onSecondaryContainer)
+ Spacer(modifier = Modifier.width(16.dp))
+ Text(
+ stringResource(R.string.beta_warning),
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSecondaryContainer
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.weight(1f))
+ }
+}
+@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
+@Composable
+fun TriggersConfigPage(prefs: AppPreferences) {
+ val activeTypes by prefs.globalNotificationTypesFlow.collectAsState(initial = emptySet())
+ val scope = rememberCoroutineScope()
+ var showSheet by remember { mutableStateOf(false) }
+ val sheetState = rememberModalBottomSheetState()
+
+ OnboardingPageLayout(
+ title = stringResource(R.string.active_triggers),
+ description = stringResource(R.string.active_triggers_global_desc),
+ icon = Icons.Default.NotificationAdd,
+ iconColor = MaterialTheme.colorScheme.primary
+ ) {
Button(
- onClick = onStartClick,
+ onClick = { showSheet = true },
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
- shape = RoundedCornerShape(16.dp),
- colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary)
+ shape = RoundedCornerShape(16.dp)
+ ) {
+ Icon(Icons.Default.Settings, null)
+ Spacer(Modifier.width(12.dp))
+ Text(stringResource(R.string.configure), style = MaterialTheme.typography.bodyLarge)
+ }
+
+ Spacer(modifier = Modifier.height(24.dp))
+
+ Text(
+ text = stringResource(R.string.onboarding_can_be_changed),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.padding(horizontal = 16.dp)
+ )
+
+ if (showSheet) {
+ ModalBottomSheet(
+ onDismissRequest = { showSheet = false },
+ sheetState = sheetState
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 24.dp)
+ .padding(bottom = 32.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Text(
+ stringResource(R.string.active_triggers),
+ style = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.padding(bottom = 8.dp)
+ )
+
+ NotificationType.entries.forEach { type ->
+ val isEnabled = activeTypes.contains(type.name)
+ Card(
+ onClick = {
+ scope.launch { prefs.updateGlobalNotificationType(type, !isEnabled) }
+ },
+ shape = RoundedCornerShape(16.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = if (isEnabled) MaterialTheme.colorScheme.primaryContainer.copy(0.3f) else MaterialTheme.colorScheme.surfaceContainerLow
+ ),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Row(
+ Modifier.padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Checkbox(checked = isEnabled, onCheckedChange = null)
+ Spacer(Modifier.width(12.dp))
+ Column {
+ Text(stringResource(type.labelRes), fontWeight = FontWeight.Bold)
+ val descRes = when (type) {
+ NotificationType.STANDARD -> R.string.type_standard_desc
+ NotificationType.PROGRESS -> R.string.type_progress_desc
+ NotificationType.DOWNLOAD -> R.string.type_download_desc
+ NotificationType.MEDIA -> R.string.type_media_desc
+ NotificationType.NAVIGATION -> R.string.type_nav_desc
+ NotificationType.CALL -> R.string.type_call_desc
+ NotificationType.TIMER -> R.string.type_timer_desc
+ }
+ Text(
+ stringResource(descRes),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun PriorityEducationPage(prefs: AppPreferences) {
+ val currentMode by prefs.limitModeFlow.collectAsState(initial = IslandLimitMode.MOST_RECENT)
+ val scope = rememberCoroutineScope()
+ var showSheet by remember { mutableStateOf(false) }
+ val sheetState = rememberModalBottomSheetState()
+
+ OnboardingPageLayout(
+ title = stringResource(R.string.priority_edu_title),
+ description = stringResource(R.string.priority_edu_desc),
+ icon = Icons.Default.LowPriority,
+ iconColor = MaterialTheme.colorScheme.primary
+ ) {
+ Button(
+ onClick = { showSheet = true },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(56.dp),
+ shape = RoundedCornerShape(16.dp)
) {
+ Icon(Icons.Default.Settings, null)
+ Spacer(Modifier.width(12.dp))
+ Text(stringResource(R.string.configure), style = MaterialTheme.typography.bodyLarge)
+ }
+
+ Spacer(modifier = Modifier.height(24.dp))
+
+ Text(
+ text = stringResource(R.string.onboarding_can_be_changed),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.padding(horizontal = 16.dp)
+ )
+
+ if (showSheet) {
+ ModalBottomSheet(
+ onDismissRequest = { showSheet = false },
+ sheetState = sheetState
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 24.dp)
+ .padding(bottom = 32.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Text(
+ stringResource(R.string.island_behavior),
+ style = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.padding(bottom = 8.dp)
+ )
+
+ IslandLimitMode.entries.forEach { mode ->
+ BehaviorOptionCard(
+ mode = mode,
+ isSelected = currentMode == mode,
+ onClick = {
+ scope.launch {
+ prefs.setLimitMode(mode)
+ prefs.setPriorityEduShown(true)
+ }
+ }
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun BehaviorOptionCard(
+ mode: IslandLimitMode,
+ isSelected: Boolean,
+ onClick: () -> Unit
+) {
+ val borderColor by animateColorAsState(
+ if (isSelected) MaterialTheme.colorScheme.primary else Color.Transparent, label = "border"
+ )
+ val containerColor = if (isSelected)
+ MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.4f)
+ else
+ MaterialTheme.colorScheme.surfaceContainerLow
+
+ Card(
+ onClick = onClick,
+ shape = RoundedCornerShape(16.dp),
+ colors = CardDefaults.cardColors(containerColor = containerColor),
+ border = if (isSelected) BorderStroke(2.dp, borderColor) else null,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Row(
+ modifier = Modifier.padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ RadioButton(selected = isSelected, onClick = null)
+ Spacer(modifier = Modifier.width(16.dp))
+ Column {
+ Text(
+ stringResource(mode.titleRes),
+ fontWeight = FontWeight.Bold,
+ style = MaterialTheme.typography.titleMedium
+ )
+ Text(
+ stringResource(mode.descRes),
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun BehaviorConfigPage(prefs: AppPreferences) {
+ val config by prefs.globalConfigFlow.collectAsState(initial = IslandConfig())
+ val scope = rememberCoroutineScope()
+
+ OnboardingPageLayout(
+ title = stringResource(R.string.island_behavior),
+ description = stringResource(R.string.behavior_desc_glob_config),
+ icon = Icons.Default.Settings,
+ iconColor = MaterialTheme.colorScheme.primary
+ ) {
+ Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
+ ListOptionCard(
+ title = stringResource(R.string.remove_original_notification),
+ subtitle = stringResource(R.string.remove_original_notification_desc),
+ icon = Icons.Default.Notifications,
+ shape = RoundedCornerShape(24.dp, 24.dp, 4.dp, 4.dp),
+ onClick = {
+ scope.launch {
+ prefs.updateGlobalConfig(config.copy(removeOriginalNotification = !(config.removeOriginalNotification ?: false)))
+ }
+ },
+ trailingContent = {
+ Checkbox(checked = config.removeOriginalNotification ?: false, onCheckedChange = null)
+ }
+ )
+
+ ListOptionCard(
+ title = stringResource(R.string.config_shade_title),
+ subtitle = stringResource(R.string.config_shade_desc),
+ icon = Icons.Default.Info,
+ shape = RoundedCornerShape(4.dp, 4.dp, 24.dp, 24.dp),
+ onClick = {
+ scope.launch {
+ prefs.updateGlobalConfig(config.copy(isShowShade = !(config.isShowShade ?: true)))
+ }
+ },
+ trailingContent = {
+ Checkbox(checked = config.isShowShade ?: true, onCheckedChange = null)
+ }
+ )
+
+ Spacer(modifier = Modifier.height(24.dp))
+
Text(
- stringResource(R.string.get_started),
- style = MaterialTheme.typography.titleMedium,
- fontWeight = FontWeight.Bold
+ text = stringResource(R.string.onboarding_can_be_changed),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.padding(horizontal = 16.dp)
+ )
+ }
+ }
+}
+
+@Composable
+fun AutoHideConfigPage(prefs: AppPreferences) {
+ val config by prefs.globalConfigFlow.collectAsState(initial = IslandConfig())
+ val scope = rememberCoroutineScope()
+ // Timeout is "Enabled" if it's > 0
+ val currentTimeout = config.timeout ?: 10
+ val isTimeoutEnabled = currentTimeout > 0
+
+ OnboardingPageLayout(
+ title = stringResource(R.string.island_behavior),
+ description = stringResource(R.string.behavior_desc_hide_long),
+ icon = Icons.Rounded.Timer,
+ iconColor = MaterialTheme.colorScheme.primary
+ ) {
+ Card(
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer),
+ shape = RoundedCornerShape(24.dp),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Column(modifier = Modifier.padding(horizontal = 20.dp, vertical = 16.dp)) {
+ // Header with Switch
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Icon(Icons.Default.AccessTime, null, tint = MaterialTheme.colorScheme.onSurfaceVariant)
+ Spacer(Modifier.width(20.dp))
+ Column(modifier = Modifier.weight(1f)) {
+ Text(stringResource(R.string.auto_hide_island), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Medium)
+ Text(
+ text = if (isTimeoutEnabled) stringResource(R.string.hides_after_a_set_time) else stringResource(
+ R.string.behavior_hide_desc
+ ),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ Switch(
+ checked = isTimeoutEnabled,
+ onCheckedChange = { enabled ->
+ // When changing this, we also ensure isFloat is set to track the override
+ val newTimeout = if (enabled) 5 else 0
+ val currentIsFloat = config.isFloat ?: true
+ scope.launch { prefs.updateGlobalConfig(config.copy(timeout = newTimeout, isFloat = currentIsFloat)) }
+ }
+ )
+ }
+
+ // Expandable Slider Section
+ AnimatedVisibility(
+ visible = isTimeoutEnabled,
+ enter = expandVertically(),
+ exit = shrinkVertically()
+ ) {
+ Column {
+ Spacer(Modifier.height(16.dp))
+
+ // Time Display Label
+ Text(
+ text = formatSeconds(currentTimeout),
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.primary,
+ fontWeight = FontWeight.Bold
+ )
+
+ // Slider mapping to our steps list
+ val currentIndex = timeoutSteps.indexOf(currentTimeout).coerceAtLeast(0).toFloat()
+
+ Slider(
+ value = currentIndex,
+ onValueChange = { index ->
+ val selectedSeconds = timeoutSteps[index.toInt()]
+ val currentIsFloat = config.isFloat ?: true
+ scope.launch { prefs.updateGlobalConfig(config.copy(timeout = selectedSeconds, isFloat = currentIsFloat))}
+ },
+ valueRange = 0f..(timeoutSteps.size - 1).toFloat(),
+ steps = timeoutSteps.size - 2
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun CustomizationPage() {
+ OnboardingPageLayout(
+ title = stringResource(R.string.design),
+ description = stringResource(R.string.onboard_design_desc),
+ icon = Icons.Default.Palette,
+ iconColor = MaterialTheme.colorScheme.secondary
+ ) {
+ Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
+ ListOptionCard(
+ title = stringResource(R.string.design_section_themes),
+ subtitle = stringResource(R.string.onboard_theme_desc),
+ icon = Icons.Default.Palette,
+ shape = RoundedCornerShape(24.dp, 24.dp, 4.dp, 4.dp),
+ onClick = {},
+ trailingContent = {}
+ )
+ ListOptionCard(
+ title = stringResource(R.string.design_section_widgets),
+ subtitle = stringResource(R.string.onboarding_widget_desc),
+ icon = Icons.Default.Architecture,
+ shape = RoundedCornerShape(4.dp, 4.dp, 24.dp, 24.dp),
+ onClick = {},
+ trailingContent = {}
)
}
- Spacer(modifier = Modifier.height(16.dp))
}
}
@@ -291,7 +800,7 @@ fun WelcomePage(onStartClick: () -> Unit) {
fun OnboardingPageLayout(
title: String,
description: String,
- icon: ImageVector,
+ icon: ImageVector? = null,
iconColor: Color = MaterialTheme.colorScheme.primary,
content: @Composable ColumnScope.() -> Unit
) {
@@ -305,22 +814,23 @@ fun OnboardingPageLayout(
Spacer(modifier = Modifier.weight(0.5f))
// Expressive Icon Container
- Box(
- modifier = Modifier
- .size(120.dp)
- .background(iconColor.copy(alpha = 0.1f), CircleShape),
- contentAlignment = Alignment.Center
- ) {
- Icon(
- imageVector = icon,
- contentDescription = null,
- modifier = Modifier.size(56.dp),
- tint = iconColor
- )
+ if (icon != null) {
+ Box(
+ modifier = Modifier
+ .size(120.dp)
+ .background(iconColor.copy(alpha = 0.1f), CircleShape),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = icon,
+ contentDescription = null,
+ modifier = Modifier.size(56.dp),
+ tint = iconColor
+ )
+ }
+ Spacer(modifier = Modifier.height(40.dp))
}
- Spacer(modifier = Modifier.height(40.dp))
-
Text(
text = title,
style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.Bold),
@@ -355,30 +865,43 @@ fun OnboardingPageLayout(
}
@Composable
-fun ExplanationPage() {
+fun EngineConfigPage(prefs: AppPreferences) {
+ val useNative by prefs.useNativeLiveUpdates.collectAsState(initial = false)
+ val scope = rememberCoroutineScope()
+
OnboardingPageLayout(
- title = stringResource(R.string.how_it_works),
- description = stringResource(R.string.how_it_works_desc),
- icon = Icons.Default.Architecture,
- iconColor = MaterialTheme.colorScheme.tertiary
+ title = stringResource(R.string.engine_config_title),
+ description = stringResource(R.string.engine_desc)
) {
- Card(
- colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.secondaryContainer),
- shape = RoundedCornerShape(24.dp)
- ) {
- Row(
- modifier = Modifier.padding(20.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Icon(Icons.Default.Construction, null, tint = MaterialTheme.colorScheme.onSecondaryContainer)
- Spacer(modifier = Modifier.width(16.dp))
- Text(
- stringResource(R.string.beta_warning),
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSecondaryContainer
- )
- }
- }
+ EnginePreview(isNative = useNative)
+
+ Spacer(modifier = Modifier.height(24.dp))
+
+ EngineOptionCard(
+ title = stringResource(R.string.engine_xiaomi_title),
+ description = stringResource(R.string.engine_xiaomi_desc),
+ isSelected = !useNative,
+ onClick = { scope.launch { prefs.setUseNativeLiveUpdates(false) } }
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ EngineOptionCard(
+ title = stringResource(R.string.engine_native_title),
+ description = stringResource(R.string.engine_native_desc),
+ isSelected = useNative,
+ onClick = { scope.launch { prefs.setUseNativeLiveUpdates(true) } }
+ )
+
+ Spacer(modifier = Modifier.height(24.dp))
+
+ Text(
+ text = stringResource(R.string.onboarding_can_be_changed),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.padding(horizontal = 16.dp)
+ )
}
}
@@ -419,9 +942,9 @@ fun CompatibilityPage() {
val deviceName = DeviceUtils.getDeviceMarketName()
val (icon, color, titleRes, descRes) = when {
- !isXiaomi -> Quad(Icons.Default.Cancel, MaterialTheme.colorScheme.error, R.string.unsupported_device, R.string.req_xiaomi)
- !isCompatibleOS -> Quad(Icons.Default.Cancel, MaterialTheme.colorScheme.error, R.string.unsupported_device, R.string.req_hyperos)
- else -> Quad(Icons.Default.CheckCircle, Color(0xFF34C759), R.string.device_compatible, R.string.compatible_msg)
+ !isXiaomi -> Quad(Icons.Rounded.Devices, MaterialTheme.colorScheme.error, R.string.unsupported_device, R.string.req_xiaomi)
+ !isCompatibleOS -> Quad(Icons.Rounded.Devices, MaterialTheme.colorScheme.error, R.string.unsupported_device, R.string.req_hyperos)
+ else -> Quad(Icons.Rounded.Devices, Color(0xFF34C759), R.string.device_compatible, R.string.compatible_msg)
}
OnboardingPageLayout(
@@ -430,43 +953,166 @@ fun CompatibilityPage() {
icon = icon,
iconColor = color
) {
- Card(
- colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerHigh),
- shape = RoundedCornerShape(24.dp),
- modifier = Modifier.fillMaxWidth()
- ) {
- Column(modifier = Modifier.padding(20.dp)) {
- Row(verticalAlignment = Alignment.CenterVertically) {
- Icon(Icons.Default.Smartphone, null, Modifier.size(24.dp), tint = MaterialTheme.colorScheme.primary)
- Spacer(Modifier.width(16.dp))
- Column {
- Text(text = Build.MANUFACTURER.uppercase(), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.primary)
- Text(text = deviceName, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
- }
- }
- Spacer(Modifier.height(16.dp))
- HorizontalDivider(color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.1f))
- Spacer(Modifier.height(16.dp))
- Row(verticalAlignment = Alignment.CenterVertically) {
- Icon(Icons.Default.Info, null, Modifier.size(24.dp), tint = MaterialTheme.colorScheme.primary)
- Spacer(Modifier.width(16.dp))
- Column {
- Text(text = stringResource(R.string.system_version), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.primary)
- Text(text = osVersion, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
+ Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
+ ListOptionCard(
+ title = Build.MANUFACTURER.uppercase(),
+ subtitle = deviceName,
+ icon = Icons.Default.Smartphone,
+ shape = RoundedCornerShape(24.dp, 24.dp, 4.dp, 4.dp),
+ onClick = {},
+ trailingContent = {}
+ )
+ ListOptionCard(
+ title = stringResource(R.string.system_version),
+ subtitle = osVersion,
+ icon = Icons.Default.Info,
+ shape = RoundedCornerShape(4.dp, 4.dp, 24.dp, 24.dp),
+ onClick = {},
+ trailingContent = {}
+ )
+
+ if (isCN && isXiaomi) {
+ Card(
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer),
+ shape = RoundedCornerShape(24.dp),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Row(Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
+ Icon(Icons.Default.Warning, null, tint = MaterialTheme.colorScheme.error)
+ Spacer(Modifier.width(12.dp))
+ Text(
+ stringResource(R.string.warning_cn_rom_title),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onErrorContainer,
+ fontWeight = FontWeight.Bold
+ )
}
}
}
}
+ }
+}
+
+@Composable
+fun ShizukuPage(prefs: AppPreferences) {
+ val context = LocalContext.current
+ val scope = rememberCoroutineScope()
+
+ val isShizukuInstalled = remember { com.d4viddf.hyperbridge.util.ShizukuManager.isShizukuInstalled(context) }
+ val isShizukuRunning by com.d4viddf.hyperbridge.util.ShizukuManager.isShizukuRunning.collectAsState()
+ val isPermissionGranted by com.d4viddf.hyperbridge.util.ShizukuManager.isPermissionGranted.collectAsState()
+ val isWorkaroundEnabled by prefs.isShizukuWorkaroundEnabled.collectAsState(initial = false)
- if (isCN && isXiaomi) {
- Spacer(modifier = Modifier.height(16.dp))
- Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer), shape = RoundedCornerShape(24.dp)) {
+ OnboardingPageLayout(
+ title = stringResource(R.string.shizuku_workaround_title),
+ description = stringResource(R.string.shizuku_workaround_desc),
+ icon = Icons.Default.Construction,
+ iconColor = MaterialTheme.colorScheme.primary
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = stringResource(R.string.shizuku_enable_workaround),
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold
+ )
+ Switch(
+ checked = isWorkaroundEnabled,
+ onCheckedChange = { scope.launch { prefs.setShizukuWorkaroundEnabled(it) } },
+ enabled = isShizukuInstalled
+ )
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ if (!isShizukuInstalled) {
+ Card(
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer),
+ shape = RoundedCornerShape(16.dp),
+ modifier = Modifier.fillMaxWidth()
+ ) {
Row(Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Default.Warning, null, tint = MaterialTheme.colorScheme.error)
Spacer(Modifier.width(12.dp))
- Text(stringResource(R.string.warning_cn_rom_title), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onErrorContainer, fontWeight = FontWeight.Bold)
+ Text(
+ stringResource(R.string.shizuku_not_installed),
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onErrorContainer,
+ )
}
}
+ } else if (isWorkaroundEnabled) {
+ ListOptionCard(
+ title = stringResource(if (isPermissionGranted) R.string.shizuku_permission_granted else R.string.shizuku_status_running),
+ subtitle = stringResource(if (isPermissionGranted) R.string.shizuku_status_running else R.string.shizuku_permission_denied),
+ icon = if (isPermissionGranted) Icons.Default.Security else Icons.Default.Warning,
+ shape = RoundedCornerShape(24.dp),
+ onClick = {},
+ trailingContent = {}
+ )
+ }
+
+ if (isShizukuInstalled && isWorkaroundEnabled) {
+ Spacer(modifier = Modifier.height(24.dp))
+ Button(
+ onClick = {
+ if (!isPermissionGranted) {
+ com.d4viddf.hyperbridge.util.ShizukuManager.requestPermission(context, 1)
+ }
+ },
+ enabled = !isPermissionGranted,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(56.dp),
+ shape = RoundedCornerShape(16.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.primary,
+ )
+ ) {
+ Text(
+ stringResource(if (isPermissionGranted) R.string.perm_granted else R.string.shizuku_request_permission),
+ style = MaterialTheme.typography.titleMedium
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun ListOptionCard(
+ title: String,
+ subtitle: String,
+ icon: ImageVector,
+ shape: Shape,
+ onClick: () -> Unit,
+ trailingContent: (@Composable () -> Unit)? = null
+) {
+ Card(
+ onClick = onClick,
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer),
+ shape = shape,
+ modifier = Modifier
+ .fillMaxWidth()
+ .heightIn(min = 88.dp)
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = 20.dp, vertical = 16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(icon, null, tint = MaterialTheme.colorScheme.onSurfaceVariant)
+ Spacer(Modifier.width(20.dp))
+ Column(modifier = Modifier.weight(1f)) {
+ Text(title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Medium)
+ Text(subtitle, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
+ }
+ if (trailingContent != null) {
+ trailingContent()
+ }
}
}
}
@@ -480,14 +1126,16 @@ fun PostPermissionPage(isGranted: Boolean, onRequest: () -> Unit) {
OnboardingPageLayout(
title = stringResource(R.string.show_island),
description = stringResource(R.string.perm_post_desc),
- icon = if (isGranted) Icons.Default.CheckCircle else Icons.Default.Notifications,
- iconColor = if (isGranted) Color(0xFF34C759) else MaterialTheme.colorScheme.primary
+ icon = Icons.Rounded.Notifications,
+ iconColor = MaterialTheme.colorScheme.primary
) {
// FIX: Standard Button, Disabled when granted, Text changes, No Icon
Button(
onClick = { if (!isGranted) onRequest() },
enabled = !isGranted,
- modifier = Modifier.fillMaxWidth().height(56.dp),
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(56.dp),
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
@@ -507,8 +1155,8 @@ fun ListenerPermissionPage(context: Context, isGranted: Boolean) {
OnboardingPageLayout(
title = stringResource(R.string.read_data),
description = stringResource(R.string.perm_listener_desc),
- icon = if (isGranted) Icons.Default.CheckCircle else Icons.Default.NotificationsActive,
- iconColor = if (isGranted) Color(0xFF34C759) else MaterialTheme.colorScheme.secondary
+ icon = Icons.Default.NotificationsActive,
+ iconColor = MaterialTheme.colorScheme.secondary
) {
// FIX: Standard Button, Disabled when granted, Text changes, No Icon
Button(
@@ -518,7 +1166,9 @@ fun ListenerPermissionPage(context: Context, isGranted: Boolean) {
}
},
enabled = !isGranted,
- modifier = Modifier.fillMaxWidth().height(56.dp),
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(56.dp),
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
@@ -533,15 +1183,16 @@ fun ListenerPermissionPage(context: Context, isGranted: Boolean) {
}
// --- 7. OPTIMIZATION PAGE ---
+@SuppressLint("BatteryLife")
@Composable
fun OptimizationPage(context: Context) {
OnboardingPageLayout(
title = stringResource(R.string.optimization_title),
description = stringResource(R.string.optimization_desc),
icon = Icons.Default.BatteryStd,
- iconColor = Color(0xFFFF9800)
+ iconColor = MaterialTheme.colorScheme.secondary
) {
- OutlinedButton(
+ Button(
onClick = {
try {
val intent = Intent()
@@ -550,9 +1201,11 @@ fun OptimizationPage(context: Context) {
"com.miui.permcenter.autostart.AutoStartManagementActivity"
)
context.startActivity(intent)
- } catch (e: Exception) { }
+ } catch (_: Exception) { }
},
- modifier = Modifier.fillMaxWidth().height(56.dp),
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(56.dp),
shape = RoundedCornerShape(16.dp)
) {
Text(stringResource(R.string.enable_autostart), style = MaterialTheme.typography.bodyLarge)
@@ -560,19 +1213,212 @@ fun OptimizationPage(context: Context) {
Spacer(modifier = Modifier.height(12.dp))
- OutlinedButton(
+ Button(
onClick = {
try {
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
data = "package:${context.packageName}".toUri()
}
context.startActivity(intent)
- } catch (e: Exception) { }
+ } catch (_: Exception) { }
},
- modifier = Modifier.fillMaxWidth().height(56.dp),
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(56.dp),
shape = RoundedCornerShape(16.dp)
) {
- Text(stringResource(R.string.no_restrictions), style = MaterialTheme.typography.bodyLarge)
+ Text(stringResource(R.string.set_battery_no_restrictions), style = MaterialTheme.typography.bodyLarge)
}
}
+}
+
+@Preview(showBackground = true, apiLevel = 36)
+@Composable
+fun OnboardingScreenPreview() {
+ HyperBridgeTheme {
+ OnboardingScreen(onFinish = {})
+ }
+}
+
+@Preview(showBackground = true, apiLevel = 36)
+@Composable
+fun WelcomePagePreview() {
+ HyperBridgeTheme {
+ WelcomePage()
+ }
+}
+
+@Preview(showBackground = true, apiLevel = 36)
+@Composable
+fun ExplanationPagePreview() {
+ HyperBridgeTheme {
+ ExplanationPage()
+ }
+}
+
+@Preview(showBackground = true, apiLevel = 36)
+@Composable
+fun PrivacyPagePreview() {
+ HyperBridgeTheme {
+ PrivacyPage()
+ }
+}
+
+@Preview(showBackground = true, apiLevel = 36)
+@Composable
+fun CompatibilityPagePreview() {
+ HyperBridgeTheme {
+ CompatibilityPage()
+ }
+}
+
+@Preview(showBackground = true, apiLevel = 36)
+@Composable
+fun PostPermissionPagePreview() {
+ HyperBridgeTheme {
+ PostPermissionPage(isGranted = false, onRequest = {})
+ }
+}
+
+@Preview(showBackground = true, apiLevel = 36)
+@Composable
+fun ListenerPermissionPagePreview() {
+ HyperBridgeTheme {
+ ListenerPermissionPage(context = LocalContext.current, isGranted = false)
+ }
+}
+
+@Preview(showBackground = true, apiLevel = 36)
+@Composable
+fun TriggersConfigPagePreview() {
+ HyperBridgeTheme {
+ TriggersConfigPage(prefs = AppPreferences(LocalContext.current))
+ }
+}
+
+@Preview(showBackground = true, apiLevel = 36)
+@Composable
+fun PriorityEducationPagePreview() {
+ HyperBridgeTheme {
+ PriorityEducationPage(prefs = AppPreferences(LocalContext.current))
+ }
+}
+
+@Preview(showBackground = true, apiLevel = 36)
+@Composable
+fun BehaviorConfigPagePreview() {
+ HyperBridgeTheme {
+ BehaviorConfigPage(prefs = AppPreferences(LocalContext.current))
+ }
+}
+
+@Preview(showBackground = true, apiLevel = 36)
+@Composable
+fun CustomizationPagePreview() {
+ HyperBridgeTheme {
+ CustomizationPage()
+ }
+}
+
+@Preview(showBackground = true, apiLevel = 36)
+@Composable
+fun EngineConfigPagePreview() {
+ HyperBridgeTheme {
+ EngineConfigPage(prefs = AppPreferences(LocalContext.current))
+ }
+}
+
+@Preview(showBackground = true, apiLevel = 36)
+@Composable
+fun OptimizationPagePreview() {
+ HyperBridgeTheme {
+ OptimizationPage(context = LocalContext.current)
+ }
+}
+
+@Preview(showBackground = true, apiLevel = 36)
+@Composable
+fun AutoHidePagePreview(){
+ HyperBridgeTheme {
+ AutoHideConfigPage(prefs = AppPreferences(LocalContext.current))
+ }
+}
+
+@Composable
+fun PermanentIslandConfigPage(prefs: AppPreferences) {
+ val isEnabled by prefs.isPermanentIslandEnabledFlow.collectAsState(initial = false)
+ val islandWidth by prefs.permanentIslandWidthFlow.collectAsState(initial = 0)
+
+ var sliderValue by androidx.compose.runtime.remember { androidx.compose.runtime.mutableFloatStateOf(0f) }
+ var isDragging by androidx.compose.runtime.remember { mutableStateOf(false) }
+
+ androidx.compose.runtime.LaunchedEffect(islandWidth) {
+ if (!isDragging) {
+ sliderValue = islandWidth.toFloat()
+ }
+ }
+
+ val scope = rememberCoroutineScope()
+
+ OnboardingPageLayout(
+ title = stringResource(R.string.permanent_island_title),
+ description = stringResource(R.string.permanent_island_desc),
+ iconColor = MaterialTheme.colorScheme.primary
+ ) {
+ PermanentIslandPreview(islandWidthValue = if (isDragging) sliderValue.toInt() else islandWidth)
+
+ Spacer(Modifier.height(24.dp))
+
+ Card(
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer),
+ shape = RoundedCornerShape(24.dp),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Column(modifier = Modifier.padding(horizontal = 20.dp, vertical = 16.dp)) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+
+ Column(modifier = Modifier.weight(1f)) {
+ Text(stringResource(R.string.permanent_island_title), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Medium)
+ }
+ Switch(
+ checked = isEnabled,
+ onCheckedChange = { checked ->
+ scope.launch { prefs.setPermanentIslandEnabled(checked) }
+ }
+ )
+ }
+
+ if (isEnabled) {
+ Spacer(Modifier.height(16.dp))
+ Text(
+ text = stringResource(R.string.permanent_island_width),
+ style = MaterialTheme.typography.titleSmall,
+ fontWeight = FontWeight.Medium
+ )
+ androidx.compose.material3.Slider(
+ value = sliderValue,
+ onValueChange = { value ->
+ isDragging = true
+ sliderValue = value
+ scope.launch {
+ prefs.setPermanentIslandWidth(value.toInt())
+ }
+ },
+ onValueChangeFinished = {
+ isDragging = false
+ },
+ valueRange = 0f..20f
+ )
+ }
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true, apiLevel = 36)
+@Composable
+fun PermanentIslandConfigPagePreview() {
+ HyperBridgeTheme {
+ PermanentIslandConfigPage(prefs = AppPreferences(LocalContext.current))
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/AppPriorityScreen.kt b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/AppPriorityScreen.kt
index 9e6df90..c992b14 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/AppPriorityScreen.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/AppPriorityScreen.kt
@@ -123,7 +123,7 @@ fun AppPriorityScreen(onBack: () -> Unit) {
val appInfo = packageManager.getApplicationInfo(pkg, 0)
val name = packageManager.getApplicationLabel(appInfo).toString()
PriorityAppInfo(name, pkg)
- } catch (e: Exception) {
+ } catch (_: Exception) {
PriorityAppInfo(pkg, pkg) // Fallback
}
}
@@ -306,7 +306,7 @@ fun DraggableLazyList(
val isDragging = index == draggingIndex
- // Visual Feedback: Lift up and scale
+ // Visual Feedback: Lift and scale
val elevation by animateDpAsState(if (isDragging) 12.dp else 0.dp, label = "elevation")
val scale by animateFloatAsState(if (isDragging) 1.05f else 1f, label = "scale")
val alpha = if (isDragging) 0.9f else 1f
@@ -443,7 +443,7 @@ fun AppIconLoader(packageName: String, contentDescription: String) {
try {
val drawable = context.packageManager.getApplicationIcon(packageName)
iconBitmap = drawable.toBitmap()
- } catch (e: Exception) { /* Ignore */ }
+ } catch (_: Exception) { /* Ignore */ }
}
}
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/ChangelogHistoryScreen.kt b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/ChangelogHistoryScreen.kt
index 27ca195..345887e 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/ChangelogHistoryScreen.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/ChangelogHistoryScreen.kt
@@ -31,7 +31,8 @@ data class VersionLog(val version: String, val titleRes: Int, val textRes: Int,
fun ChangelogHistoryScreen(onBack: () -> Unit) {
// Define history here (Newest first)
val history = listOf(
- VersionLog("0.4.2", R.string.title_0_4_2, R.string.changelog_0_4_2, isLatest = true),
+ VersionLog("0.5.0", R.string.title_0_5_0, R.string.changelog_0_5_0, isLatest = true),
+ VersionLog("0.4.2", R.string.title_0_4_2, R.string.changelog_0_4_2),
VersionLog("0.4.0", R.string.title_0_4_0, R.string.changelog_0_4_0),
VersionLog("0.3.1", R.string.title_0_3_1, R.string.changelog_0_3_1),
VersionLog("0.3.0", R.string.title_0_3_0, R.string.changelog_0_3_0),
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/DndSettingsScreen.kt b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/DndSettingsScreen.kt
new file mode 100644
index 0000000..8470606
--- /dev/null
+++ b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/DndSettingsScreen.kt
@@ -0,0 +1,104 @@
+package com.d4viddf.hyperbridge.ui.screens.settings
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.outlined.DoNotDisturbOn
+import androidx.compose.material.icons.outlined.NotificationsPaused
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FilledTonalIconButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Switch
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.d4viddf.hyperbridge.R
+import com.d4viddf.hyperbridge.data.AppPreferences
+import com.d4viddf.hyperbridge.ui.components.ListOptionCard
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun DndSettingsScreen(onBack: () -> Unit) {
+ val context = LocalContext.current
+ val prefs = remember { AppPreferences(context) }
+ val isDndModeEnabled by prefs.isDndModeEnabledFlow.collectAsState(initial = false)
+ val autoDetectDnd by prefs.autoDetectDndFlow.collectAsState(initial = true)
+ val scope = rememberCoroutineScope()
+
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text(stringResource(R.string.dnd_mode_title)) },
+ navigationIcon = {
+ FilledTonalIconButton(onClick = onBack) {
+ Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.back))
+ }
+ }
+ )
+ }
+ ) { padding ->
+ Column(
+ modifier = Modifier
+ .padding(padding)
+ .padding(16.dp)
+ .verticalScroll(rememberScrollState())
+ ) {
+ Text(
+ text = stringResource(R.string.dnd_mode_desc),
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.padding(bottom = 16.dp, start = 8.dp, end = 8.dp)
+ )
+
+ ListOptionCard(
+ title = stringResource(R.string.dnd_auto_detect),
+ subtitle = stringResource(R.string.dnd_auto_detect_desc),
+ icon = Icons.Outlined.DoNotDisturbOn,
+ shape = RoundedCornerShape(24.dp, 24.dp, 4.dp, 4.dp),
+ onClick = {
+ scope.launch { prefs.setAutoDetectDnd(!autoDetectDnd) }
+ },
+ trailingContent = {
+ Switch(checked = autoDetectDnd, onCheckedChange = {
+ scope.launch { prefs.setAutoDetectDnd(it) }
+ })
+ }
+ )
+
+ Spacer(Modifier.height(2.dp))
+
+ ListOptionCard(
+ title = stringResource(R.string.dnd_manual_toggle),
+ subtitle = stringResource(R.string.dnd_manual_toggle_desc),
+ icon = Icons.Outlined.NotificationsPaused,
+ shape = RoundedCornerShape(4.dp, 4.dp, 24.dp, 24.dp),
+ onClick = {
+ scope.launch { prefs.setDndModeEnabled(!isDndModeEnabled) }
+ },
+ trailingContent = {
+ Switch(checked = isDndModeEnabled, onCheckedChange = {
+ scope.launch { prefs.setDndModeEnabled(it) }
+ })
+ }
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/EngineSettingsScreen.kt b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/EngineSettingsScreen.kt
new file mode 100644
index 0000000..836dac2
--- /dev/null
+++ b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/EngineSettingsScreen.kt
@@ -0,0 +1,219 @@
+package com.d4viddf.hyperbridge.ui.screens.settings
+
+import android.content.Intent
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FilledTonalIconButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.filled.Security
+import androidx.compose.material.icons.filled.Warning
+import com.d4viddf.hyperbridge.R
+import com.d4viddf.hyperbridge.data.AppPreferences
+import com.d4viddf.hyperbridge.data.theme.ThemeRepository
+import com.d4viddf.hyperbridge.service.NotificationReaderService
+import com.d4viddf.hyperbridge.ui.components.EngineOptionCard
+import com.d4viddf.hyperbridge.ui.components.EnginePreview
+import com.d4viddf.hyperbridge.ui.screens.onboarding.ListOptionCard
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun EngineSettingsScreen(onBack: () -> Unit) {
+ val context = LocalContext.current
+ val prefs = remember { AppPreferences(context) }
+ val repo = remember { ThemeRepository(context) }
+ val scope = rememberCoroutineScope()
+
+ // 1. Observe both the Global Preference and the Active Theme
+ val defaultEnginePref by prefs.useNativeLiveUpdates.collectAsState(initial = false)
+ val activeTheme by repo.activeTheme.collectAsState(initial = null)
+
+ // 2. Calculate which engine is ACTUALLY being used right now
+ val isCustomTheme = activeTheme != null && activeTheme!!.id.isNotEmpty()
+ val isNative = if (isCustomTheme) {
+ // [FIX] If the theme inherits (null), fall back to the global preference!
+ activeTheme!!.global.useNativeLiveUpdates ?: defaultEnginePref
+ } else {
+ defaultEnginePref
+ }
+
+ // 3. Handle Engine Changes intelligently based on active state
+ val onEngineChange = { useNative: Boolean ->
+ scope.launch {
+ if (isCustomTheme) {
+ // Patch the active Custom Theme so the setting actually sticks!
+ val currentTheme = activeTheme!!
+ val updatedGlobal = currentTheme.global.copy(useNativeLiveUpdates = useNative)
+ val updatedTheme = currentTheme.copy(global = updatedGlobal)
+ repo.saveTheme(updatedTheme)
+ repo.activateTheme(updatedTheme.id)
+ } else {
+ // Save to standard AppPreferences
+ prefs.setUseNativeLiveUpdates(useNative)
+ }
+
+ // Immediately tell the background service to reload the engine config
+ val intent = Intent(context, NotificationReaderService::class.java).apply {
+ action = NotificationReaderService.ACTION_RELOAD_THEME
+ }
+ context.startService(intent)
+ }
+ }
+
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text(stringResource(R.string.engine), fontWeight = FontWeight.Bold) },
+ navigationIcon = {
+ FilledTonalIconButton(onClick = onBack) {
+ Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back))
+ }
+ }
+ )
+ }
+ ) { paddingValues ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ .padding(horizontal = 16.dp)
+ .verticalScroll(rememberScrollState())
+ ) {
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Text(
+ text = stringResource(R.string.engine_preview_title),
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+
+ EnginePreview(isNative = isNative)
+
+ Spacer(modifier = Modifier.height(32.dp))
+
+ Text(
+ text = stringResource(R.string.engine_section_design),
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+
+ EngineOptionCard(
+ title = stringResource(R.string.engine_xiaomi_title),
+ description = stringResource(R.string.engine_xiaomi_desc),
+ isSelected = !isNative,
+ onClick = { onEngineChange(false) }
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ EngineOptionCard(
+ title = stringResource(R.string.engine_native_title),
+ description = stringResource(R.string.engine_native_desc),
+ isSelected = isNative,
+ onClick = { onEngineChange(true) }
+ )
+
+ if (!isNative) {
+ Spacer(modifier = Modifier.height(32.dp))
+
+ val isShizukuInstalled = remember { com.d4viddf.hyperbridge.util.ShizukuManager.isShizukuInstalled(context) }
+ val isShizukuRunning by com.d4viddf.hyperbridge.util.ShizukuManager.isShizukuRunning.collectAsState()
+ val isPermissionGranted by com.d4viddf.hyperbridge.util.ShizukuManager.isPermissionGranted.collectAsState()
+ val isWorkaroundEnabled by prefs.isShizukuWorkaroundEnabled.collectAsState(initial = false)
+
+ androidx.compose.foundation.layout.Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = androidx.compose.ui.Alignment.CenterVertically,
+ horizontalArrangement = androidx.compose.foundation.layout.Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = stringResource(R.string.shizuku_workaround_title),
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold
+ )
+ androidx.compose.material3.Switch(
+ checked = isWorkaroundEnabled,
+ onCheckedChange = { scope.launch { prefs.setShizukuWorkaroundEnabled(it) } },
+ enabled = isShizukuInstalled
+ )
+ }
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Text(
+ text = stringResource(R.string.shizuku_workaround_desc),
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+
+ if (isShizukuInstalled && isWorkaroundEnabled) {
+ ListOptionCard(
+ title = stringResource(if (isPermissionGranted) R.string.shizuku_permission_granted else R.string.shizuku_status_running),
+ subtitle = stringResource(if (isPermissionGranted) R.string.shizuku_status_running else R.string.shizuku_permission_denied),
+ icon = if (isPermissionGranted) Icons.Default.Security else Icons.Default.Warning,
+ shape = RoundedCornerShape(16.dp),
+ onClick = {},
+ trailingContent = {}
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+ androidx.compose.material3.Button(
+ onClick = {
+ if (!isPermissionGranted) {
+ com.d4viddf.hyperbridge.util.ShizukuManager.requestPermission(context, 1)
+ }
+ },
+ enabled = !isPermissionGranted,
+ modifier = Modifier.fillMaxWidth().height(56.dp),
+ shape = androidx.compose.foundation.shape.RoundedCornerShape(16.dp),
+ colors = androidx.compose.material3.ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.primary,
+ )
+ ) {
+ Text(
+ stringResource(if (isPermissionGranted) R.string.perm_granted else R.string.shizuku_request_permission),
+ style = MaterialTheme.typography.titleMedium
+ )
+ }
+ }
+
+ if (!isShizukuInstalled) {
+ Spacer(modifier = Modifier.height(16.dp))
+ Text(
+ text = stringResource(R.string.shizuku_not_installed),
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.error
+ )
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/GlobalSettingsScreen.kt b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/GlobalSettingsScreen.kt
index dcb7994..c388231 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/GlobalSettingsScreen.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/GlobalSettingsScreen.kt
@@ -1,41 +1,56 @@
package com.d4viddf.hyperbridge.ui.screens.settings
-import android.content.Context
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
-import androidx.compose.material.icons.automirrored.rounded.ArrowForwardIos
-import androidx.compose.material.icons.filled.Navigation
-import androidx.compose.material.icons.filled.Notifications
-import androidx.compose.material3.*
-import androidx.compose.runtime.*
+import androidx.compose.material.icons.outlined.DisplaySettings
+import androidx.compose.material.icons.outlined.DoNotDisturbOn
+import androidx.compose.material.icons.outlined.Memory
+import androidx.compose.material.icons.outlined.Navigation
+import androidx.compose.material.icons.outlined.PushPin
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FilledTonalIconButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Switch
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.d4viddf.hyperbridge.R
-import com.d4viddf.hyperbridge.data.AppPreferences
-import com.d4viddf.hyperbridge.models.IslandConfig
-import com.d4viddf.hyperbridge.ui.components.IslandSettingsControl
-import kotlinx.coroutines.launch
+import com.d4viddf.hyperbridge.ui.components.ListOptionCard
+import com.d4viddf.hyperbridge.ui.screens.theme.ShapeStyle
+import com.d4viddf.hyperbridge.ui.screens.theme.getExpressiveShape
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun GlobalSettingsScreen(
onBack: () -> Unit,
- onNavSettingsClick: () -> Unit // New Callback
+ onNavSettingsClick: () -> Unit,
+ onIslandSettingsClick: () -> Unit,
+ onEngineSettingsClick: () -> Unit,
+ onDndSettingsClick: () -> Unit,
+ onPermanentIslandClick: () -> Unit
) {
- val context = LocalContext.current
- val scope = rememberCoroutineScope()
- val preferences = remember { AppPreferences(context) }
-
- val globalConfig by preferences.globalConfigFlow.collectAsState(initial = IslandConfig(true, true, 5))
Scaffold(
topBar = {
@@ -49,107 +64,52 @@ fun GlobalSettingsScreen(
)
}
) { padding ->
- Column(modifier = Modifier.padding(padding).padding(16.dp)) {
- // Island Settings Card
- Card(
- colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerHigh)
- ) {
- Column(Modifier.padding(16.dp)) {
- IslandSettingsControl(
- config = globalConfig,
- onUpdate = { newConfig ->
- scope.launch { preferences.updateGlobalConfig(newConfig) }
- }
- )
- }
- }
- Spacer(modifier = Modifier.height(16.dp))
-
- // Navigation Layout Card
- Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerHigh)) {
- SettingsItem(
- icon = Icons.Default.Navigation,
- title = stringResource(R.string.nav_layout_title),
- subtitle = stringResource(R.string.nav_layout_desc),
- onClick = onNavSettingsClick
- )
- }
- Spacer(modifier = Modifier.height(16.dp))
-
- var useNativeLiveUpdates by remember { mutableStateOf(false) }
-
- LaunchedEffect(Unit) {
- val prefs = context.getSharedPreferences("hyperbridge_settings", Context.MODE_PRIVATE)
- useNativeLiveUpdates = prefs.getBoolean("use_native_live_updates", false)
- }
-
- // Native Live Updates Card
- Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerHigh)) {
- SettingsSwitchItem(
- icon = Icons.Default.Notifications,
- title = stringResource(R.string.settings_live_updates_title),
- subtitle = stringResource(R.string.settings_live_updates_desc),
- checked = useNativeLiveUpdates,
- onCheckedChange = { isChecked ->
- useNativeLiveUpdates = isChecked
- context.getSharedPreferences("hyperbridge_settings", Context.MODE_PRIVATE)
- .edit().putBoolean("use_native_live_updates", isChecked).apply()
- }
- )
- }
- }
- }
-}
-
-@Composable
-fun SettingsItem(
- icon: ImageVector,
- title: String,
- subtitle: String,
- onClick: () -> Unit
-) {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .clickable { onClick() }
- .padding(horizontal = 20.dp, vertical = 16.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Box(
- modifier = Modifier
- .size(40.dp)
- .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f), CircleShape),
- contentAlignment = Alignment.Center
+ Column(modifier = Modifier
+ .padding(padding)
+ .padding(16.dp)
+ .verticalScroll(rememberScrollState())
) {
- Icon(
- imageVector = icon,
- contentDescription = null,
- tint = MaterialTheme.colorScheme.primary,
- modifier = Modifier.size(24.dp)
+ // Island Settings Card
+ ListOptionCard(
+ title = stringResource(R.string.engine),
+ subtitle = stringResource(R.string.engine_desc),
+ icon = Icons.Outlined.Memory,
+ shape = getExpressiveShape(5, 0, ShapeStyle.Large),
+ onClick = onEngineSettingsClick
)
- }
- Spacer(modifier = Modifier.width(16.dp))
- Column(modifier = Modifier.weight(1f)) {
- Text(
- text = title,
- style = MaterialTheme.typography.bodyLarge,
- fontWeight = FontWeight.SemiBold,
- color = MaterialTheme.colorScheme.onSurface
+ Spacer(Modifier.height(2.dp))
+ ListOptionCard(
+ title = stringResource(R.string.island_behavior_title),
+ subtitle = stringResource(R.string.island_behavior_desc),
+ icon = Icons.Outlined.DisplaySettings,
+ shape = getExpressiveShape(5, 1, ShapeStyle.Large),
+ onClick = onIslandSettingsClick
)
- Spacer(modifier = Modifier.height(2.dp))
- Text(
- text = subtitle,
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant
+ Spacer(Modifier.height(2.dp))
+ ListOptionCard(
+ title = stringResource(R.string.dnd_mode_title),
+ subtitle = stringResource(R.string.dnd_mode_desc),
+ icon = Icons.Outlined.DoNotDisturbOn,
+ shape = getExpressiveShape(5, 2, ShapeStyle.Large),
+ onClick = onDndSettingsClick
+ )
+ Spacer(Modifier.height(2.dp))
+ ListOptionCard(
+ title = stringResource(R.string.nav_layout_title),
+ subtitle = stringResource(R.string.nav_layout_desc),
+ icon = Icons.Outlined.Navigation,
+ shape = getExpressiveShape(5, 3, ShapeStyle.Large),
+ onClick = onNavSettingsClick
+ )
+ Spacer(Modifier.height(2.dp))
+ ListOptionCard(
+ title = stringResource(R.string.permanent_island_title),
+ subtitle = stringResource(R.string.permanent_island_desc),
+ icon = Icons.Outlined.PushPin,
+ shape = getExpressiveShape(5, 4, ShapeStyle.Large),
+ onClick = onPermanentIslandClick
)
}
- // Use AutoMirrored icon for RTL support
- Icon(
- imageVector = Icons.AutoMirrored.Rounded.ArrowForwardIos,
- contentDescription = null,
- tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f),
- modifier = Modifier.size(16.dp)
- )
}
}
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/InfoScreen.kt b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/InfoScreen.kt
index a0f5151..7e19b40 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/InfoScreen.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/InfoScreen.kt
@@ -292,6 +292,7 @@ fun LanguageSelectorDialog(onDismiss: () -> Unit) {
"Deutsch" to "de",
"English" to "en",
"Español" to "es",
+ "Français" to "fr", // Added (French)
"Italiano" to "it", // Added (Italian)
"Magyar" to "hu", // Added (Hungarian)
"Português (BR)" to "pt-BR",
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/IslandSettingsScreen.kt b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/IslandSettingsScreen.kt
new file mode 100644
index 0000000..e713461
--- /dev/null
+++ b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/IslandSettingsScreen.kt
@@ -0,0 +1,99 @@
+package com.d4viddf.hyperbridge.ui.screens.settings
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FilledTonalIconButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.d4viddf.hyperbridge.R
+import com.d4viddf.hyperbridge.data.AppPreferences
+import com.d4viddf.hyperbridge.models.IslandConfig
+import com.d4viddf.hyperbridge.ui.components.IslandSettingsControl
+import com.d4viddf.hyperbridge.ui.theme.HyperBridgeTheme
+import kotlinx.coroutines.launch
+
+@Composable
+fun IslandSettingsScreen(
+ onBack: () -> Unit,
+) {
+
+ val context = LocalContext.current
+ val scope = rememberCoroutineScope()
+ val preferences = remember { AppPreferences(context) }
+
+ val globalConfig by preferences.globalConfigFlow.collectAsState(initial = IslandConfig(
+ isFloat = false,
+ isShowShade = false,
+ timeout = 10
+ ))
+
+ IslandSettingsContent(
+ globalConfig = globalConfig,
+ onBack = onBack,
+ onUpdateConfig = { newConfig ->
+ scope.launch { preferences.updateGlobalConfig(newConfig) }
+ }
+ )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun IslandSettingsContent(
+ globalConfig: IslandConfig,
+ onBack: () -> Unit,
+ onUpdateConfig: (IslandConfig) -> Unit
+) {
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text(stringResource(R.string.global_settings)) },
+ navigationIcon = {
+ FilledTonalIconButton(onClick = onBack) {
+ Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.back))
+ }
+ }
+ )
+ }
+ ) { padding ->
+ Column(
+ modifier = Modifier
+ .padding(padding)
+ .verticalScroll(rememberScrollState())
+ .padding(16.dp)
+ ) {
+ IslandSettingsControl(
+ config = globalConfig,
+ onUpdate = onUpdateConfig
+ )
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun IslandSettingsScreenPreview() {
+ HyperBridgeTheme {
+ IslandSettingsContent(
+ globalConfig = IslandConfig(isFloat = true, isShowShade = true, timeout = 5, floatTimeout = 6),
+ onBack = {},
+ onUpdateConfig = {}
+ )
+ }
+}
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/LicensesScreen.kt b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/LicensesScreen.kt
index 6557ca0..5083692 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/LicensesScreen.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/LicensesScreen.kt
@@ -19,7 +19,9 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
+import com.d4viddf.hyperbridge.R
data class Library(val name: String, val author: String, val license: String, val url: String)
@@ -33,13 +35,23 @@ fun LicensesScreen(onBack: () -> Unit) {
Library("Jetpack Compose", "Google", "Apache 2.0", "https://developer.android.com/jetpack/compose"),
Library("Material 3", "Google", "Apache 2.0", "https://m3.material.io/"),
Library("AndroidX Core", "Google", "Apache 2.0", "https://developer.android.com/jetpack/androidx"),
- Library("Kotlin Coroutines", "JetBrains", "Apache 2.0", "https://github.com/Kotlin/kotlinx.coroutines")
- )
+ Library("AndroidX Activity", "Google", "Apache 2.0", "https://developer.android.com/jetpack/androidx/releases/activity"),
+ Library("AndroidX AppCompat", "Google", "Apache 2.0", "https://developer.android.com/jetpack/androidx/releases/appcompat"),
+ Library("AndroidX DataStore", "Google", "Apache 2.0", "https://developer.android.com/topic/libraries/architecture/datastore"),
+ Library("AndroidX Lifecycle", "Google", "Apache 2.0", "https://developer.android.com/jetpack/androidx/releases/lifecycle"),
+ Library("AndroidX Navigation 3", "Google", "Apache 2.0", "https://developer.android.com/jetpack/androidx/releases/navigation"),
+ Library("AndroidX Palette", "Google", "Apache 2.0", "https://developer.android.com/develop/ui/views/graphics/palette"),
+ Library("AndroidX Room", "Google", "Apache 2.0", "https://developer.android.com/training/data-storage/room"),
+ Library("Gson", "Google", "Apache 2.0", "https://github.com/google/gson"),
+ Library("Kotlin Coroutines", "JetBrains", "Apache 2.0", "https://github.com/Kotlin/kotlinx.coroutines"),
+ Library("Kotlin Serialization", "JetBrains", "Apache 2.0", "https://github.com/Kotlin/kotlinx.serialization"),
+ Library("Shizuku API", "RikkaApps", "Apache 2.0", "https://github.com/RikkaApps/Shizuku-API")
+ ).sortedBy { it.name }
Scaffold(
topBar = {
TopAppBar(
- title = { Text("Open Source Licenses") },
+ title = { Text(stringResource(R.string.open_source_licenses)) },
navigationIcon = {
FilledTonalIconButton(
onClick = onBack,
@@ -49,7 +61,8 @@ fun LicensesScreen(onBack: () -> Unit) {
) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back")
}
- } )
+ }
+ )
}
) { padding ->
LazyColumn(modifier = Modifier.padding(padding)) {
@@ -63,4 +76,4 @@ fun LicensesScreen(onBack: () -> Unit) {
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/NavCustomizationScreen.kt b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/NavCustomizationScreen.kt
index db95220..4f49737 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/NavCustomizationScreen.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/NavCustomizationScreen.kt
@@ -1,9 +1,6 @@
package com.d4viddf.hyperbridge.ui.screens.settings
-import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -11,24 +8,18 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
-import androidx.compose.material.icons.filled.TurnRight
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Checkbox
-import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.ExposedDropdownMenuBox
-import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
@@ -38,62 +29,74 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.Brush
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
import com.d4viddf.hyperbridge.R
import com.d4viddf.hyperbridge.data.AppPreferences
import com.d4viddf.hyperbridge.models.NavContent
+import com.d4viddf.hyperbridge.ui.components.NavDropdown
+import com.d4viddf.hyperbridge.ui.components.NavPreview
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NavCustomizationScreen(
onBack: () -> Unit,
- packageName: String? = null
+ packageName: String? = null,
+ showTopBar: Boolean = true // <-- NEW FLAG
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val preferences = remember { AppPreferences(context) }
+ // 1. Get Global Fallback
val globalLayout by preferences.globalNavLayoutFlow.collectAsState(initial = NavContent.DISTANCE_ETA to NavContent.INSTRUCTION)
+
+ // 2. Get local AppPreference (if packageName is provided)
val appLayout by if (packageName != null) {
preferences.getAppNavLayout(packageName).collectAsState(initial = null to null)
} else {
- remember { mutableStateOf(null to null) }
+ remember { mutableStateOf>(null to null) }
}
+ // 3. Resolve what is currently active
val isGlobalMode = packageName == null
val isUsingGlobalDefault = !isGlobalMode && appLayout.first == null
- val currentLeft = if (isGlobalMode || isUsingGlobalDefault) globalLayout.first else (appLayout.first ?: globalLayout.first)
- val currentRight = if (isGlobalMode || isUsingGlobalDefault) globalLayout.second else (appLayout.second ?: globalLayout.second)
+ val currentLeft = appLayout.first ?: globalLayout.first
+ val currentRight = appLayout.second ?: globalLayout.second
Scaffold(
topBar = {
- TopAppBar(
- title = { Text(stringResource(R.string.nav_layout_title)) },
- navigationIcon = {
- FilledTonalIconButton(onClick = onBack) {
- Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.back))
+ if (showTopBar) { // Only show if requested
+ TopAppBar(
+ title = { Text(stringResource(R.string.nav_layout_title)) },
+ navigationIcon = {
+ FilledTonalIconButton(onClick = onBack) {
+ Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.back))
+ }
}
- }
- )
+ )
+ }
}
) { padding ->
- Column(modifier = Modifier.padding(padding).padding(16.dp)) {
+ Column(
+ modifier = Modifier
+ .padding(padding)
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState()) // Allow scrolling to fit on smaller screens
+ .padding(
+ start = 16.dp,
+ end = 16.dp,
+ bottom = 16.dp,
+ top = if (showTopBar) 16.dp else 0.dp // <-- DYNAMIC TOP PADDING
+ )
+ ) {
Text("Preview", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.height(16.dp))
@@ -116,9 +119,21 @@ fun NavCustomizationScreen(
.fillMaxWidth()
.clickable {
if (isUsingGlobalDefault) {
- scope.launch { preferences.updateAppNavLayout(packageName, globalLayout.first, globalLayout.second) }
+ scope.launch {
+ preferences.updateAppNavLayout(
+ packageName,
+ globalLayout.first,
+ globalLayout.second
+ )
+ }
} else {
- scope.launch { preferences.updateAppNavLayout(packageName, null, null) }
+ scope.launch {
+ preferences.updateAppNavLayout(
+ packageName,
+ null,
+ null
+ )
+ }
}
}
.padding(vertical = 8.dp),
@@ -158,172 +173,28 @@ fun NavCustomizationScreen(
}
}
}
- }
- }
-}
-
-@Composable
-fun NavPreview(left: NavContent, right: NavContent) {
- val leftLabel = stringResource(getNavContentLabelRes(left))
- val rightLabel = stringResource(getNavContentLabelRes(right))
- val cd = stringResource(R.string.cd_nav_preview, leftLabel, rightLabel)
- Card(
- colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerHigh),
- shape = RoundedCornerShape(24.dp),
- modifier = Modifier
- .fillMaxWidth()
- .semantics(mergeDescendants = true) { contentDescription = cd }
- ) {
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .padding(vertical = 32.dp),
- contentAlignment = Alignment.Center
- ) {
- // --- UPDATED ISLAND SIZE ---
- Box(
- modifier = Modifier
- .width(330.dp) // Increased from 280dp to 340dp to fit Dist+ETA
- .height(46.dp)
- .clip(RoundedCornerShape(50))
- .background(Color.Black)
+ // --- Informational Notes ---
+ Spacer(modifier = Modifier.height(24.dp))
+ Card(
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.5f))
) {
- // Camera Cutout
- Box(
- modifier = Modifier
- .align(Alignment.Center)
- .size(30.dp)
- .clip(CircleShape)
- .background(Color(0xFF1F1F1F))
- )
-
- Row(
- modifier = Modifier
- .fillMaxSize()
- .padding(horizontal = 12.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- // LEFT SIDE
- Row(
- modifier = Modifier.weight(1f),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- Icon(
- imageVector = Icons.Default.TurnRight,
- contentDescription = null,
- modifier = Modifier.size(20.dp)
- )
- Spacer(Modifier.width(6.dp))
- NavContentRenderer(left, Alignment.Start)
- }
-
- // SPACER
- Spacer(modifier = Modifier.width(32.dp))
-
- // RIGHT SIDE
- Box(
- modifier = Modifier.weight(1f),
- contentAlignment = Alignment.CenterEnd
- ) {
- NavContentRenderer(right, Alignment.End)
- }
- }
- }
- }
- }
-}
-
-@Composable
-fun NavContentRenderer(type: NavContent, align: Alignment.Horizontal) {
-
- // Fade Logic: Only applies to long text (Instruction)
- val fadeBrush = if (type == NavContent.INSTRUCTION) {
- if (align == Alignment.Start) {
- Brush.horizontalGradient(0.85f to Color.White, 1.0f to Color.Transparent)
- } else {
- Brush.horizontalGradient(0.0f to Color.Transparent, 0.15f to Color.White)
- }
- } else null
-
- val textStyle = if (fadeBrush != null) {
- TextStyle(brush = fadeBrush, fontWeight = FontWeight.Bold, fontSize = 14.sp)
- } else {
- TextStyle(color = Color.White, fontWeight = FontWeight.Bold, fontSize = 14.sp)
- }
-
- val timeStyle = TextStyle(color = Color.White, fontWeight = FontWeight.Normal, fontSize = 14.sp)
-
- when (type) {
- NavContent.INSTRUCTION -> {
- Text(
- text = stringResource(R.string.nav_preview_instruction),
- style = textStyle,
- maxLines = 1,
- overflow = TextOverflow.Clip
- )
- }
- NavContent.DISTANCE -> {
- Text(
- text = stringResource(R.string.nav_preview_distance),
- style = textStyle // Solid White
- )
- }
- NavContent.ETA -> {
- Text(
- text = stringResource(R.string.nav_preview_time),
- style = timeStyle // Standard weight
- )
- }
- NavContent.DISTANCE_ETA -> {
- // Combined Row
- Row(verticalAlignment = Alignment.CenterVertically) {
- Text(stringResource(R.string.nav_preview_distance), style = textStyle.copy(fontSize = 13.sp))
- Spacer(Modifier.width(4.dp))
- Text("•", color = Color.Gray, fontSize = 12.sp)
- Spacer(Modifier.width(4.dp))
- Text(stringResource(R.string.nav_preview_time), style = timeStyle.copy(fontSize = 13.sp))
- }
- }
- NavContent.NONE -> { /* Empty */ }
- }
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun NavDropdown(label: String, selected: NavContent, onSelect: (NavContent) -> Unit) {
- var expanded by remember { mutableStateOf(false) }
- Column {
- Text(label, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.primary)
- Spacer(Modifier.height(4.dp))
- ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = { expanded = it }) {
- OutlinedTextField(
- value = stringResource(getNavContentLabelRes(selected)),
- onValueChange = {},
- readOnly = true,
- trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
- modifier = Modifier.menuAnchor().fillMaxWidth(),
- textStyle = MaterialTheme.typography.bodyMedium
- )
- ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
- NavContent.entries.forEach { option ->
- DropdownMenuItem(
- text = { Text(stringResource(getNavContentLabelRes(option))) },
- onClick = { onSelect(option); expanded = false }
+ Column(Modifier.padding(16.dp)) {
+ Text(
+ text = stringResource(R.string.good_to_know),
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.primary,
+ style = MaterialTheme.typography.titleSmall
+ )
+ Spacer(Modifier.height(4.dp))
+ Text(
+ text = stringResource(R.string.nav_layout_info),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSecondaryContainer
)
}
}
+ Spacer(modifier = Modifier.height(32.dp))
}
}
}
-
-private fun getNavContentLabelRes(content: NavContent): Int {
- return when(content) {
- NavContent.INSTRUCTION -> R.string.nav_content_instruction
- NavContent.DISTANCE -> R.string.nav_content_distance
- NavContent.ETA -> R.string.nav_content_eta
- NavContent.DISTANCE_ETA -> R.string.nav_content_distance_eta
- NavContent.NONE -> R.string.nav_content_none
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/PermanentIslandConfigScreen.kt b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/PermanentIslandConfigScreen.kt
new file mode 100644
index 0000000..448c3d2
--- /dev/null
+++ b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/settings/PermanentIslandConfigScreen.kt
@@ -0,0 +1,181 @@
+package com.d4viddf.hyperbridge.ui.screens.settings
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FilledTonalIconButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.d4viddf.hyperbridge.R
+import com.d4viddf.hyperbridge.data.AppPreferences
+import androidx.compose.material3.Switch
+import androidx.compose.material3.Slider
+import androidx.compose.foundation.layout.Row
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.ui.Alignment
+import kotlinx.coroutines.launch
+import androidx.compose.ui.tooling.preview.Preview
+import com.d4viddf.hyperbridge.ui.theme.HyperBridgeTheme
+import com.d4viddf.hyperbridge.ui.components.PermanentIslandPreview
+
+@Composable
+fun PermanentIslandConfigScreen(
+ onBack: () -> Unit
+) {
+ val context = LocalContext.current
+ val prefs = AppPreferences(context)
+ val scope = rememberCoroutineScope()
+
+ val isEnabled by prefs.isPermanentIslandEnabledFlow.collectAsState(initial = false)
+ val islandWidth by prefs.permanentIslandWidthFlow.collectAsState(initial = 0)
+
+ PermanentIslandConfigContent(
+ isEnabled = isEnabled,
+ islandWidth = islandWidth,
+ onEnabledChange = { checked ->
+ scope.launch {
+ prefs.setPermanentIslandEnabled(checked)
+ }
+ },
+ onWidthChange = { width ->
+ scope.launch {
+ prefs.setPermanentIslandWidth(width)
+ }
+ },
+ onBack = onBack
+ )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun PermanentIslandConfigContent(
+ isEnabled: Boolean,
+ islandWidth: Int,
+ onEnabledChange: (Boolean) -> Unit,
+ onWidthChange: (Int) -> Unit,
+ onBack: () -> Unit
+) {
+ var sliderValue by androidx.compose.runtime.remember { androidx.compose.runtime.mutableFloatStateOf(0f) }
+ var isDragging by androidx.compose.runtime.remember { androidx.compose.runtime.mutableStateOf(false) }
+
+ androidx.compose.runtime.LaunchedEffect(islandWidth) {
+ if (!isDragging) {
+ sliderValue = islandWidth.toFloat()
+ }
+ }
+
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text(stringResource(R.string.permanent_island_title)) },
+ navigationIcon = {
+ FilledTonalIconButton(onClick = onBack) {
+ Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.back))
+ }
+ }
+ )
+ }
+ ) { padding ->
+ Column(
+ modifier = Modifier
+ .padding(padding)
+ .padding(16.dp)
+ .fillMaxSize()
+ ) {
+ PermanentIslandPreview(islandWidthValue = if (isDragging) sliderValue.toInt() else islandWidth)
+
+ Spacer(Modifier.height(24.dp))
+
+
+
+ Card(
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer),
+ shape = androidx.compose.foundation.shape.RoundedCornerShape(24.dp),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Column(modifier = Modifier.padding(horizontal = 20.dp, vertical = 16.dp)) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = stringResource(R.string.permanent_island_title),
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = androidx.compose.ui.text.font.FontWeight.Medium
+ )
+
+ }
+ Switch(
+ checked = isEnabled,
+ onCheckedChange = onEnabledChange
+ )
+ }
+
+ if (isEnabled) {
+ Spacer(Modifier.height(16.dp))
+ Text(
+ text = stringResource(R.string.permanent_island_width),
+ style = MaterialTheme.typography.titleSmall,
+ fontWeight = androidx.compose.ui.text.font.FontWeight.Medium
+ )
+ Slider(
+ value = sliderValue,
+ onValueChange = { value ->
+ isDragging = true
+ sliderValue = value
+ onWidthChange(value.toInt())
+ },
+ onValueChangeFinished = {
+ isDragging = false
+ },
+ valueRange = 0f..20f
+ )
+ }
+ }
+ }
+ Spacer(Modifier.height(24.dp))
+ Text(
+ text = stringResource(R.string.permanent_island_screen_desc),
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun PermanentIslandConfigScreenPreview() {
+ HyperBridgeTheme {
+ PermanentIslandConfigContent(
+ isEnabled = true,
+ islandWidth = 10,
+ onEnabledChange = {},
+ onWidthChange = {},
+ onBack = {}
+ )
+ }
+}
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/theme/AppThemeEditorScreen.kt b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/theme/AppThemeEditorScreen.kt
index 3ba0f27..6249da4 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/theme/AppThemeEditorScreen.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/theme/AppThemeEditorScreen.kt
@@ -2,8 +2,11 @@ package com.d4viddf.hyperbridge.ui.screens.theme
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
+import androidx.compose.animation.shrinkVertically
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
@@ -26,6 +29,9 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.outlined.Call
import androidx.compose.material.icons.outlined.ColorLens
+import androidx.compose.material.icons.outlined.DisplaySettings
+import androidx.compose.material.icons.outlined.Map
+import androidx.compose.material.icons.outlined.NotificationsActive
import androidx.compose.material.icons.outlined.TouchApp
import androidx.compose.material.icons.outlined.Widgets
import androidx.compose.material3.AlertDialog
@@ -52,18 +58,26 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.d4viddf.hyperbridge.R
+import com.d4viddf.hyperbridge.models.NotificationType
+import com.d4viddf.hyperbridge.ui.components.SettingsToggleCard
+import com.d4viddf.hyperbridge.ui.screens.settings.NavCustomizationScreen
import com.d4viddf.hyperbridge.ui.screens.theme.content.ActionConfigSheet
import com.d4viddf.hyperbridge.ui.screens.theme.content.ActionsDetailContent
+import com.d4viddf.hyperbridge.ui.screens.theme.content.BehaviourMenuContent
import com.d4viddf.hyperbridge.ui.screens.theme.content.CallStyleSheetContent
import com.d4viddf.hyperbridge.ui.screens.theme.content.ColorsDetailContent
+import com.d4viddf.hyperbridge.ui.screens.theme.content.EngineThemeContent
import com.d4viddf.hyperbridge.ui.screens.theme.content.IconsDetailContent
-import com.d4viddf.hyperbridge.ui.screens.theme.content.SharedThemePreview
+import com.d4viddf.hyperbridge.ui.screens.theme.content.NotificationTypesContent
+import com.d4viddf.hyperbridge.ui.screens.theme.content.ThemeBehaviourContent
import com.d4viddf.hyperbridge.ui.screens.theme.content.safeParseColor
+import com.d4viddf.hyperbridge.ui.theme.HyperBridgeTheme
enum class AppEditorRoute {
- MENU, COLORS, ICONS, CALLS, ACTIONS
+ MENU, BEHAVIOR_MENU, BEHAVIOR_ENGINE, BEHAVIOR_ISLAND, BEHAVIOR_TYPES, COLORS, ICONS, CALLS, NAVIGATION, ACTIONS
}
@OptIn(ExperimentalMaterial3Api::class)
@@ -74,12 +88,12 @@ fun AppThemeEditor(viewModel: ThemeViewModel) {
// Logic to handle "Back" request
val handleBack = {
- if (currentRoute != AppEditorRoute.MENU) {
- // If in a sub-menu, just go back to App Menu
- currentRoute = AppEditorRoute.MENU
- } else {
- // If at App Menu root, ask to save before exiting
- showUnsavedDialog = true
+ when (currentRoute) {
+ AppEditorRoute.BEHAVIOR_ENGINE,
+ AppEditorRoute.BEHAVIOR_ISLAND,
+ AppEditorRoute.BEHAVIOR_TYPES -> currentRoute = AppEditorRoute.BEHAVIOR_MENU // Go back to sub-menu
+ AppEditorRoute.MENU -> showUnsavedDialog = true // Ask to save before exiting
+ else -> currentRoute = AppEditorRoute.MENU // Go back to main app menu
}
}
@@ -96,9 +110,14 @@ fun AppThemeEditor(viewModel: ThemeViewModel) {
Text(
text = if (currentRoute == AppEditorRoute.MENU) stringResource(R.string.edit_app) else stringResource(
when (currentRoute) {
+ AppEditorRoute.BEHAVIOR_MENU -> R.string.behaviour_triggers
+ AppEditorRoute.BEHAVIOR_ENGINE -> R.string.engine
+ AppEditorRoute.BEHAVIOR_ISLAND -> R.string.island_behavior
+ AppEditorRoute.BEHAVIOR_TYPES -> R.string.active_notifications_title
AppEditorRoute.COLORS -> R.string.creator_nav_colors
AppEditorRoute.ICONS -> R.string.creator_nav_icons
AppEditorRoute.CALLS -> R.string.creator_nav_calls
+ AppEditorRoute.NAVIGATION -> R.string.nav_layout_title
AppEditorRoute.ACTIONS -> R.string.creator_nav_actions
else -> R.string.app_name
}
@@ -145,7 +164,7 @@ fun AppThemeEditor(viewModel: ThemeViewModel) {
AnimatedContent(
targetState = currentRoute,
transitionSpec = {
- if (targetState == AppEditorRoute.MENU) {
+ if (targetState == AppEditorRoute.MENU || targetState == AppEditorRoute.BEHAVIOR_MENU && initialState != AppEditorRoute.MENU) {
slideInHorizontally { -it } + fadeIn() togetherWith slideOutHorizontally { it } + fadeOut()
} else {
slideInHorizontally { it } + fadeIn() togetherWith slideOutHorizontally { -it } + fadeOut()
@@ -166,6 +185,28 @@ fun AppThemeEditor(viewModel: ThemeViewModel) {
viewModel = viewModel,
onNavigate = { currentRoute = it }
)
+ AppEditorRoute.BEHAVIOR_MENU -> Box(Modifier.fillMaxSize()) {
+ BehaviourMenuContent(onNavigate = {
+ currentRoute = when (it) {
+ CreatorRoute.BEHAVIOR_ENGINE -> AppEditorRoute.BEHAVIOR_ENGINE
+ CreatorRoute.BEHAVIOR_ISLAND -> AppEditorRoute.BEHAVIOR_ISLAND
+ CreatorRoute.BEHAVIOR_TYPES -> AppEditorRoute.BEHAVIOR_TYPES
+ else -> AppEditorRoute.BEHAVIOR_MENU
+ }
+ })
+ }
+ AppEditorRoute.BEHAVIOR_ENGINE -> Box(Modifier.fillMaxSize()) {
+ EngineThemeContent(
+ isNative = viewModel.appUseNativeLiveUpdates,
+ onEngineChange = { viewModel.appUseNativeLiveUpdates = it }
+ )
+ }
+ AppEditorRoute.BEHAVIOR_ISLAND -> Box(Modifier.fillMaxSize()) {
+ ThemeBehaviourContent() // Still uses standard island behavior
+ }
+ AppEditorRoute.BEHAVIOR_TYPES -> Box(Modifier.fillMaxSize()) {
+ AppNotificationTypesEditor(viewModel = viewModel)
+ }
AppEditorRoute.COLORS -> DetailScreenShell(
previewContent = { SharedThemePreview(effHighlight, false, effShape, effPadding, effAnswerColor, effDeclineColor, effAnswerShape, effDeclineShape) },
content = {
@@ -184,6 +225,13 @@ fun AppThemeEditor(viewModel: ThemeViewModel) {
AppCallEditor(viewModel)
}
)
+ AppEditorRoute.NAVIGATION -> Box(Modifier.fillMaxSize()) {
+ NavCustomizationScreen(
+ onBack = { currentRoute = AppEditorRoute.MENU },
+ packageName = viewModel.editingAppPackage,
+ showTopBar = false
+ )
+ }
AppEditorRoute.ACTIONS -> Box(Modifier.fillMaxSize()) {
AppActionEditor(viewModel)
}
@@ -266,15 +314,24 @@ fun AppEditorMenu(
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
val items = listOf(
+ AppEditorRoute.BEHAVIOR_MENU,
AppEditorRoute.COLORS,
AppEditorRoute.ICONS,
AppEditorRoute.CALLS,
+ AppEditorRoute.NAVIGATION,
AppEditorRoute.ACTIONS
)
items.forEachIndexed { index, route ->
val shape = getExpressiveShape(items.size, index, ShapeStyle.Large)
when(route) {
+ AppEditorRoute.BEHAVIOR_MENU -> CreatorOptionCard(
+ title = stringResource(R.string.engine),
+ subtitle = stringResource(R.string.engine_timeouts_triggers),
+ icon = Icons.Outlined.DisplaySettings,
+ shape = shape,
+ onClick = { onNavigate(route) }
+ )
AppEditorRoute.COLORS -> CreatorOptionCard(
title = stringResource(R.string.creator_nav_colors),
subtitle = if (viewModel.appHighlightColor != null) stringResource(R.string.creator_sub_colors_custom) else stringResource(R.string.creator_sub_colors_default),
@@ -303,6 +360,13 @@ fun AppEditorMenu(
shape = shape,
onClick = { onNavigate(route) }
)
+ AppEditorRoute.NAVIGATION -> CreatorOptionCard(
+ title = stringResource(R.string.nav_layout_title),
+ subtitle = stringResource(R.string.nav_layout_desc),
+ icon = Icons.Outlined.Map,
+ shape = shape,
+ onClick = { onNavigate(route) }
+ )
AppEditorRoute.ACTIONS -> CreatorOptionCard(
title = stringResource(R.string.creator_nav_actions),
subtitle = pluralStringResource(
@@ -385,4 +449,171 @@ fun AppActionEditor(viewModel: ThemeViewModel) {
}
)
}
+}
+
+
+// ========================================================================
+// APP-SPECIFIC BEHAVIOR EDITORS
+// ========================================================================
+
+@Composable
+fun AppNotificationTypesEditor(viewModel: ThemeViewModel) {
+ // If it's not null, it means the user is overriding the global setting
+ var isOverride by remember { mutableStateOf(viewModel.appEnabledNotificationTypes != null) }
+ val activeTypes = viewModel.appEnabledNotificationTypes ?: emptySet()
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ .padding(16.dp)
+ ) {
+ Text(stringResource(R.string.trigger_override), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
+ Text(stringResource(R.string.trigger_app_desc), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
+ Spacer(Modifier.height(16.dp))
+
+ SettingsToggleCard(
+ title = stringResource(R.string.override_global_triggers),
+ subtitle = stringResource(R.string.override_global_trigger_desc),
+ icon = Icons.Outlined.NotificationsActive,
+ checked = isOverride,
+ onCheckedChange = { checked ->
+ isOverride = checked
+ // If turning on, default to everything enabled for this app
+ viewModel.appEnabledNotificationTypes = if (checked) NotificationType.entries.map { it.name }.toSet() else null
+ },
+ shape = RoundedCornerShape(24.dp)
+ )
+
+ AnimatedVisibility(
+ visible = isOverride,
+ enter = expandVertically() + fadeIn(),
+ exit = shrinkVertically() + fadeOut()
+ ) {
+ Column(Modifier.padding(top = 16.dp)) {
+ NotificationType.entries.forEachIndexed { index, type ->
+ val shape = when {
+ NotificationType.entries.size == 1 -> RoundedCornerShape(24.dp)
+ index == 0 -> RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp, bottomStart = 4.dp, bottomEnd = 4.dp)
+ index == NotificationType.entries.size - 1 -> RoundedCornerShape(topStart = 4.dp, topEnd = 4.dp, bottomStart = 24.dp, bottomEnd = 24.dp)
+ else -> RoundedCornerShape(4.dp)
+ }
+
+ // You can map the exact icons and subtitles here as you did in ThemeSettingsContent
+ SettingsToggleCard(
+ title = stringResource(type.labelRes),
+ subtitle = stringResource(R.string.enable_triggers),
+ icon = Icons.Outlined.TouchApp, // Update this with your specific type icons if desired
+ checked = activeTypes.contains(type.name),
+ onCheckedChange = { isChecked ->
+ val newSet = if (isChecked) activeTypes + type.name else activeTypes - type.name
+ viewModel.appEnabledNotificationTypes = newSet
+ },
+ shape = shape
+ )
+
+ if (index < NotificationType.entries.size - 1) {
+ Spacer(modifier = Modifier.height(2.dp))
+ }
+ }
+ }
+ }
+ }
+}
+
+// ========================================================================
+// PREVIEWS
+// ========================================================================
+
+@Preview(showBackground = true, name = "1. App Editor Menu (Light)")
+@Composable
+fun PreviewAppEditorMenuLight() {
+ HyperBridgeTheme(darkTheme = false) {
+ Box(modifier = Modifier
+ .fillMaxSize()
+ .background(MaterialTheme.colorScheme.background)) {
+ BehaviourMenuContent(onNavigate = {})
+ }
+ }
+}
+
+@Preview(showBackground = true, name = "2. App Editor Menu (Dark)")
+@Composable
+fun PreviewAppEditorMenuDark() {
+ HyperBridgeTheme(darkTheme = true) {
+ Box(modifier = Modifier
+ .fillMaxSize()
+ .background(MaterialTheme.colorScheme.background)) {
+ BehaviourMenuContent(onNavigate = {})
+ }
+ }
+}
+
+@Preview(showBackground = true, name = "3. Notification Types (Light)")
+@Composable
+fun PreviewNotificationTypesContentLight() {
+ HyperBridgeTheme(darkTheme = false) {
+ Box(modifier = Modifier
+ .fillMaxSize()
+ .background(MaterialTheme.colorScheme.background)) {
+ NotificationTypesContent()
+ }
+ }
+}
+
+@Preview(showBackground = true, name = "4. Notification Types (Dark)")
+@Composable
+fun PreviewNotificationTypesContentDark() {
+ HyperBridgeTheme(darkTheme = true) {
+ Box(modifier = Modifier
+ .fillMaxSize()
+ .background(MaterialTheme.colorScheme.background)) {
+ NotificationTypesContent()
+ }
+ }
+}
+
+@Preview(showBackground = true, name = "5. Engine - Default Inherit (Light)")
+@Composable
+fun PreviewEngineThemeContentDefault() {
+ HyperBridgeTheme(darkTheme = false) {
+ Box(modifier = Modifier
+ .fillMaxSize()
+ .background(MaterialTheme.colorScheme.background)) {
+ EngineThemeContent(
+ isNative = null, // Demonstrates the Default option selected
+ onEngineChange = {}
+ )
+ }
+ }
+}
+
+@Preview(showBackground = true, name = "6. Engine - Native Live Updates (Dark)")
+@Composable
+fun PreviewEngineThemeContentNativeDark() {
+ HyperBridgeTheme(darkTheme = true) {
+ Box(modifier = Modifier
+ .fillMaxSize()
+ .background(MaterialTheme.colorScheme.background)) {
+ EngineThemeContent(
+ isNative = true, // Demonstrates the Native option selected
+ onEngineChange = {}
+ )
+ }
+ }
+}
+
+@Preview(showBackground = true, name = "7. Engine - Xiaomi Custom (Light)")
+@Composable
+fun PreviewEngineThemeContentXiaomiLight() {
+ HyperBridgeTheme(darkTheme = false) {
+ Box(modifier = Modifier
+ .fillMaxSize()
+ .background(MaterialTheme.colorScheme.background)) {
+ EngineThemeContent(
+ isNative = false, // Demonstrates the Xiaomi option selected
+ onEngineChange = {}
+ )
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/theme/ThemeCreatorScreen.kt b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/theme/ThemeCreatorScreen.kt
index 3295f9d..fa16efb 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/theme/ThemeCreatorScreen.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/theme/ThemeCreatorScreen.kt
@@ -41,8 +41,10 @@ import androidx.compose.material.icons.automirrored.rounded.ArrowForwardIos
import androidx.compose.material.icons.outlined.Apps
import androidx.compose.material.icons.outlined.Call
import androidx.compose.material.icons.outlined.ColorLens
+import androidx.compose.material.icons.outlined.DisplaySettings
import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material.icons.outlined.Image
+import androidx.compose.material.icons.outlined.Map
import androidx.compose.material.icons.outlined.TouchApp
import androidx.compose.material.icons.outlined.Widgets
import androidx.compose.material.icons.rounded.Delete
@@ -91,18 +93,23 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.d4viddf.hyperbridge.R
import com.d4viddf.hyperbridge.models.theme.ColorMode
+import com.d4viddf.hyperbridge.ui.screens.settings.NavCustomizationScreen
import com.d4viddf.hyperbridge.ui.screens.theme.content.ActionsDetailContent
import com.d4viddf.hyperbridge.ui.screens.theme.content.AppsDetailContent
+import com.d4viddf.hyperbridge.ui.screens.theme.content.BehaviourMenuContent
import com.d4viddf.hyperbridge.ui.screens.theme.content.CallStyleSheetContent
import com.d4viddf.hyperbridge.ui.screens.theme.content.ColorsDetailContent
+import com.d4viddf.hyperbridge.ui.screens.theme.content.EngineThemeContent
import com.d4viddf.hyperbridge.ui.screens.theme.content.IconsDetailContent
-import com.d4viddf.hyperbridge.ui.screens.theme.content.SharedThemePreview
+import com.d4viddf.hyperbridge.ui.screens.theme.content.NotificationTypesContent
+import com.d4viddf.hyperbridge.ui.screens.theme.content.ThemeBehaviourContent
import com.d4viddf.hyperbridge.ui.screens.theme.content.safeParseColor
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
+// [NEW] Sub-menu routing added
enum class CreatorRoute {
- MAIN_MENU, COLORS, ICONS, CALLS, ACTIONS, APPS
+ MAIN_MENU, BEHAVIOR_MENU, BEHAVIOR_ENGINE, BEHAVIOR_ISLAND, BEHAVIOR_TYPES, COLORS, ICONS, CALLS, NAVIGATION, ACTIONS, APPS
}
@OptIn(ExperimentalMaterial3Api::class)
@@ -114,9 +121,12 @@ fun ThemeCreatorScreen(
) {
val viewModel: ThemeViewModel = viewModel()
val activeThemeId by viewModel.activeThemeId.collectAsState()
+ val isNative by viewModel.useNativeLiveUpdates.collectAsState()
if (viewModel.editingAppPackage != null) {
- AppThemeEditor(viewModel)
+ AppThemeEditor(
+ viewModel = viewModel
+ )
} else {
var currentRoute by remember { mutableStateOf(CreatorRoute.MAIN_MENU) }
var showSettingsSheet by remember { mutableStateOf(false) }
@@ -134,8 +144,27 @@ fun ThemeCreatorScreen(
}
}
- BackHandler(enabled = currentRoute != CreatorRoute.MAIN_MENU) {
- currentRoute = CreatorRoute.MAIN_MENU
+ // --- Shared Back Logic ---
+ val handleBackNavigation = {
+ when (currentRoute) {
+ CreatorRoute.BEHAVIOR_ENGINE,
+ CreatorRoute.BEHAVIOR_ISLAND,
+ CreatorRoute.BEHAVIOR_TYPES -> currentRoute = CreatorRoute.BEHAVIOR_MENU // Return to sub-menu
+ CreatorRoute.MAIN_MENU -> {
+ viewModel.currentEditingThemeId = null
+ onBack() // Exit entirely
+ }
+ else -> currentRoute = CreatorRoute.MAIN_MENU // Return to main menu
+ }
+ }
+
+ BackHandler(enabled = true) {
+ handleBackNavigation()
+ }
+
+ BackHandler(enabled = currentRoute == CreatorRoute.MAIN_MENU) {
+ viewModel.currentEditingThemeId = null
+ onBack()
}
BackHandler(enabled = currentRoute == CreatorRoute.MAIN_MENU) {
@@ -150,9 +179,14 @@ fun ThemeCreatorScreen(
Text(
text = when (currentRoute) {
CreatorRoute.MAIN_MENU -> if (editThemeId == null) stringResource(R.string.creator_title_new) else stringResource(R.string.creator_title_edit)
+ CreatorRoute.BEHAVIOR_MENU -> stringResource(R.string.behaviour_triggers)
+ CreatorRoute.BEHAVIOR_ENGINE -> stringResource(R.string.engine)
+ CreatorRoute.BEHAVIOR_ISLAND -> stringResource(R.string.island_behavior)
+ CreatorRoute.BEHAVIOR_TYPES -> stringResource(R.string.active_notifications_title)
CreatorRoute.COLORS -> stringResource(R.string.creator_nav_colors)
CreatorRoute.ICONS -> stringResource(R.string.creator_nav_icons)
CreatorRoute.CALLS -> stringResource(R.string.creator_nav_calls)
+ CreatorRoute.NAVIGATION -> stringResource(R.string.nav_layout_title)
CreatorRoute.ACTIONS -> stringResource(R.string.creator_nav_actions)
CreatorRoute.APPS -> stringResource(R.string.creator_nav_apps)
},
@@ -161,14 +195,7 @@ fun ThemeCreatorScreen(
},
navigationIcon = {
FilledTonalIconButton(
- onClick = {
- if (currentRoute != CreatorRoute.MAIN_MENU) {
- currentRoute = CreatorRoute.MAIN_MENU
- } else {
- viewModel.currentEditingThemeId = null
- onBack()
- }
- },
+ onClick = handleBackNavigation,
colors = IconButtonDefaults.filledTonalIconButtonColors(containerColor = MaterialTheme.colorScheme.surfaceContainerHighest)
) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back))
@@ -179,7 +206,7 @@ fun ThemeCreatorScreen(
Button(
onClick = {
if (editThemeId != null && editThemeId == activeThemeId) {
- viewModel.saveTheme(editThemeId)
+ viewModel.saveTheme(existingId = editThemeId, apply = true)
viewModel.currentEditingThemeId = null
onThemeCreated()
} else {
@@ -196,12 +223,16 @@ fun ThemeCreatorScreen(
},
containerColor = MaterialTheme.colorScheme.surface
) { padding ->
- Box(modifier = Modifier.padding(top = padding.calculateTopPadding()).fillMaxSize()) {
+ Box(modifier = Modifier
+ .padding(top = padding.calculateTopPadding())
+ .fillMaxSize()) {
AnimatedContent(
targetState = currentRoute,
transitionSpec = {
- if (targetState == CreatorRoute.MAIN_MENU) slideInHorizontally { -it } + fadeIn() togetherWith slideOutHorizontally { it } + fadeOut()
- else slideInHorizontally { it } + fadeIn() togetherWith slideOutHorizontally { -it } + fadeOut()
+ if (targetState == CreatorRoute.MAIN_MENU || targetState == CreatorRoute.BEHAVIOR_MENU && initialState != CreatorRoute.MAIN_MENU)
+ slideInHorizontally { -it } + fadeIn() togetherWith slideOutHorizontally { it } + fadeOut()
+ else
+ slideInHorizontally { it } + fadeIn() togetherWith slideOutHorizontally { -it } + fadeOut()
},
label = "CreatorNav"
) { route ->
@@ -211,11 +242,29 @@ fun ThemeCreatorScreen(
onNavigate = { currentRoute = it },
onEditSettings = { showSettingsSheet = true }
)
+ CreatorRoute.BEHAVIOR_MENU -> Box(Modifier.fillMaxSize()) {
+ BehaviourMenuContent(onNavigate = { currentRoute = it })
+ }
+ CreatorRoute.BEHAVIOR_ENGINE -> {
+ EngineThemeContent(
+ isNative = isNative,
+ showDefaultOption = false,
+ onEngineChange = { viewModel.setUseNativeLiveUpdates(it ?: false) }
+ )
+ }
+ CreatorRoute.BEHAVIOR_ISLAND -> Box(Modifier.fillMaxSize()) {
+ ThemeBehaviourContent()
+ }
+ CreatorRoute.BEHAVIOR_TYPES -> Box(Modifier.fillMaxSize()) {
+ NotificationTypesContent()
+ }
+ CreatorRoute.NAVIGATION -> Box(Modifier.fillMaxSize()) {
+ NavCustomizationScreen(onBack = { currentRoute = CreatorRoute.MAIN_MENU },null, false)
+ }
CreatorRoute.COLORS -> DetailScreenShell(
previewContent = {
SharedThemePreview(
highlightColorHex = viewModel.selectedColorHex,
- // [FIX 1] Convert ColorMode Enum to boolean for the preview
useAppColors = (viewModel.colorMode == ColorMode.APP_ICON),
shapeId = viewModel.selectedShapeId,
paddingPercent = viewModel.iconPaddingPercent,
@@ -228,7 +277,6 @@ fun ThemeCreatorScreen(
content = {
ColorsDetailContent(
selectedColorHex = viewModel.selectedColorHex,
- // [FIX 2] Pass ColorMode Enum instead of boolean
colorMode = viewModel.colorMode,
onColorSelected = { viewModel.selectedColorHex = it },
onColorModeChanged = { viewModel.colorMode = it }
@@ -239,7 +287,6 @@ fun ThemeCreatorScreen(
previewContent = {
SharedThemePreview(
highlightColorHex = viewModel.selectedColorHex,
- // [FIX 3] Convert ColorMode Enum to boolean
useAppColors = (viewModel.colorMode == ColorMode.APP_ICON),
shapeId = viewModel.selectedShapeId,
paddingPercent = viewModel.iconPaddingPercent,
@@ -263,7 +310,6 @@ fun ThemeCreatorScreen(
previewContent = {
SharedThemePreview(
highlightColorHex = viewModel.selectedColorHex,
- // [FIX 4] Convert ColorMode Enum to boolean
useAppColors = (viewModel.colorMode == ColorMode.APP_ICON),
shapeId = viewModel.selectedShapeId,
paddingPercent = viewModel.iconPaddingPercent,
@@ -317,16 +363,16 @@ fun SaveDialog(viewModel: ThemeViewModel, editThemeId: String?, activeThemeId: S
confirmButton = {
Button(onClick = {
onDismiss()
- viewModel.saveTheme(editThemeId, apply = true)
- viewModel.currentEditingThemeId = null // Clear state!
+ viewModel.saveTheme(existingId = editThemeId, apply = true)
+ viewModel.currentEditingThemeId = null
onThemeCreated()
}) { Text(stringResource(R.string.creator_dialog_action_save_apply)) }
},
dismissButton = {
TextButton(onClick = {
onDismiss()
- viewModel.saveTheme(editThemeId, apply = false)
- viewModel.currentEditingThemeId = null // Clear state!
+ viewModel.saveTheme(existingId = editThemeId, apply = false)
+ viewModel.currentEditingThemeId = null
onThemeCreated()
}) { Text(stringResource(R.string.creator_dialog_action_save_only)) }
}
@@ -334,13 +380,18 @@ fun SaveDialog(viewModel: ThemeViewModel, editThemeId: String?, activeThemeId: S
}
@Composable
fun CreatorMainList(viewModel: ThemeViewModel, onNavigate: (CreatorRoute) -> Unit, onEditSettings: () -> Unit) {
- Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())) {
- Box(modifier = Modifier.fillMaxWidth().height(200.dp), contentAlignment = Alignment.Center) {
- Surface(modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 16.dp), shape = RoundedCornerShape(24.dp), color = MaterialTheme.colorScheme.surfaceContainer) {
+ Column(modifier = Modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())) {
+ Box(modifier = Modifier
+ .fillMaxWidth()
+ .height(200.dp), contentAlignment = Alignment.Center) {
+ Surface(modifier = Modifier
+ .fillMaxWidth()
+ .padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 16.dp), shape = RoundedCornerShape(24.dp), color = MaterialTheme.colorScheme.surfaceContainer) {
Box(contentAlignment = Alignment.Center, modifier = Modifier.padding(vertical = 12.dp)) {
SharedThemePreview(
viewModel.selectedColorHex,
- // [FIX 5] Convert ColorMode Enum to boolean
(viewModel.colorMode == ColorMode.APP_ICON),
viewModel.selectedShapeId, viewModel.iconPaddingPercent,
viewModel.callAnswerColor, viewModel.callDeclineColor, viewModel.callAnswerShapeId, viewModel.callDeclineShapeId
@@ -349,13 +400,25 @@ fun CreatorMainList(viewModel: ThemeViewModel, onNavigate: (CreatorRoute) -> Uni
}
}
- Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp)) {
- Button(onClick = onEditSettings, modifier = Modifier.fillMaxWidth().height(52.dp), shape = RoundedCornerShape(24.dp), colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer, contentColor = MaterialTheme.colorScheme.onSecondaryContainer), elevation = ButtonDefaults.buttonElevation(defaultElevation = 0.dp)) {
+ Column(modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp)) {
+ Button(onClick = onEditSettings, modifier = Modifier
+ .fillMaxWidth()
+ .height(52.dp), shape = RoundedCornerShape(24.dp), colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer, contentColor = MaterialTheme.colorScheme.onSecondaryContainer), elevation = ButtonDefaults.buttonElevation(defaultElevation = 0.dp)) {
Icon(Icons.Outlined.Edit, null, modifier = Modifier.size(18.dp)); Spacer(Modifier.width(8.dp)); Text(stringResource(R.string.creator_btn_edit_info))
}
Spacer(Modifier.height(16.dp))
- val menuItems = CreatorRoute.entries.filter { it != CreatorRoute.MAIN_MENU }
+ val menuItems = listOf(
+ CreatorRoute.BEHAVIOR_MENU,
+ CreatorRoute.COLORS,
+ CreatorRoute.ICONS,
+ CreatorRoute.CALLS,
+ CreatorRoute.NAVIGATION,
+ CreatorRoute.ACTIONS,
+ CreatorRoute.APPS
+ )
Column(
modifier = Modifier.fillMaxWidth(),
@@ -363,15 +426,62 @@ fun CreatorMainList(viewModel: ThemeViewModel, onNavigate: (CreatorRoute) -> Uni
) {
menuItems.forEachIndexed { index, route ->
val shape = getExpressiveShape(menuItems.size, index, ShapeStyle.Large)
- val icon = when(route) { CreatorRoute.COLORS -> Icons.Outlined.ColorLens; CreatorRoute.ICONS -> Icons.Outlined.Widgets; CreatorRoute.CALLS -> Icons.Outlined.Call; CreatorRoute.ACTIONS -> Icons.Outlined.TouchApp; CreatorRoute.APPS -> Icons.Outlined.Apps; else -> Icons.Outlined.Image }
- val title = stringResource(when(route) { CreatorRoute.COLORS -> R.string.creator_nav_colors; CreatorRoute.ICONS -> R.string.creator_nav_icons; CreatorRoute.CALLS -> R.string.creator_nav_calls; CreatorRoute.ACTIONS -> R.string.creator_nav_actions; CreatorRoute.APPS -> R.string.creator_nav_apps; else -> R.string.app_name })
- val sub = stringResource(when(route) { CreatorRoute.COLORS -> R.string.creator_sub_colors; CreatorRoute.ICONS -> R.string.creator_sub_icons; CreatorRoute.CALLS -> R.string.creator_sub_calls; CreatorRoute.ACTIONS -> R.string.creator_sub_actions; CreatorRoute.APPS -> R.string.creator_sub_apps; else -> R.string.app_name })
- val trailing: (@Composable () -> Unit)? = if (route == CreatorRoute.COLORS) {
- { Box(modifier = Modifier.size(28.dp).clip(CircleShape).background(safeParseColor(viewModel.selectedColorHex)).border(1.dp, MaterialTheme.colorScheme.outlineVariant, CircleShape)) }
- } else if (route == CreatorRoute.ICONS) {
- { Icon(Icons.Outlined.Image, null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(24.dp)) }
- } else null
+ val icon = when(route) {
+ CreatorRoute.BEHAVIOR_MENU -> Icons.Outlined.DisplaySettings
+ CreatorRoute.COLORS -> Icons.Outlined.ColorLens
+ CreatorRoute.ICONS -> Icons.Outlined.Widgets
+ CreatorRoute.CALLS -> Icons.Outlined.Call
+ CreatorRoute.NAVIGATION -> Icons.Outlined.Map
+ CreatorRoute.ACTIONS -> Icons.Outlined.TouchApp
+ CreatorRoute.APPS -> Icons.Outlined.Apps
+ else -> Icons.Outlined.Image
+ }
+ val title = when(route) {
+ CreatorRoute.BEHAVIOR_MENU -> stringResource(R.string.engine)
+ CreatorRoute.COLORS -> stringResource(R.string.creator_nav_colors)
+ CreatorRoute.ICONS -> stringResource(R.string.creator_nav_icons)
+ CreatorRoute.CALLS -> stringResource(R.string.creator_nav_calls)
+ CreatorRoute.NAVIGATION -> stringResource(R.string.nav_layout_title)
+ CreatorRoute.ACTIONS -> stringResource(R.string.creator_nav_actions)
+ CreatorRoute.APPS -> stringResource(R.string.creator_nav_apps)
+ else -> stringResource(R.string.app_name)
+ }
+ val sub = when(route) {
+ CreatorRoute.BEHAVIOR_MENU -> stringResource(R.string.engine_timeouts_triggers)
+ CreatorRoute.COLORS -> stringResource(R.string.creator_sub_colors)
+ CreatorRoute.ICONS -> stringResource(R.string.creator_sub_icons)
+ CreatorRoute.CALLS -> stringResource(R.string.creator_sub_calls)
+ CreatorRoute.NAVIGATION -> stringResource(R.string.nav_layout_desc)
+ CreatorRoute.ACTIONS -> stringResource(R.string.creator_sub_actions)
+ CreatorRoute.APPS -> stringResource(R.string.creator_sub_apps)
+ else -> stringResource(R.string.app_name)
+ }
+
+ val trailing: (@Composable () -> Unit)? = when (route) {
+ CreatorRoute.COLORS -> {
+ {
+ Box(
+ modifier = Modifier
+ .size(28.dp)
+ .clip(CircleShape)
+ .background(safeParseColor(viewModel.selectedColorHex))
+ .border(1.dp, MaterialTheme.colorScheme.outlineVariant, CircleShape)
+ )
+ }
+ }
+ CreatorRoute.ICONS -> {
+ {
+ Icon(
+ Icons.Outlined.Image,
+ null,
+ tint = MaterialTheme.colorScheme.primary,
+ modifier = Modifier.size(24.dp)
+ )
+ }
+ }
+ else -> null
+ }
CreatorOptionCard(title, sub, icon, shape, { onNavigate(route) }, trailing)
}
@@ -383,8 +493,12 @@ fun CreatorMainList(viewModel: ThemeViewModel, onNavigate: (CreatorRoute) -> Uni
@Composable
fun CreatorOptionCard(title: String, subtitle: String, icon: ImageVector, shape: Shape, onClick: () -> Unit, trailingContent: (@Composable () -> Unit)? = null) {
- Card(onClick = onClick, colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer), shape = shape, modifier = Modifier.fillMaxWidth().heightIn(min = 88.dp)) {
- Row(modifier = Modifier.fillMaxSize().padding(horizontal = 20.dp, vertical = 16.dp), verticalAlignment = Alignment.CenterVertically) {
+ Card(onClick = onClick, colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer), shape = shape, modifier = Modifier
+ .fillMaxWidth()
+ .heightIn(min = 88.dp)) {
+ Row(modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = 20.dp, vertical = 16.dp), verticalAlignment = Alignment.CenterVertically) {
Icon(icon, null, tint = MaterialTheme.colorScheme.onSurfaceVariant)
Spacer(Modifier.width(20.dp))
Column(modifier = Modifier.weight(1f)) {
@@ -399,12 +513,18 @@ fun CreatorOptionCard(title: String, subtitle: String, icon: ImageVector, shape:
@Composable
fun DetailScreenShell(previewContent: @Composable () -> Unit, content: @Composable () -> Unit) {
Column(modifier = Modifier.fillMaxSize()) {
- Box(modifier = Modifier.fillMaxWidth().height(200.dp), contentAlignment = Alignment.Center) {
- Surface(modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 16.dp), shape = RoundedCornerShape(24.dp), color = MaterialTheme.colorScheme.surfaceContainer) {
+ Box(modifier = Modifier
+ .fillMaxWidth()
+ .height(200.dp), contentAlignment = Alignment.Center) {
+ Surface(modifier = Modifier
+ .fillMaxWidth()
+ .padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 16.dp), shape = RoundedCornerShape(24.dp), color = MaterialTheme.colorScheme.surfaceContainer) {
Box(contentAlignment = Alignment.Center, modifier = Modifier.padding(vertical = 12.dp)) { previewContent() }
}
}
- Box(modifier = Modifier.weight(1f).fillMaxWidth()) { content() }
+ Box(modifier = Modifier
+ .weight(1f)
+ .fillMaxWidth()) { content() }
}
}
@@ -431,10 +551,18 @@ fun ThemeMetadataSheet(viewModel: ThemeViewModel, onDismiss: () -> Unit) {
}
ModalBottomSheet(onDismissRequest = onDismiss, sheetState = sheetState) {
- Column(modifier = Modifier.padding(24.dp).navigationBarsPadding().verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally) {
+ Column(modifier = Modifier
+ .padding(24.dp)
+ .navigationBarsPadding()
+ .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally) {
Text(stringResource(R.string.meta_title), style = MaterialTheme.typography.headlineSmall)
Spacer(Modifier.height(32.dp))
- Box(modifier = Modifier.size(120.dp).clip(RoundedCornerShape(24.dp)).background(MaterialTheme.colorScheme.surfaceContainerHigh).border(1.dp, MaterialTheme.colorScheme.outlineVariant, RoundedCornerShape(24.dp)).clickable { iconLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) }, contentAlignment = Alignment.Center) {
+ Box(modifier = Modifier
+ .size(120.dp)
+ .clip(RoundedCornerShape(24.dp))
+ .background(MaterialTheme.colorScheme.surfaceContainerHigh)
+ .border(1.dp, MaterialTheme.colorScheme.outlineVariant, RoundedCornerShape(24.dp))
+ .clickable { iconLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) }, contentAlignment = Alignment.Center) {
if (iconBitmap != null) { Image(bitmap = iconBitmap!!, contentDescription = null, modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop) }
else { Icon(Icons.Outlined.Image, null, tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.size(48.dp)) }
}
@@ -442,10 +570,14 @@ fun ThemeMetadataSheet(viewModel: ThemeViewModel, onDismiss: () -> Unit) {
if (viewModel.themeIconUri != null) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) {
FilledTonalButton(onClick = { viewModel.themeIconUri = null }, modifier = Modifier.size(50.dp), shape = RoundedCornerShape(14.dp), colors = ButtonDefaults.filledTonalButtonColors(containerColor = MaterialTheme.colorScheme.errorContainer, contentColor = MaterialTheme.colorScheme.onErrorContainer), contentPadding = PaddingValues(0.dp)) { Icon(Icons.Rounded.Delete, null, modifier = Modifier.size(20.dp)) }
- Button(onClick = { iconLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) }, modifier = Modifier.weight(1f).height(50.dp), shape = RoundedCornerShape(14.dp), colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer, contentColor = MaterialTheme.colorScheme.onSecondaryContainer)) { Icon(Icons.Rounded.Edit, null, modifier = Modifier.size(18.dp)); Spacer(Modifier.width(8.dp)); Text(stringResource(R.string.change_icon)) }
+ Button(onClick = { iconLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) }, modifier = Modifier
+ .weight(1f)
+ .height(50.dp), shape = RoundedCornerShape(14.dp), colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer, contentColor = MaterialTheme.colorScheme.onSecondaryContainer)) { Icon(Icons.Rounded.Edit, null, modifier = Modifier.size(18.dp)); Spacer(Modifier.width(8.dp)); Text(stringResource(R.string.change_icon)) }
}
} else {
- Button(onClick = { iconLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) }, modifier = Modifier.fillMaxWidth().height(50.dp), shape = RoundedCornerShape(14.dp), colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer, contentColor = MaterialTheme.colorScheme.onSecondaryContainer)) { Icon(Icons.Rounded.Image, null, modifier = Modifier.size(18.dp)); Spacer(Modifier.width(8.dp)); Text(stringResource(R.string.select_icon)) }
+ Button(onClick = { iconLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) }, modifier = Modifier
+ .fillMaxWidth()
+ .height(50.dp), shape = RoundedCornerShape(14.dp), colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer, contentColor = MaterialTheme.colorScheme.onSecondaryContainer)) { Icon(Icons.Rounded.Image, null, modifier = Modifier.size(18.dp)); Spacer(Modifier.width(8.dp)); Text(stringResource(R.string.select_icon)) }
}
Spacer(Modifier.height(32.dp))
OutlinedTextField(value = viewModel.themeName, onValueChange = { viewModel.themeName = it }, label = { Text(stringResource(R.string.meta_label_name)) }, modifier = Modifier.fillMaxWidth(), singleLine = true, keyboardActions = KeyboardActions(onDone = { fm.clearFocus() }))
@@ -454,7 +586,9 @@ fun ThemeMetadataSheet(viewModel: ThemeViewModel, onDismiss: () -> Unit) {
Spacer(Modifier.height(16.dp))
OutlinedTextField(value = viewModel.themeDescription, onValueChange = { viewModel.themeDescription = it }, label = { Text("Description") }, modifier = Modifier.fillMaxWidth(), minLines = 3, maxLines = 5)
Spacer(Modifier.height(32.dp))
- Button(onClick = onDismiss, modifier = Modifier.fillMaxWidth().height(50.dp), shape = ButtonDefaults.shape) { Text(stringResource(R.string.meta_action_done)) }
+ Button(onClick = onDismiss, modifier = Modifier
+ .fillMaxWidth()
+ .height(50.dp), shape = ButtonDefaults.shape) { Text(stringResource(R.string.meta_action_done)) }
Spacer(Modifier.height(24.dp))
}
}
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/theme/ThemeInstallerActivity.kt b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/theme/ThemeInstallerActivity.kt
index a7ff275..eb5ea86 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/theme/ThemeInstallerActivity.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/theme/ThemeInstallerActivity.kt
@@ -1,7 +1,6 @@
package com.d4viddf.hyperbridge.ui.screens.theme
import android.net.Uri
-import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
@@ -30,8 +29,6 @@ import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.darkColorScheme
-import androidx.compose.material3.dynamicDarkColorScheme
-import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -72,9 +69,6 @@ class ThemeInstallerActivity : ComponentActivity() {
val context = LocalContext.current
val colorScheme = when {
- Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
- if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
- }
darkTheme -> darkColorScheme()
else -> lightColorScheme()
}
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/theme/ThemePreviews.kt b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/theme/ThemePreviews.kt
index b619d49..919649f 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/theme/ThemePreviews.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/theme/ThemePreviews.kt
@@ -1,4 +1,4 @@
-package com.d4viddf.hyperbridge.ui.screens.theme.content
+package com.d4viddf.hyperbridge.ui.screens.theme
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
@@ -35,7 +35,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
-import com.d4viddf.hyperbridge.ui.screens.theme.getShapeFromId
+import com.d4viddf.hyperbridge.ui.screens.theme.content.safeParseColor
// --- SHARED PREVIEW CAROUSEL ---
@Composable
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/theme/ThemeShared.kt b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/theme/ThemeShared.kt
index b657152..888b0df 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/theme/ThemeShared.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/theme/ThemeShared.kt
@@ -29,9 +29,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
-import androidx.graphics.shapes.RoundedPolygon
-import android.graphics.Color as AndroidColor
import androidx.core.graphics.toColorInt
+import androidx.graphics.shapes.RoundedPolygon
// --- SHAPES ---
@@ -49,7 +48,7 @@ fun getShapeFromId(id: String): RoundedPolygon {
// --- COLORS ---
fun safeParseColor(hex: String): Color {
- return try { Color(hex.toColorInt()) } catch (e: Exception) { Color.White }
+ return try { Color(hex.toColorInt()) } catch (_: Exception) { Color.White }
}
// --- COMPONENTS ---
diff --git a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/theme/ThemeViewModel.kt b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/theme/ThemeViewModel.kt
index aa79186..3ff9175 100644
--- a/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/theme/ThemeViewModel.kt
+++ b/app/src/main/java/com/d4viddf/hyperbridge/ui/screens/theme/ThemeViewModel.kt
@@ -38,6 +38,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.util.UUID
+import androidx.core.graphics.scale
class ThemeViewModel(application: Application) : AndroidViewModel(application) {
@@ -69,6 +70,7 @@ class ThemeViewModel(application: Application) : AndroidViewModel(application) {
var selectedColorHex by mutableStateOf("#3DDA82")
var colorMode by mutableStateOf(ColorMode.CUSTOM)
+
var isDarkThemePreview by mutableStateOf(true)
var selectedShapeId by mutableStateOf("circle")
@@ -83,6 +85,9 @@ class ThemeViewModel(application: Application) : AndroidViewModel(application) {
var themeDefaultActions by mutableStateOf
- Nuevo: Controla la Isla
- HyperBridge ahora gestiona cómo se comportan múltiples notificaciones cuando se alcanza el límite de 9.\n\n<b>Por Defecto: Más Reciente</b>\nLas nuevas notificaciones reemplazan a las más antiguas automáticamente.
+ Controla la Isla
+ Elige cómo debe comportarse la isla cuando hay varias notificaciones activas al mismo tiempo.MantenerConfigurarNUEVO
@@ -230,6 +237,15 @@
DesconocidoROM China DetectadaTu dispositivo parece ejecutar un firmware chino. Xiaomi modifica drásticamente las notificaciones en estas ROMs. Las funciones podrían ser inestables o no funcionar.
+ Habilitar parche de Shizuku
+ Habilitar parche
+ Si estás en una ROM China, debes habilitar el parche de Shizuku para que las notificaciones se activen correctamente.
+ Shizuku está en ejecución
+ Shizuku no está en ejecución
+ Permiso concedido
+ Permiso denegado
+ Otorgar permiso de Shizuku
+ La aplicación Shizuku no está instalada o en ejecución. Por favor, instala e inicia Shizuku primero.Versión del SistemaLlamada EntranteLlamada en Curso
@@ -551,21 +567,108 @@
•<b>Corrección de errores:</b> Se solucionó un error crítico que provocaba el cierre de la aplicación al abrir la configuración en Chino Tradicional.\n
•<b>Mejoras generales:</b> Corrección de errores menores y mejoras de estabilidad para que la Isla funcione sin problemas.
-
TonoSaturaciónClaridad
-
+ Colores DinámicosMaterial You (Sistema)Extrae los colores de tu fondo de pantalla actual.
-
Mostrar solo compatibles con la IslaDetener widgets activosAviso de actualización¡HyperBridge se ha actualizado! Por favor, asegúrate de que los permisos de Inicio automático y Batería en segundo plano no hayan sido revocados silenciosamente por el sistema para que tus widgets sigan funcionando sin problemas.Entendido
-
RecomendadosTodosFavoritos
+ Actualizaciones nativas en vivo de Android
+ Transforma las notificaciones en las notificaciones en vivo de Android 16 en lugar del diseño de Xiaomi.
+ Actualizaciones nativas en vivo
+ Vista previa
+ Diseño de notificaciones
+ Diseño destacado de Xiaomi
+ Diseño moderno de HyperOS de Xiaomi.
+ Actualización en vivo
+ Diseño de notificaciones de Android, funciona en CN ROM.
+ Configuación del motor
+ Vista previa
+ Diseño de notificaciones
+ A tener en cuenta
+ • Los diseños de navegación se integrarán completamente en el Motor de Temas en una próxima actualización.\n• Al usar el motor nativo de Live Updates, solo el contenido del Lado Derecho es visible.
+ Notificaciones Destacadas de Xiaomi
+ Comportamiento de la Isla
+
+ Motor
+ Live Updates vs Isla de Xiaomi
+ No molestar
+ Gestionar la visibilidad de la isla durante No molestar
+ Pausar islas
+ Pausar manualmente todas las islas hasta que vuelvas a activarlo
+ Autodetectar No molestar del sistema
+ Pausar automáticamente las islas cuando tu teléfono esté en modo No molestar
+
+
+ Comportamiento Global
+ Ocultar automáticamente la isla
+ Se oculta tras un tiempo establecido
+ Visible hasta que se descarte manualmente
+ Determina cuánto tiempo permanece la isla en pantalla antes de desaparecer automáticamente.
+ Comportamiento gestionado por el tema activo.
+ Usar el valor predeterminado para el motor de notificaciones
+ Desencadenantes Activos
+ Selecciona qué eventos globales activan la isla
+ Mensajes normales, correos electrónicos y alertas
+ Ongoing tasks, rendering, and operations
+ Downloads, uploads, and file transfers
+ Controles de reproducción de música y video
+ Indicaciones paso a paso y tiempo estimado (ETA)
+ Llamadas telefónicas entrantes y en curso
+ Temporizadores activos y cronómetros
+ Seleccionar eventos desencadenantes
+ Anular Desencadenantes Globales
+ Establecer notificaciones permitidas personalizadas para esta aplicación
+ Anular desencadenantes
+ Selecciona qué eventos activan la isla para esta aplicación.
+ Habilitar eventos específicos
+ Licencias de código abierto
+ Actualizaciones en tiempo real
+ Eliminar notificación original
+ Descartar automáticamente la notificación original cuando una Live Update está activa.
+ Puedes cambiar estas opciones más tarde en Ajustes y el Configurador de Temas.
+ Añade widgets estándar de Android a tu isla para un acceso rápido.
+ Personaliza completamente los colores, formas e iconos de tu isla.
+ Amplía las posibilidades con diferentes opciones de personalización
+ Determina el comportamiento de la isla.
+ Vista previa del widget
+
+ Soporte para ROMs CN\n
+ • Habilita la nueva solución Sui & Shizuku para disfrutar de Hyper Islands en tus dispositivos con ROMs HyperOS CN (China) sin soporte nativo.\n\n
+ Mejoras del Motor de Temas\n
+ • Selección de Motor: Ahora puedes elegir motores de animación por aplicación o globalmente.\n
+ • Solución Shizuku: Opción agregada directamente a la pestaña Motor del Editor de Temas.\n
+ • Controles de Comportamiento: Tiempos de espera personalizados y lógica de ventanas emergentes son ahora totalmente compatibles.\n\n
+ Isla Permanente Inteligente\n
+ • Siempre Visible: Mantén la isla en pantalla incluso sin notificaciones activas.\n
+ • Ocultar Automáticamente: La isla permanente ahora se oculta automáticamente cuando se muestra un widget activo en la pantalla.\n\n
+ Tintado Inteligente de Iconos\n
+ • El nuevo sistema de detección analiza automáticamente los iconos de notificación extraídos y los tiñe inteligentemente de blanco si son oscuros o monocromáticos.\n\n
+ Descargas y Progreso\n
+ • Nuevo Diseño Dedicado para Descargas: Diseños de descarga y progreso separados con diseños visuales dedicados.\n
+ • Soporte de Progreso de Texto: Texto de porcentaje analizado (por ejemplo, "12%") para actualizaciones de aplicaciones incluso sin barras de progreso nativas.\n
+ • Filtrado Inteligente: Verificación de progreso basada en texto restringida a descargas de tiendas e instaladores para evitar activaciones falsas en otras notificaciones de estado.\n\n
+ Correcciones de Mensajería\n
+ • Se corrigió la falta de avatares del remitente en aplicaciones como Telegram (ahora extrae correctamente los datos "sender_person").\n
+ • Se resolvieron problemas con notificaciones agrupadas (resúmenes de chat) que no activaban las islas.\n
+ • Se corrigieron errores de desaparición de la isla causados por condiciones de carrera en la actualización de notificaciones.\n\n
+ Sistema y Configuración Inicial\n
+ • Nueva Pantalla de Bienvenida: Un proceso de configuración completamente nuevo y más fácil para los permisos críticos.\n
+ • Corrección de Inicio: El servicio ahora se inicia inmediata y confiablemente después de reiniciar.\n
+ • Alertas más Inteligentes: Las descargas y medios se actualizan silenciosamente, mientras que las actualizaciones de mensajes activan animaciones emergentes de manera confiable.\n
+ • Navegación: Transiciones de aplicaciones más rápidas y fluidas.
+
+ La actualización en vivo
+ ¡Si ves esto, algo salió mal!
+ ¡Si estás viendo esto, algo salió mal! El sistema no puede mostrar el widget
+
+
diff --git a/app/src/main/res/values-fr-rFR/strings.xml b/app/src/main/res/values-fr-rFR/strings.xml
new file mode 100644
index 0000000..f250af8
--- /dev/null
+++ b/app/src/main/res/values-fr-rFR/strings.xml
@@ -0,0 +1,673 @@
+
+
+ Hyper Bridge
+ Précédent
+ Suivant
+ Terminer
+ Fait
+ Logo
+ Paramètres
+ Info
+ Configurer
+ Effacer
+ Tri
+ Bienvenue dans\nHyper Bridge
+ Intégrez parfaitement HyperIsland à votre expérience HyperOS.
+ Démarrer
+ Comment cela fonctionne
+ Choisissez vos applications favorites (Spotify, Maps, Horloge), HyperBridge transforme leurs notifications en bulle HyperIsland.
+ Développement précoce
+ Note : Cette version est une bêta. Vous pourriez rencontrer des bugs ou des problèmes d\'affichage.
+ Appareil compatible
+ Appareil non supporté
+ Votre appareil semble exécuter HyperOS/MIUI. Vous êtes prêt !
+ HyperBridge s\'appuie sur des fonctionnalités spécifiques du système HyperOS. Le fabricant de votre appareil ne semble pas être Xiaomi/POCO/Redmi.
+ ✅ Système détecté
+ ⚠️ L\'appli peut ne pas fonctionner
+ Afficher la bulle
+ Nous avons besoin de la permission pour publier la notification \"Island\" sur votre écran.
+ Permission accordée
+ Autoriser les notifications
+ Lire les données
+ Pour connecter vos applications, nous devons lire leurs notifications (Musique, Maps, Horloge).
+ Ouvrir les paramètres
+ Garder actif
+ HyperOS tue les applications en arrière-plan. Activez le démarrage automatique et pas de restrictions pour garder l\'application active.
+ 1. Activer le démarrage automatique
+ 2. Définir le mode de batterie sur \'Aucune restriction\'
+ Actif
+ Bibliothèque
+ Applications actives
+ Bibliothèque d\'applications
+ Rechercher des applications…
+ Tri : A-Z
+ Tri : Z-A
+ Aucune application active
+ Allez dans la bibliothèque pour activer les applications.
+ Aucune application trouvée
+ À propos
+ Version %s
+ par D4vidDf
+ Configuration
+ Configuration du système
+ Corriger les permissions & batterie
+
+ Permanent Island
+ Keep the Dynamic Island permanently visible on screen when there are no active notifications.
+ When enabled, an empty Dynamic Island will be shown permanently on your screen. It will automatically hide when real notifications arrive and reappear when all notifications are cleared. It will not be visible in your notification shade.
+ Island Width
+ A propos
+ Développeur
+ Visiter d4viddf.com
+ Code source
+ Dépôt GitHub
+ Licences Open Source
+ Logiciel tiers
+ Fabriqué avec ❤️ en Espagne
+ Santé de l\'application
+ Assurez-vous que toutes les vérifications soient en vert pour que l\'application fonctionne correctement en arrière-plan.
+ Autorisations Requises
+ Accès aux notifications
+ Requis pour lire les événements des applications
+ Permission de superposition d\'affichage
+ Optimisation de l\'appareil
+ Xiaomi Autostart
+ Autoriser l\'application à se lancer au démarrage
+ Pas de restriction de batterie
+ Empêcher le système de tuer l\'application
+ Note : Verrouille également l\'application dans le menu \'Récents\'.
+ Actif
+ Action requise
+ Toutes
+ Musique
+ Navigation
+ Productivité
+ Autre
+ Sélectionner les notifications actives
+ Messages & Général
+ Téléchargements & Progression
+ Downloads
+ Musique & Média
+ Cartes & GPS
+ Appels
+ Minuteurs & Alarmes
+ Actif
+ Téléchargement terminé
+ En attente…
+ Terminé
+ Téléchargement
+ En attente…
+ Lecture en cours
+ Numéro inconnu
+ Appel
+ Maps
+ Minuteur
+
+ téléchargé
+ terminé
+ terminé
+ installé
+ fait
+ réussi
+ réussi
+
+ Téléchargement terminé
+ Téléchargement
+ En attente…
+ Terminé
+ Lecture en cours
+ Appel entrant
+ Numéro inconnu
+ Maps
+ Minuteur
+ Chronomètre
+ Quoi de neuf dans %s
+ Génial !
+ Comportement de la bulle
+ Stratégie de limite
+ Que se passe-t-il lorsque vous atteignez 9 bulles ?
+ Premier arrivé, premier affiché
+ Garder les 9 premières applications. Les nouvelles applications seront ignorées jusqu\'à ce qu\'il y ait de la place.
+ Plus récent (par défaut)
+ Les nouvelles notifications remplacent les plus anciennes.
+ Priorité personnalisée
+ Définir quels types sont les plus importants (par exemple les appels).
+ Ordre de priorité
+ La priorité élevée (haut) remplace la priorité basse (bas).
+ Déplacer vers le haut
+ Déplacer en bas
+ Configurer l\'ordre des applications
+ Paramètres généraux
+ Apparence des bulles de notifications
+ Popup de notification flottante
+ Afficher brièvement la bannière de notification complète avant de minimiser la bulle.
+ Pendant combien de temps la fenêtre flottante reste visible.
+ Panneau des notifications
+ Garder la notification visible dans le panneau de notification. Si désactivé, elle n\'apparaît que dans une bulle.
+ Délai de fermeture automatique
+ %ds
+ Utiliser les valeurs par défaut
+ Personnaliser
+ Outrepasser les paramètres
+
+ arrivée
+ arrive
+ eta
+ atteinte
+ destination
+
+ Options
+ Historique des versions
+ La version de l\'information
+ • <b>Progression avancée:</b> Ajout d\'étapes de progressions (par ex. 1 sur 3) et de barres de progression avec icônes dans la bulle de notification.\n
+ • <b>Système de priorité :</b> Nouveaux paramètres « Comportement des bulles de notification. Choisissez entre \"Les plus récents\", \"Premier arrivé\" ou une liste personnalisée \"Priorité des applications\".\n
+ • <b> Filtrage intelligent :</b> Détection améliorée des notifications en double et des téléchargements groupés.\n
+ • <b>Améliorations visuelles :</b> Correction de problèmes de mise en page avec les instructions de navigation.\n
+ • <b>Configuration par application :</b> Personnalisez le comportement du délai d\'attente et du flottement par application.
+
+ La version expressive
+ • <b>Révision majeure de l\'interface utilisateur :</b> Nouveau design \"Material 3 expressive\" avec des animations plus fluides.\n
+ • <b> Meilleure bulle:</b> Le texte apparaît maintenant sur le côté droit. Ajout d\'une coche verte pour les téléchargements terminés.\n
+ • <b>Configuration par application :</b> Sélectionnez les types de notifications spécifiques (Musique, Plans, Appels) par application.\n
+ • <b>Filtres intelligents :</b> Bloque les notifications en double et économise la batterie.\n
+ • <b>Nouvelle bibliothèque :</b> Filtrer les applications par catégorie et ordre de tri.\n
+ • <b>Localisation :</b> Ajout de la langue espagnole
+
+ Nouveau : Contrôler la bulle de notification
+ HyperBridge peut maintenant gérer le comportement de plusieurs notifications lorsque la limite de 9 bulles est atteinte.\n\n<b>Par défaut : Les plus récentes</b>\nLes nouvelles notifications remplacent automatiquement les plus anciennes.
+ Conserver la valeur par défaut
+ Configurer maintenant
+ NOUVEAU
+ Design personnalisé
+ Disposition de la navigation
+ Choisir où les informations apparaissent
+ Côté gauche
+ Côté droit
+ Instructions
+ Distance
+ Heure d\'arrivée
+ Dist. & Heure
+ Vide
+ Bulles actives
+ Basculer %s
+ Configurer %1$s
+ Activer les %s notifications
+ Désactiver les %s notifications
+ Modifier la disposition de navigation
+ Actif
+ Inactif
+ Déplacer l\'élément vers le haut
+ Déplacer l\'élément vers le bas
+ Glissez l\'interrupteur. Double-appui et maintien pour réorganiser, ou utilisez les actions pour déplacer.
+ Icône d\'application pour %s
+ Aperçu de la navigation : %1$s à gauche, %2$s à droite
+ Développer les détails de la version
+ Réduire les détails de la version
+ Paramètres de configuration
+ À propos de l\'information
+ Tournez à droite
+ 200m
+ 10:30
+ Termes bloqués
+ Les notifications qui contiennent ces mots seront ignorées.
+ Ajouter un mot (par ex. spoiler)
+ Ajouter
+ Supprimer
+ Appuyez sur une application pour configurer des règles de blocage spécifiques.
+ Gérer les filtres pour empêcher les notifications
+ Types de notifications
+ %d actif
+ Utiliser les valeurs par défaut du système pour la vitesse d\'animation et les fenêtres flottantes.
+ %d règles actives
+ Règles globales
+ Règles spécifiques à l\'application
+ Personnaliser les filtres pour certaines applications
+ %d termes bloqués
+ Aucune règle active
+ Compatibilité du système
+ Appareil : %s
+ OS: %s
+ Nécessite HyperOS 3.0+ (Android 15+)
+ Appareil Xiaomi/Redmi/Poco requis
+ Démarrage automatique en arrière-plan
+ Requis pour que le service redémarre après un redémarrage.
+ Autoriser
+ Compatible
+ Incompatible
+ Inconnu
+ Rom chinoise détectée
+ Votre appareil semble exécuter un firmware chinois. Xiaomi modifie considérablement le système de notification interne sur les ROMs CN. Les fonctionnalités peuvent être instables ou ne pas fonctionner du tout.
+ Enable Shizuku Workaround
+ If you are on a CN ROM, you must enable the Shizuku Workaround for notifications to trigger correctly.
+ Shizuku is running
+ Shizuku is not running
+ Permission Granted
+ Permission Denied
+ Grant Shizuku Permission
+ Shizuku app is not installed or running. Please install and start Shizuku first.
+ Version système
+ Appel entrant
+ Appel en cours
+
+ prendre
+ fin
+ refuser
+ rejeter
+
+
+ répondre
+ accepter
+
+
+ appelant
+ audio
+
+ La version \"Globale\"
+ Notification masquée 👻\n
+ Nouvelle option pour masquer les notifications du panneau système tout en gardant les bulles active. Vérifiez les paramètres généraux!\n\n
+ Nouvelles langues 🌍\n
+ Un merci spécial à notre communauté pour ces traductions :\n
+ • 🇧🇷 portugais (Brésil)\n
+ • 🇵🇱 polonais\n
+ • 🇰🇷 coréen\n\n
+ • 🇺🇦 ukrainien\n\n
+ Personnalisation de la navigation 🧭\n
+ Vous pouvez maintenant choisir les informations qui apparaissent sur les côtés gauche et droit de la bulle de navigation (distance, ETA, instruction).\n\n
+ Protection contre les spoilers 🛡️\n
+ Nouvelle fonctionnalité « termes bloqués ». Définissez des mots globalement ou par application pour empêcher l\'affichage de notifications spécifiques.\n\n
+ Améliorations des appels 📞\n
+ Logique de détection améliorée pour prendre en charge une plus grande gamme d\'applications VoIP et de clavier.\n\n
+ Corrections et & améliorations 🛠️\n
+ • Correction d\'un bug critique qui empêchait le chargement de l\'application après un redémarrage.\n
+ • Correction des notifications affichant parfois les noms bruts des packages (par exemple, \"com.google...\") au lieu du contenu.\n
+ • Feuille de configuration de l\'application repensée pour une meilleure convivialité.\n
+ • Écran de vérification de compatibilité amélioré avec des informations détaillées sur l\'appareil
+
+ 100% hors ligne & sécurisé
+ Hyper Bridge traite tout directement via le processeur de votre téléphone.\n\nIl ne nécessite PAS un accès à Internet, ce qui signifie que vos notifications et vos données personnelles ne quittent jamais votre appareil.
+ Langue
+ Changer la langue de l\'application
+ Par défaut (Système)
+ Enregistrer
+ Sauvegarde & restauration
+ Gérer les données
+ Sélectionner les données pour l\'opération
+ Paramètres de l\'application
+ État de la configuration, thème, comportement
+ Base de données de blocage
+ Votre liste de mot-clés et d\'applications bloquées
+ Règles de Priorité
+ Configurations personnalisées de priorité des notifications
+ Créer une sauvegarde
+ Restaurer une sauvegarde
+ Sauvegarde créée avec succès
+ Échec de l\'export : %s
+ Données restaurées avec succès
+ Échec de l\'import : %s
+ Fichier de sauvegarde HyperBridge invalide
+ Attention : Sauvegarde depuis une autre version de la base de données
+ Le statut ne peut pas être vérifié automatiquement.\nVeuillez le vérifier manuellement pour vous assurer que le service fonctionne correctement
+ Informations de sauvegarde
+ Date
+ Appareil
+ Version
+ Nombre d\'éléments
+ (Vide)
+ Restaurer les données sélectionnées
+ Optimisation de l\'appareil
+ Définir \"Pas de restrictions\"
+ Approche \"sans connexion\"
+ Aucune connexion internet requise.
+ Sauvegarde & langues
+ <b>Sauvegarde & Restauration 📦</b>\n
+ Nouvelle fonctionnalité utilisateur ! Exportez vos paramètres, listes de blocage et priorités d\'applications en toute sécurité vers un fichier .hbr et restaurez les à tout moment.\n\n
+ <b>Nouvelles Langues 🌍</b>\n
+ Support Ajouté pour :\n
+ • :Allemagne : Allemand\n
+ • :Russie : Russe\n
+ • 🇺🇦 Ukrainien (Imprové)\n
+ <i>Plus : Un nouveau sélecteur de langue a été intégré pour changer de Langue sans toucher aux paramètres système.</i>\n\n\n
+ <b>Mise à jour visuelle 🎨</b>\n
+ L\'application a été mise à jour avec <b>Material 3 Expressive</b> . Appréciez des en-têtes plus grands, des formes arrondies et une expérience plus agréable.\n\n
+ <b>Confidentialité & Configuration 🛡️</b>\n
+ • Amélioration de l\'intégration avec une page consacrée à la protection de la vie privée qui clarifie l\'architecture hors ligne.\n
+ • Des contrôles de compatibilité plus stricts pour assurer la meilleure expérience sur HyperOS 3.0.\n\n\n
+ <b>Corrections 🛠️</b>\n
+ • Correction d\'un problème critique où les paramètres étaient perdus après le redémarrage de l\'appareil (migré vers la base de données).\n
+ • <Correction du bouton \"Quitter\" sur les bulles de navigation s\'affichant sous la forme d\'une icône au lieu du texte.\n
+ • Correction de l\'icône de chargement dans les paramètres.\n
+
+ Fermer
+ Rechercher
+ Widget mis à jour
+ Impossible de modifier les paramètres de ce widget
+ Ajouter un Widget
+ Rechercher des applications...
+
+ %d widgets
+ %d widget
+
+ Configurer le widget
+ Paramètres du widget
+ Apparence
+ Comportement
+ Mode de rendu
+ Taille
+ Afficher en ombre
+ Visible dans le panneau de notification
+ Fermeture automatique
+ Fermer la bulle après le délai
+
+ %d secondes
+ %d seconde
+
+ Rafraichir automatiquement
+ Mettre à jour l\'instantané régulièrement
+
+ Toutes les %d minutes
+ Toutes les minutes
+
+ Une fréquence plus élevée consomme plus de batterie.
+ Interactif
+ Fonctionnement par boutons. Idéal pour la musique et les contrôles.
+ Instantané
+ Corrige les listes vides. Idéal pour les calendriers et Gmail.
+ Original
+ Petite (100dp)
+ Moyenne (180dp)
+ Grande (280dp)
+ Très grande (380dp)
+ Non installée
+ Paramètres
+ Widgets sauvegardés
+ Nouveau Widget
+ Aucun widget enregistré trouvé.
+ Widget #%d
+ Afficher la bulle
+ Importer un thème
+ Analyse du paquet du thème...
+ Thème prêt
+ Créé par %s
+ Version %d
+ Aucune description fournie.
+ Souhaitez-vous appliquer ce thème à HyperBridge maintenant ?
+ Appliquer thème
+ Enregistrer pour plus tard
+ Échec de l\'importation
+ Impossible de vérifier le fichier du thème.
+ Fermer
+ Mes thèmes
+ Revenir en arrière
+ Ajouter un nouveau thème
+ par %s
+ Thème actuel
+ Supprimer le thème
+ Par défaut (Système)
+ Style standard
+ Ajouter un nouveau thème
+ Nouveau thème
+ Nom du thème
+ Nom de l\'auteur
+ Couleur de surbrillance
+ Prévisualisation
+ Sauvegarder le thème
+ Veuillez saisir un nom et un auteur
+ Créer un nouveau thème
+ Rechercher
+ Importer depuis un fichier
+ Importer
+ Modifier
+ Exporter
+ Création de thème
+ Modification du thème
+ Couleurs
+ Icônes & formes
+ Style des appels
+ Configuration des applications
+ Enregistrer
+ Modifier les informations du thème
+ Palettes & contraste
+ Formes & remplissage
+ Boutons de réponse & rejet d\'appel
+ Personnalisations spécifiques d\'application
+ Informations sur le thème
+ Nom du thème
+ Nom de l\'auteur
+ Terminé
+ Préréglages
+ Personnalisé
+ Préréglages
+ Utiliser les couleurs de l\'application
+ Adapter les couleurs des applications installées
+ Couleur personnalisée
+ Ajouter une couleur personnalisée
+ Choisissez une couleur
+ Choisissez une couleur personnalisée pour votre thème.
+ Code Hexa
+ Fait
+ Style
+ Icônes
+ Forme
+ Taille de l\'icône
+ Pleine
+ Minimale
+ Navigation
+ Début
+ Fin
+ Avancement
+ Succès
+ Forme d\'icône
+ Cercle
+ Carré
+ Cookie
+ Arc
+ Trèfle
+ Décrocher
+ Rejeter
+ Couleur héxa
+ Applications configurées
+ Pas d\'applications configurées
+ Configurer l\'application
+ Sélectionner l’application
+ Application
+ Couleur de surbrillance (hexa)
+ Actions personnalisées
+ Mot-clé
+ Bouton contenant \'%s\'
+ Enregistrer la configuration
+ Couleur par défaut
+
+ %d actions
+ %d action
+
+ Personnalisation
+ Apprendre comment créer des thèmes
+ Fonctionnalités Pro
+ Déverrouillez plus de fonctionnalités
+ Communauté
+ Rejoignez notre GitHub
+ Thèmes
+ Widgets
+ Voir plus
+ Ajouter un nouveau
+ Ajouter un design
+ Personnalisation
+ Widget système (Bêta)
+ Obtenir des thèmes
+ Ajoutez votre premier Widget
+ %d actifs
+ Apparence
+ Actions globales
+ Boutons de style par mot clé
+ Configurer l\'action
+ Mot-clé (par exemple \'Répondre\')
+ Couleur de fond
+ Couleur de l\'icône/texte
+ Aperçu
+ Icône seulement
+ Texte
+ Les deux
+ Actions personnalisées
+ Ajouter des règles pour afficher les boutons de notification en fonction de leur texte (par exemple, « Répondre », « Archiver »).
+ Action
+ Rechercher des actions...
+ Aucune action trouvée pour \"%1$s\"
+ Cette fonctionnalité est en cours de développement. La recherche de mots clés dépend de la façon dont les applications structurent leurs notifications et peuvent ne pas fonctionner selon les applications.
+ Appliquer le thème ?
+ Voulez-vous appliquer ce thème maintenant ?
+ Enregistrer & appliquer
+ Enregistrer uniquement
+ Sélectionner une icône
+ Changer l\'icône
+ Ajouter une application
+ Sélectionner l’application
+ Modifier l\'application
+ Couleur personnalisée
+ Utiliser les valeurs par défaut
+ Ensemble de formes personnalisées
+ Utiliser les valeurs par défaut
+ Ajouter une application pour personnaliser son apparence de manière spécifique.
+ Abandonner
+ Modifications non enregistrées
+ Vous avez des modifications non enregistrées pour cette application. Voulez-vous les enregistrer avant de quitter ?
+ Partager le thème
+ Mes thèmes
+
+ %1$d action personnalisée
+ %1$d actions personnalisées
+
+ La version de la personnalisation
+ <b>Système de thèmes :</b>\n
+ La plus grande mise à jour ! Vous pouvez maintenant personnaliser pleinement l\'expérience visuelle des bulles de notification. \n
+ • <b>Créateur de Thèmes :</b> Concevez des thèmes directement sur votre téléphone avec un aperçu en temps réel.\n
+ • <b>Paramétrage plus fin:</b> Choisissez exactement comment les icônes sont affichées, personnalisez les couleurs et les formes des notifications, et configurer les remplacements des applications individuelles.\n
+ • <b>Couleurs intelligentes:</b> Extraction automatique des couleurs des icônes d\'application.\n
+ • <b>Personnalisation par action (Bêta):</b> </b> Ajustement de l\'apparence de certaines notifications d\'actions spécifiques.\n
+ • <b>Partage :</b> Exportez et importez des thèmes au format <i>.hbr</i> .\n\n
+
+ <b>Support des widget 🧩</b>\n
+ Verrouillez les widgets standard d\'Android pour un accès rapide — même sur votre <b>écran verrouillé</b>!\n
+ • Choisissez entre les modes <b>interractifs</b> pour la vitesse ou <b>instantané</b> pour l\'efficacité de la batterie.\n\n
+
+ <b>Moteur de notification 2.0⚡</b>\n
+ Réécriture complète du moteur pour une meilleure stabilité:\n
+ • <b>Analyse intelligente : </b> Résolutions de problèmes où les notifications affichaient les noms des paquets (par ex. : com.whatsapp) au lieu du contenu du message.\n
+ • <b>Anti-scintillement :</b> La vérification de contenu empêche les notifications redondantes et les saccades.\n
+ • <b>Correction des fantômes :</b> Résolution d\'un problème où les notifications persistaient après leur suppression.\n\n
+
+ <b>Rafraîchissement visuel ✨</b>\n
+ • Onglet \"Design\" redessiné pour plus de fluidité avec des blocs dynamiques.\n
+ • Nouveaux boutons de textes pour des actions de notification plus claires.\n
+ • Ajout du support pour l\'indonésien 🇮🇩 et le turque 🇹🇷.\n\n
+
+ <i>Note : Cette version comprend des changements architecturaux importants. Veuillez signaler tout bug sur GitHub !</i>
+
+ Mise à jour de corrections de bugs
+ •<b>Correction d\'un crash :</b> Résolution d\'un problème critique qui faisait planter l\'application lors de l\'ouverture du menu des paramètres en chinois traditionnel.\n
+ •<b>Corrections diverses :</b> Corrections générales de bugs pour améliorer la stabilité et la fluidité de l\'application.
+
+ Teinte
+ Saturation
+ Clarté
+ Couleurs dynamiques
+ Material You (Système)
+ Extrait les couleurs de votre fond d\'écran actuel.
+ Afficher uniquement ceux compatibles
+ Tuer les widgets actifs
+ Notification de mise à jour
+ HyperBridge a été mis à jour ! Assurez-vous que vos autorisations de démarrage automatique et de fonctionnement en arrière-plan n\'ont pas été révoquées par le système pour que vos widgets continuent de fonctionner correctement.
+ J\'ai compris
+ Recommandé
+ Toutes
+ Favoris
+ Notifications natives d\'Android en temps réel
+ Transforme les notifications en tâches standards d\'Android 16 au lieu de les remplacer par des bulles Xiaomi Islands.
+ Notifications natives en temps réel
+ Aperçu
+ Design des notifications
+ Design standard de Xiaomi
+ Mise en page HyperOS moderne avec le design de Xiaomi.
+ Design natif
+ Design de notification natives Android, fonctionne avec les ROM CN.
+ Configurations du moteur
+ Aperçu
+ Design des notifications
+ Bon à savoir
+ • Les dispositions de navigation seront intégrées au moteur de thèmes dans une prochaine mise à jour.\n• En cas d\'utilisation du moteur de mise à jour natif, seul le contenu du côté droit est visible.
+ Notifications standard de Xiaomi
+ Comportement de la bulle
+
+ Moteur
+ Notifications natives vs Xiaomi
+ Do Not Disturb
+ Manage island visibility during DND
+ Pause Islands
+ Manually pause all islands until you turn this back on
+ Auto-detect System DND
+ Automatically pause islands when your phone is in Do Not Disturb mode
+
+
+ Comportement global
+ Cacher automatiquement la bulle de notification
+ Masquer après une heure définie
+ Visible jusqu\'au masquage manuel
+ Détermine combien de temps la bulle de notification reste à l\'écran avant de disparaître automatiquement.
+ Comportement géré par le thème actif.
+ Utiliser la valeur par défaut du moteur de notification
+ Déclencheurs actifs
+ Sélectionnez quels événements globaux déclenchent les notifications
+ Messages standards, emails et alertes
+ Téléchargements, envois et tâches en cours
+ Downloads, uploads, and file transfers
+ Contrôles de lecture de musique et de vidéo
+ Instructions de navigation et temps estimé d\'arrivée
+ Appels entrants et en cours
+ Minuteurs et chronomètres actifs
+ Sélectionner les événements déclenchés
+ Outrepasser les déclencheurs globaux
+ Définir des notifications personnalisées autorisées pour cette application
+ Remplacement du déclencheur
+ Sélectionnez quels événements déclenchent les bulles de notification pour cette application.
+ Activer des déclencheurs spécifiques
+ Licences Open Source
+ Mises à jour en temps réel
+ Supprimer la notification originale
+ Ignorer automatiquement la notification source quand une mise à jour en direct est active.
+ Vous pourrez modifier ces options plus tard dans Paramètres et Configurateur de thèmes.
+ Ajoutez des widgets standard Android à votre bulle de notification pour un accès rapide.
+ Personnalisez entièrement les couleurs, les formes et les icônes de votre bulle de notification.
+ Augmentez les possibilités avec différentes options de personnalisation
+ Détermine le comportement de la bulle de notification.
+ Aperçu du widget
+
+ CN ROM Support\n
+ • Enable the new Sui & Shizuku workaround to enjoy the Hyper Islands in your devices running HyperOS CN (Chinese) ROMs without native support.\n\n
+ Theme Engine Improvements\n
+ • Engine Selection: You can now choose animation engines per-app or globally.\n
+ • Shizuku Workaround: Toggle added directly to the Theme Editor Engine tab.\n
+ • Behavior Controls: Custom timeouts and popup logic are now fully supported.\n\n
+ Intelligent Permanent Island\n
+ • Always Visible: Keep the island on screen even without active notifications.\n
+ • Auto-Hide: The permanent island now automatically hides itself whenever an active widget is shown on the screen.\n\n
+ Smart Icon Tinting\n
+ • New detection system automatically analyzes extracted notification icons and intelligently tints them white if they are dark or monochrome.\n\n
+ Downloads & Progress\n
+ • New Dedicated Download Layout: Separated download and progress layouts with dedicated visual designs.\n
+ • Text Progress Support: Parsed percentage text (e.g. "12%") for app updates even without native progress bars.\n
+ • Smart Filtering: Restricted text-based progress checking to store and installer downloads to prevent false triggers on other status notifications.\n\n
+ Messaging Fixes\n
+ • Fixed missing sender avatars in apps like Telegram (now correctly extracts "sender_person" data).\n
+ • Resolved issues with grouped notifications (chat summaries) not triggering islands.\n
+ • Fixed island disappearance bugs caused by notification update race conditions.\n\n
+ System & Onboarding\n
+ • New Onboarding Screen: A brand new and easier setup process for critical permissions.\n
+ • Boot Fix: Service now starts immediately and reliably after restart.\n
+ • Smarter Alerts: Downloads and Media update silently, while messaging updates reliably trigger popup animations.\n
+ • Navigation: Faster and smoother app transitions.
+
+ The Live Update
+ If you are seeing this, something went wrong!
+ If you are seeing this, something went wrong! The system couldn\'t show the widget
+
+
+
diff --git a/app/src/main/res/values-gl-rES/strings.xml b/app/src/main/res/values-gl-rES/strings.xml
index 40b1502..ee368fa 100644
--- a/app/src/main/res/values-gl-rES/strings.xml
+++ b/app/src/main/res/values-gl-rES/strings.xml
@@ -526,3 +526,5 @@
%1$d custom actions
+
+
diff --git a/app/src/main/res/values-hu-rHU/strings.xml b/app/src/main/res/values-hu-rHU/strings.xml
index c423a1a..0d7651d 100644
--- a/app/src/main/res/values-hu-rHU/strings.xml
+++ b/app/src/main/res/values-hu-rHU/strings.xml
@@ -51,6 +51,11 @@
KonfigurációRendszerbeállításEngedélyek és akkumulátor javítása
+
+ Permanent Island
+ Keep the Dynamic Island permanently visible on screen when there are no active notifications.
+ When enabled, an empty Dynamic Island will be shown permanently on your screen. It will automatically hide when real notifications arrive and reappear when all notifications are cleared. It will not be visible in your notification shade.
+ Island WidthAlkalmazásrólFejlesztőKeresd fel d4viddf.com
@@ -80,7 +85,8 @@
EgyébAktív értesítések kiválasztásaÜzenetek és általános információk
- Letöltések és folyamatok
+ Progress & Tasks
+ DownloadsZene és médiaTérképek és GPSHívások
@@ -135,6 +141,7 @@
Island megjelenéseFelugró ablakA minimálisra csökkentés előtt rövid ideig mutassa meg a teljes értesítési sávot az Islandon.
+ How long the heads-up popup stays visible.Értesítési panelTartsa láthatóvá az értesítést az értesítési panelen. Ha letiltja, akkor csak Island-on jelenik meg.Automatikus bezárás késleltetése
@@ -143,11 +150,11 @@
TestreszabásBeállítások felülírása
- arrival
- arrive
- eta
- reaching
- destination
+ érkezés
+ érkezés
+ évi
+ elér
+ célLehetőségekVerzióelőzmények
@@ -230,11 +237,19 @@
IsmeretlenKínai ROM észlelveÚgy tűnik, hogy az eszközön kínai rendszer fut. A Xiaomi jelentősen módosítja az értesítések belső működését a CN ROM-okon. A funkciók instabilak lehetnek, vagy egyáltalán nem működnek.
+ Enable Shizuku Workaround
+ If you are on a CN ROM, you must enable the Shizuku Workaround for notifications to trigger correctly.
+ Shizuku is running
+ Shizuku is not running
+ Permission Granted
+ Permission Denied
+ Grant Shizuku Permission
+ Shizuku app is not installed or running. Please install and start Shizuku first.RendszerverzióBejövő hívásAktív hívás
- hang
+ lerakvégeelutasításelutasít
@@ -245,7 +260,7 @@
hangszóró
- audio
+ HangAz általános frissítésRejtett értesítések 👻\n
@@ -546,4 +561,112 @@ Továbbfejlesztett felismerési logika a VoIP- és tárcsázóalkalmazások szé
• Hozzáadott támogatás 🇮🇩 indonéz és 🇹🇷 török nyelvekhez.\n\n
Megjegyzés: Ez a kiadás jelentős architektúraváltozásokat tartalmaz. A hibákat a GitHubon jelentsd!
+ Hibaijavító frissítés
+ •<b>Hibajavítás:</b> Kijavítottunk egy kritikus hibát, amely miatt az alkalmazás összeomlott, amikor a beállítások menüt hagyományos kínai nyelven nyitották meg.\n
+ •<b>Háttérmunkák:</b> Általános hibajavítások és stabilitási fejlesztések, hogy az island-od zökkenőmentesen működjön.
+
+ Árnyalat
+ Telítettség
+ Fényesség
+ Dinamikus színek
+ Material You (Rendszer)
+ A háttérképed színeit használja
+ Csak Island kompatibilisek megjelenítése
+ Aktív modulok megölése
+ Frissítési értesítés
+ HyperBridge frissítve lett! Ellenőrizd, hogy az Automatikus indítás és a Háttérben való futás nem lett-e csendben elvéve a rendszer által, hogy a modulok továbbra is folyékonyan tudjanak működni.
+ Értem
+ Ajánlott
+ Mind
+ Kedvencek
+ Natív Android élő frissítések
+ Ahelyett, hogy az értesítéseket egyedi Xiaomi Islands-ekkel felülírnád, alakítsd át őket szabványos Android 16-os háttérfeladatokká.
+ Natív élő frissítések
+ Előnézet
+ Értesítés kinézete
+ Xiaomi kiemelt kinézet
+ Modern HyperOS elrendezés a Xiaomi-nak a tervezett kinézete.
+ Élő frissítéses kinézet
+ Natív android értesítés kinézet, Kínai ROM-on működik.
+ Motor beállítás
+ Előnézet
+ Értesítés kinézete
+ Érdemes tudni
+ • A navigációs elrendezések egy közelgő frissítéssel teljes mértékben beépülnek a témamotorba.\n• A Natív élő frissítési motor használata esetén csak a jobb oldali tartalom látható.
+ Xiaomi kiemelt értesítés
+ Island működése
+
+ Motor
+ Élő frissítések vagy Xiaomi Island
+ Do Not Disturb
+ Manage island visibility during DND
+ Pause Islands
+ Manually pause all islands until you turn this back on
+ Auto-detect System DND
+ Automatically pause islands when your phone is in Do Not Disturb mode
+
+
+ Általános működés
+ Island automatikus elrejtése
+ Megadott idő után eltűnik
+ Látható a kézi eltüntetésig
+ Meghatározza, hogy az Island mennyi ideig marad a képernyőn, mielőtt automatikusan eltűnik.
+ Működés kezelve az aktív téma által.
+ Alapértelmezett érték használata az értesítési motorhoz
+ Aktív aktiválók
+ Válaszd ki melyik általános események aktiválják az Island-ot
+ Általános üzenetek, emailek, értesítések
+ Ongoing tasks, rendering, and operations
+ Downloads, uploads, and file transfers
+ Zene- és videólejátszás vezérlői
+ Lépésről lépésre szóló útvonalterv és ÉVI
+ Bejövő és folyamatban lévő telefonhívások
+ Aktív időzítők és stopperek
+ Válassz aktivált műveleteket
+ Általános aktiválások felülírása
+ Állíts be egyedi engedélyezett értesítést ehhez az apphoz
+ Aktiválás felülírása
+ Válaszd ki, hogy melyik esemény aktiválja a szigetet ehhez az apphoz.
+ Bizonyos aktiválók engedélyezése
+ Open Source Licenses
+ Live updates
+ Remove original notification
+ Automatically dismiss the source notification when a Live Update is active.
+ You can change these options later in Settings and Theme Configurator.
+ Add standard Android widgets to your island for quick access.
+ Fully customize colors, shapes and icons of your island.
+ Expand the possibilities with different customization options
+ Determines how the island behaves.
+ Preview of the widget
+
+ CN ROM Support\n
+ • Enable the new Sui & Shizuku workaround to enjoy the Hyper Islands in your devices running HyperOS CN (Chinese) ROMs without native support.\n\n
+ Theme Engine Improvements\n
+ • Engine Selection: You can now choose animation engines per-app or globally.\n
+ • Shizuku Workaround: Toggle added directly to the Theme Editor Engine tab.\n
+ • Behavior Controls: Custom timeouts and popup logic are now fully supported.\n\n
+ Intelligent Permanent Island\n
+ • Always Visible: Keep the island on screen even without active notifications.\n
+ • Auto-Hide: The permanent island now automatically hides itself whenever an active widget is shown on the screen.\n\n
+ Smart Icon Tinting\n
+ • New detection system automatically analyzes extracted notification icons and intelligently tints them white if they are dark or monochrome.\n\n
+ Downloads & Progress\n
+ • New Dedicated Download Layout: Separated download and progress layouts with dedicated visual designs.\n
+ • Text Progress Support: Parsed percentage text (e.g. "12%") for app updates even without native progress bars.\n
+ • Smart Filtering: Restricted text-based progress checking to store and installer downloads to prevent false triggers on other status notifications.\n\n
+ Messaging Fixes\n
+ • Fixed missing sender avatars in apps like Telegram (now correctly extracts "sender_person" data).\n
+ • Resolved issues with grouped notifications (chat summaries) not triggering islands.\n
+ • Fixed island disappearance bugs caused by notification update race conditions.\n\n
+ System & Onboarding\n
+ • New Onboarding Screen: A brand new and easier setup process for critical permissions.\n
+ • Boot Fix: Service now starts immediately and reliably after restart.\n
+ • Smarter Alerts: Downloads and Media update silently, while messaging updates reliably trigger popup animations.\n
+ • Navigation: Faster and smoother app transitions.
+
+ The Live Update
+ If you are seeing this, something went wrong!
+ If you are seeing this, something went wrong! The system couldn\'t show the widget
+
+
diff --git a/app/src/main/res/values-in-rID/strings.xml b/app/src/main/res/values-in-rID/strings.xml
index bc8b649..c583d5e 100644
--- a/app/src/main/res/values-in-rID/strings.xml
+++ b/app/src/main/res/values-in-rID/strings.xml
@@ -28,7 +28,7 @@
Kami butuh izin untuk memunculkan notifikasi \'Island\' di layar Anda.Izin DiberikanIzinkan Notifikasi
- Baca Data
+ Baca NotifikasiUntuk menjembatani aplikasi Anda, kami perlu membaca notifikasinya (Musik, Peta, Timer).Buka PengaturanTetap Aktif
@@ -51,6 +51,11 @@
KonfigurasiPengaturan SistemPerbaiki izin & baterai
+
+ Permanent Island
+ Keep the Dynamic Island permanently visible on screen when there are no active notifications.
+ When enabled, an empty Dynamic Island will be shown permanently on your screen. It will automatically hide when real notifications arrive and reappear when all notifications are cleared. It will not be visible in your notification shade.
+ Island WidthTentangPengembangKunjungi d4viddf.com
@@ -80,7 +85,8 @@
LainnyaPilih notifikasi aktifPesan & Umum
- Unduhan & Progres
+ Progress & Tasks
+ DownloadsMusik & MediaPeta & GPSPanggilan
@@ -95,7 +101,7 @@
Penelepon Tidak DiketahuiPanggilanPeta
- Timer
+ Pengatur waktudiunduhsukses
@@ -113,7 +119,7 @@
Panggilan MasukPenelepon Tidak DiketahuiPeta
- Timer
+ Pengatur waktuStopwatchApa yang Baru di %sKeren!
@@ -135,6 +141,7 @@
Tampilan IslandPop-up Heads-upTampilkan banner notifikasi penuh sejenak sebelum dikecilkan ke Island.
+ Berapa lama pop-up pemberitahuan tetap terlihat.Panel NotifikasiTetap tampilkan notifikasi di panel notifikasi. Jika dinonaktifkan, notifikasi hanya akan muncul sebagai Island.Penundaan Tutup Otomatis
@@ -143,11 +150,11 @@
KustomisasiTimpa Pengaturan
- arrival
- arrive
- eta
- reaching
- destination
+ kedatangan
+ tiba
+ est
+ dekat
+ tujuanOpsiRiwayat Versi
@@ -217,7 +224,7 @@
Tidak ada aturan aktifKompatibilitas SistemPerangkat: %s
- OS: %s
+ SO: %sMembutuhkan HyperOS 3.0+ (Android 15+)Perangkat Xiaomi/Redmi/Poco diperlukanMulai Otomatis (Background)
@@ -228,6 +235,14 @@
Tidak DiketahuiROM China TerdeteksiPerangkat Anda tampaknya menjalankan firmware China. Xiaomi memodifikasi internal notifikasi secara signifikan pada ROM CN. Fitur mungkin tidak stabil atau tidak berfungsi sama sekali.
+ Enable Shizuku Workaround
+ If you are on a CN ROM, you must enable the Shizuku Workaround for notifications to trigger correctly.
+ Shizuku is running
+ Shizuku is not running
+ Permission Granted
+ Permission Denied
+ Grant Shizuku Permission
+ Shizuku app is not installed or running. Please install and start Shizuku first.Versi SistemPanggilan MasukPanggilan Aktif
@@ -242,7 +257,7 @@
terima
- speaker
+ pengeras suaraaudioPembaruan Global
@@ -394,7 +409,7 @@
CariImpor dari FileImpor
- Edit
+ SuntingEksporBuat TemaEdit Tema
@@ -537,4 +552,112 @@ Penulisan ulang total logika inti untuk stabilitas yang lebih baik.\n
• Menambahkan dukungan untuk 🇮🇩 bahasa Indonesia dan 🇹🇷 Turki.\n\n
Catatan: Rilis ini mencakup perubahan arsitektur yang signifikan. Silakan laporkan bug apa pun di GitHub.
+ Pembaruan Hotfix
+ •<b>Crash Fix:</b> Resolved a critical issue that caused the app to crash when opening the settings menu in Traditional Chinese.\n
+ •<b>Under the Hood:</b> General bug fixes and stability improvements to keep your Island running smoothly.
+
+ Hue
+ Saturasi
+ Lightness
+ Warna Dinamis
+ Material You (System)
+ Extracts colors from your current wallpaper.
+ Show Island Compatible Only
+ Kill Active Widgets
+ Update Notice
+ HyperBridge has been updated! Please ensure that your Autostart and Background Battery permissions haven\'t been silently revoked by the system to keep your widgets running smoothly.
+ Mengerti
+ Direkomendasikan
+ Semua
+ Favorit
+ Pembaruan Langsung Android Asli
+ Ubah notifikasi menjadi notifikasi tugas standar Android 16 alih-alih menggantinya dengan Xiaomi Islands kustom.
+ Pembaruan Langsung Asli
+ Pratinjau
+ Desain Notifikasi
+ Xiaomi Featured Design
+ Modern HyperOS layout with Xiaomi intended design.
+ Desain Pembaruan Langsung
+ Desain notifikasi Android asli, hanya bekerja di ROM China (CN).
+ Konfigurasi mesin
+ Pratinjau
+ Desain Notifikasi
+ Bagus untuk diketahui
+ • Tata letak navigasi akan sepenuhnya terintegrasi ke dalam Mesin Tema pada pembaruan mendatang.\n• Saat menggunakan Mesin Desain Pembaruan Langsung hanya konten Sisi Kanan yang terlihat.
+ Fitur Notifikasi Xiaomi
+ Island Behavior
+
+ Engine
+ Pembaruan Langsung Android vs Xiaomi Island
+ Do Not Disturb
+ Manage island visibility during DND
+ Pause Islands
+ Manually pause all islands until you turn this back on
+ Auto-detect System DND
+ Automatically pause islands when your phone is in Do Not Disturb mode
+
+
+ Global Behavior
+ Auto-hide Island
+ Hides after a set time
+ Visible until manually dismissed
+ Determines how long the island stays on screen before disappearing automatically.
+ Behavior managed by active theme.
+ Use the default value for the notification engine
+ Active Triggers
+ Select which global events trigger the island
+ Regular messages, emails, and alerts
+ Ongoing tasks, rendering, and operations
+ Downloads, uploads, and file transfers
+ Music and video playback controls
+ Turn-by-turn directions and ETA
+ Incoming and ongoing phone calls
+ Active timers and stopwatches
+ Select triggered events
+ Override Global Triggers
+ Set custom allowed notifications for this app
+ Trigger Override
+ Select which events trigger the island for this app.
+ Enable specific triggers
+ Kumpulan lisensi kode sumber terbuka
+ Pembaruan langsung
+ Remove original notification
+ Automatically dismiss the source notification when a Live Update is active.
+ You can change these options later in Settings and Theme Configurator.
+ Add standard Android widgets to your island for quick access.
+ Fully customize colors, shapes and icons of your island.
+ Expand the possibilities with different customization options
+ Determines how the island behaves.
+ Preview of the widget
+
+ CN ROM Support\n
+ • Enable the new Sui & Shizuku workaround to enjoy the Hyper Islands in your devices running HyperOS CN (Chinese) ROMs without native support.\n\n
+ Theme Engine Improvements\n
+ • Engine Selection: You can now choose animation engines per-app or globally.\n
+ • Shizuku Workaround: Toggle added directly to the Theme Editor Engine tab.\n
+ • Behavior Controls: Custom timeouts and popup logic are now fully supported.\n\n
+ Intelligent Permanent Island\n
+ • Always Visible: Keep the island on screen even without active notifications.\n
+ • Auto-Hide: The permanent island now automatically hides itself whenever an active widget is shown on the screen.\n\n
+ Smart Icon Tinting\n
+ • New detection system automatically analyzes extracted notification icons and intelligently tints them white if they are dark or monochrome.\n\n
+ Downloads & Progress\n
+ • New Dedicated Download Layout: Separated download and progress layouts with dedicated visual designs.\n
+ • Text Progress Support: Parsed percentage text (e.g. "12%") for app updates even without native progress bars.\n
+ • Smart Filtering: Restricted text-based progress checking to store and installer downloads to prevent false triggers on other status notifications.\n\n
+ Messaging Fixes\n
+ • Fixed missing sender avatars in apps like Telegram (now correctly extracts "sender_person" data).\n
+ • Resolved issues with grouped notifications (chat summaries) not triggering islands.\n
+ • Fixed island disappearance bugs caused by notification update race conditions.\n\n
+ System & Onboarding\n
+ • New Onboarding Screen: A brand new and easier setup process for critical permissions.\n
+ • Boot Fix: Service now starts immediately and reliably after restart.\n
+ • Smarter Alerts: Downloads and Media update silently, while messaging updates reliably trigger popup animations.\n
+ • Navigation: Faster and smoother app transitions.
+
+ The Live Update
+ If you are seeing this, something went wrong!
+ If you are seeing this, something went wrong! The system couldn\'t show the widget
+
+
diff --git a/app/src/main/res/values-it-rIT/strings.xml b/app/src/main/res/values-it-rIT/strings.xml
index aa6d772..5aae089 100644
--- a/app/src/main/res/values-it-rIT/strings.xml
+++ b/app/src/main/res/values-it-rIT/strings.xml
@@ -51,6 +51,11 @@
ConfigurazioneImpostazioni di sistemaCorreggi i permessi & batteria
+
+ Permanent Island
+ Keep the Dynamic Island permanently visible on screen when there are no active notifications.
+ When enabled, an empty Dynamic Island will be shown permanently on your screen. It will automatically hide when real notifications arrive and reappear when all notifications are cleared. It will not be visible in your notification shade.
+ Island WidthInformazioniSviluppatoreVisita d4viddf.com
@@ -80,7 +85,8 @@
AltroSeleziona notifiche attiveMessaggi & Generale
- Download & Progresso
+ Progress & Tasks
+ DownloadsMusica & MediaMappe & GPSChiamate
@@ -135,6 +141,7 @@
Aspetto Dell\'isolaAvviso Pop-upMostra brevemente il banner di notifica completo prima di ridurre al minimo l\'isola.
+ Quanto tempo rimane visibile il pop-up headsup.Pannello delle NotificheMantieni la notifica visibile nel pannello di notifica. Se disabilitata, appare solo come un\'isola.Ritardo chiusura automatica
@@ -143,11 +150,11 @@
PersonalizzaSovrascrivi Impostazioni
- arrival
- arrive
- eta
- reaching
- destination
+ arrivo
+ arrivo
+ tempo stimato
+ raggiungere
+ destinazioneOpzioniCronologia versioni
@@ -230,6 +237,14 @@
SconosciutoRom Cina RilevataIl tuo dispositivo sembra aver eseguito un firmware cinese. Xiaomi modifica significativamente le notifiche interne sulle ROM CN. Le caratteristiche potrebbero essere instabili o non funzionare affatto.
+ Enable Shizuku Workaround
+ If you are on a CN ROM, you must enable the Shizuku Workaround for notifications to trigger correctly.
+ Shizuku is running
+ Shizuku is not running
+ Permission Granted
+ Permission Denied
+ Grant Shizuku Permission
+ Shizuku app is not installed or running. Please install and start Shizuku first.Versione del sistemaChiamata in entrataChiamata attiva
@@ -547,4 +562,112 @@
Nota: Questa versione include cambiamenti architettonici significativi. Si prega di segnalare eventuali bug su GitHub!
+ Aggiornamento Hotfix
+ •<b>Crash Fix:</b> Risolto un problema critico che ha causato l\'arresto dell\'app quando si apre il menu delle impostazioni in Cinese Tradizionale.\n
+ •<b> Sotto al cofano:</b> Correzioni generali di bug e miglioramenti della stabilità per mantenere la tua Isola senza intoppi.
+
+ Tonalità
+ Saturazione
+ Luminosità
+ Colori dinamici
+ Material You (Sistema)
+ Estrae i colori dallo sfondo attuale.
+ Visualizza Solo l\'isola Compatibile
+ Termina Widget Attivi
+ Avviso
+ HyperBridge è stato aggiornato! Assicurarsi che le autorizzazioni Autostart e Background Battery non siano state revocate silenziosamente dal sistema per mantenere i widget in esecuzione senza intoppi.
+ Ho capito
+ Consigliato
+ Tutto
+ Preferiti
+ Aggiornamenti Live Nativi Android
+ Trasforma le notifiche in attività standard Android 16 invece di sovrascriverle con le isole Xiaomi personalizzate.
+ Aggiornamenti Live Nativi Android
+ Anteprima
+ Design Notifiche
+ Xiaomi Design In Evidenza
+ Grafica HyperOS con design Xiaomi.
+ Design live update
+ Grafica nativa Android, funziona con ROM CN.
+ Configurazione Motore
+ Anteprima
+ Design Notifiche
+ Informazioni utili
+ • I layout di navigazione saranno completamente integrati nel motore del tema in un prossimo aggiornamento.\n• Quando si utilizza il motore Native Live Update, è visibile solo il contenuto laterale destro.
+ Notifiche Xiaomi In Evidenza
+ Comportamento Dell\'Isola
+
+ Motore
+ Aggiornamenti dal vivo vs Isola Xiaomi
+ Do Not Disturb
+ Manage island visibility during DND
+ Pause Islands
+ Manually pause all islands until you turn this back on
+ Auto-detect System DND
+ Automatically pause islands when your phone is in Do Not Disturb mode
+
+
+ Comportamento Globale
+ Nascondi Automaticamente L\'Isola
+ Nascosta dopo un tempo impostato
+ Visibile fino a scomparsa manuale
+ Determina quanto tempo l\'isola rimane sullo schermo prima di scomparire automaticamente.
+ Comportamento gestito dal tema attivo.
+ Usa il valore predefinito per il motore di notifica
+ Ponte Attivo
+ Seleziona quali eventi globali attivano l\'isola
+ Messaggi, e-mail e avvisi regolari
+ Ongoing tasks, rendering, and operations
+ Downloads, uploads, and file transfers
+ Controlli di riproduzione musicale e video
+ Direzione Turn-by-turn e ETA
+ Chiamate in arrivo e in corso
+ Timer attivi e cronometri
+ Seleziona eventi attivati
+ Sovrascrivi I Trigger Globali
+ Imposta notifiche personalizzate consentite per questa app
+ Sovrascrivi Attivazione
+ Seleziona quali eventi attivano l\'isola per questa app.
+ Abilita trigger specifici
+ Licenze open source
+ Aggiornamenti in tempo reale
+ Rimuovi notifica originale
+ Rimuovi automaticamente la notifica di origine quando è attivo un Live Update.
+ Puoi modificare queste opzioni in seguito in Impostazioni e Configuratore del tema.
+ Aggiungi widget Android standard alla tua isola per un accesso rapido.
+ Personalizza completamente colori, forme e icone della tua isola.
+ Espandi le possibilità con diverse opzioni di personalizzazione
+ Determina come si comporta l\'isola.
+ Anteprima widget
+
+ CN ROM Support\n
+ • Enable the new Sui & Shizuku workaround to enjoy the Hyper Islands in your devices running HyperOS CN (Chinese) ROMs without native support.\n\n
+ Theme Engine Improvements\n
+ • Engine Selection: You can now choose animation engines per-app or globally.\n
+ • Shizuku Workaround: Toggle added directly to the Theme Editor Engine tab.\n
+ • Behavior Controls: Custom timeouts and popup logic are now fully supported.\n\n
+ Intelligent Permanent Island\n
+ • Always Visible: Keep the island on screen even without active notifications.\n
+ • Auto-Hide: The permanent island now automatically hides itself whenever an active widget is shown on the screen.\n\n
+ Smart Icon Tinting\n
+ • New detection system automatically analyzes extracted notification icons and intelligently tints them white if they are dark or monochrome.\n\n
+ Downloads & Progress\n
+ • New Dedicated Download Layout: Separated download and progress layouts with dedicated visual designs.\n
+ • Text Progress Support: Parsed percentage text (e.g. "12%") for app updates even without native progress bars.\n
+ • Smart Filtering: Restricted text-based progress checking to store and installer downloads to prevent false triggers on other status notifications.\n\n
+ Messaging Fixes\n
+ • Fixed missing sender avatars in apps like Telegram (now correctly extracts "sender_person" data).\n
+ • Resolved issues with grouped notifications (chat summaries) not triggering islands.\n
+ • Fixed island disappearance bugs caused by notification update race conditions.\n\n
+ System & Onboarding\n
+ • New Onboarding Screen: A brand new and easier setup process for critical permissions.\n
+ • Boot Fix: Service now starts immediately and reliably after restart.\n
+ • Smarter Alerts: Downloads and Media update silently, while messaging updates reliably trigger popup animations.\n
+ • Navigation: Faster and smoother app transitions.
+
+ Il Live Update
+ Se stai vedendo questo, qualcosa è andato storto!
+ Se stai vedendo questo, qualcosa è andato storto! Il sistema non ha potuto mostrare il widget
+
+
diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml
index 596c328..48a56b1 100644
--- a/app/src/main/res/values-ko-rKR/strings.xml
+++ b/app/src/main/res/values-ko-rKR/strings.xml
@@ -51,6 +51,11 @@
구성시스템 설정권한 및 배터리 문제 해결
+
+ Permanent Island
+ Keep the Dynamic Island permanently visible on screen when there are no active notifications.
+ When enabled, an empty Dynamic Island will be shown permanently on your screen. It will automatically hide when real notifications arrive and reappear when all notifications are cleared. It will not be visible in your notification shade.
+ Island Width앱 정보개발자d4viddf.com 방문
@@ -80,7 +85,8 @@
기타활성 알림 선택메시지 및 일반
- 다운로드 및 진행 상황
+ Progress & Tasks
+ Downloads음악 및 미디어지도 및 GPS통화
@@ -135,6 +141,7 @@
섬 외형플로팅 섬 표시팝업 알림 표시
+ How long the heads-up popup stays visible.알림창에 표시Keep the notification visible in the notification panel. If disabled, it only appears as an Island.Auto-Close delay
@@ -230,6 +237,14 @@
알 수 없음중국(CN) 롬 감지됨기기가 중국 롬을 사용 중입니다. Xiaomi는 이 버전에서 알림 구조를 크게 수정하므로 기능이 불안정하거나 작동하지 않을 수 있습니다.
+ Enable Shizuku Workaround
+ If you are on a CN ROM, you must enable the Shizuku Workaround for notifications to trigger correctly.
+ Shizuku is running
+ Shizuku is not running
+ Permission Granted
+ Permission Denied
+ Grant Shizuku Permission
+ Shizuku app is not installed or running. Please install and start Shizuku first.시스템 버전수신 전화통화 중
@@ -537,4 +552,112 @@
Note: This release includes significant architectural changes. Please report any bugs on GitHub!
+ Hotfix Update
+ •<b>Crash Fix:</b> Resolved a critical issue that caused the app to crash when opening the settings menu in Traditional Chinese.\n
+ •<b>Under the Hood:</b> General bug fixes and stability improvements to keep your Island running smoothly.
+
+ Hue
+ Saturation
+ Lightness
+ Dynamic Colors
+ Material You (System)
+ Extracts colors from your current wallpaper.
+ Show Island Compatible Only
+ Kill Active Widgets
+ Update Notice
+ HyperBridge has been updated! Please ensure that your Autostart and Background Battery permissions haven\'t been silently revoked by the system to keep your widgets running smoothly.
+ Got it
+ Recommended
+ All
+ Favorites
+ Native Android Live Updates
+ Transform notifications into standard Android 16 ongoing tasks instead of overriding them with custom Xiaomi Islands.
+ Native Live Updates
+ Preview
+ Notification Design
+ Xiaomi Featured Design
+ Modern HyperOS layout with Xiaomi intended design.
+ Live Update Design
+ Native android notification designs, works on CN ROM.
+ Engine Configuration
+ Preview
+ Notification Design
+ Good to know
+ • Navigation layouts will be fully integrated into the Theme Engine in an upcoming update.\n• When using the Native Live Update engine, only the Right Side content is visible.
+ Xiaomi Featured Notifications
+ Island Behavior
+
+ Engine
+ Live Updates vs Xiaomi Island
+ Do Not Disturb
+ Manage island visibility during DND
+ Pause Islands
+ Manually pause all islands until you turn this back on
+ Auto-detect System DND
+ Automatically pause islands when your phone is in Do Not Disturb mode
+
+
+ Global Behavior
+ Auto-hide Island
+ Hides after a set time
+ Visible until manually dismissed
+ Determines how long the island stays on screen before disappearing automatically.
+ Behavior managed by active theme.
+ Use the default value for the notification engine
+ Active Triggers
+ Select which global events trigger the island
+ Regular messages, emails, and alerts
+ Ongoing tasks, rendering, and operations
+ Downloads, uploads, and file transfers
+ Music and video playback controls
+ Turn-by-turn directions and ETA
+ Incoming and ongoing phone calls
+ Active timers and stopwatches
+ Select triggered events
+ Override Global Triggers
+ Set custom allowed notifications for this app
+ Trigger Override
+ Select which events trigger the island for this app.
+ Enable specific triggers
+ Open Source Licenses
+ Live updates
+ Remove original notification
+ Automatically dismiss the source notification when a Live Update is active.
+ You can change these options later in Settings and Theme Configurator.
+ Add standard Android widgets to your island for quick access.
+ Fully customize colors, shapes and icons of your island.
+ Expand the possibilities with different customization options
+ Determines how the island behaves.
+ Preview of the widget
+
+ CN ROM Support\n
+ • Enable the new Sui & Shizuku workaround to enjoy the Hyper Islands in your devices running HyperOS CN (Chinese) ROMs without native support.\n\n
+ Theme Engine Improvements\n
+ • Engine Selection: You can now choose animation engines per-app or globally.\n
+ • Shizuku Workaround: Toggle added directly to the Theme Editor Engine tab.\n
+ • Behavior Controls: Custom timeouts and popup logic are now fully supported.\n\n
+ Intelligent Permanent Island\n
+ • Always Visible: Keep the island on screen even without active notifications.\n
+ • Auto-Hide: The permanent island now automatically hides itself whenever an active widget is shown on the screen.\n\n
+ Smart Icon Tinting\n
+ • New detection system automatically analyzes extracted notification icons and intelligently tints them white if they are dark or monochrome.\n\n
+ Downloads & Progress\n
+ • New Dedicated Download Layout: Separated download and progress layouts with dedicated visual designs.\n
+ • Text Progress Support: Parsed percentage text (e.g. "12%") for app updates even without native progress bars.\n
+ • Smart Filtering: Restricted text-based progress checking to store and installer downloads to prevent false triggers on other status notifications.\n\n
+ Messaging Fixes\n
+ • Fixed missing sender avatars in apps like Telegram (now correctly extracts "sender_person" data).\n
+ • Resolved issues with grouped notifications (chat summaries) not triggering islands.\n
+ • Fixed island disappearance bugs caused by notification update race conditions.\n\n
+ System & Onboarding\n
+ • New Onboarding Screen: A brand new and easier setup process for critical permissions.\n
+ • Boot Fix: Service now starts immediately and reliably after restart.\n
+ • Smarter Alerts: Downloads and Media update silently, while messaging updates reliably trigger popup animations.\n
+ • Navigation: Faster and smoother app transitions.
+
+ The Live Update
+ If you are seeing this, something went wrong!
+ If you are seeing this, something went wrong! The system couldn\'t show the widget
+
+
diff --git a/app/src/main/res/values-pl-rPL/strings.xml b/app/src/main/res/values-pl-rPL/strings.xml
index 4a8143b..f2c8e4d 100644
--- a/app/src/main/res/values-pl-rPL/strings.xml
+++ b/app/src/main/res/values-pl-rPL/strings.xml
@@ -51,6 +51,11 @@
KonfiguracjaUstawienia systemuSprawdź poprawność uprawnień
+
+ Permanent Island
+ Keep the Dynamic Island permanently visible on screen when there are no active notifications.
+ When enabled, an empty Dynamic Island will be shown permanently on your screen. It will automatically hide when real notifications arrive and reappear when all notifications are cleared. It will not be visible in your notification shade.
+ Island WidthO aplikacjiAutorOdwiedź d4viddf.com
@@ -80,7 +85,8 @@
InneWybierz aktywne powiadomieniaWiadomości i ogólne
- Pobieranie i postęp
+ Progress & Tasks
+ DownloadsMuzyka i multimediaMapy i GPSPołączenia
@@ -135,6 +141,7 @@
Pojawianie się wyspPopup typu heads-upPokaż powiadomienie, które zostanie potem zminimalizowane do formy wyspy.
+ How long the heads-up popup stays visible.Panel powiadomieńZachowaj powiadomienie w Panelu powiadomień. Jeśli opcja jest wyłączona, pojawi się ono tylko jako wyspa.Opóźnienie automatycznego zamknięcia
@@ -230,6 +237,14 @@
NieznanyWykryto ROM ChinaTwoje urządzenie prawdopodobnie działa na ROM-ie China. W tej wersji Xiaomi znacznie modyfikuje działanie powiadomień. Funkcje mogą być niestabilne lub w ogóle nie działać.
+ Enable Shizuku Workaround
+ If you are on a CN ROM, you must enable the Shizuku Workaround for notifications to trigger correctly.
+ Shizuku is running
+ Shizuku is not running
+ Permission Granted
+ Permission Denied
+ Grant Shizuku Permission
+ Shizuku app is not installed or running. Please install and start Shizuku first.Wersja systemuPołączenie przychodząceAktywne połączenie
@@ -557,4 +572,112 @@
Uwaga: Ta wersja zawiera istotne zmiany architektoniczne. Prosimy o zgłaszanie wszelkich błędów na GitHubie!
+ Hotfix Update
+ •<b>Crash Fix:</b> Resolved a critical issue that caused the app to crash when opening the settings menu in Traditional Chinese.\n
+ •<b>Under the Hood:</b> General bug fixes and stability improvements to keep your Island running smoothly.
+
+ Hue
+ Saturation
+ Lightness
+ Dynamic Colors
+ Material You (System)
+ Extracts colors from your current wallpaper.
+ Show Island Compatible Only
+ Kill Active Widgets
+ Update Notice
+ HyperBridge has been updated! Please ensure that your Autostart and Background Battery permissions haven\'t been silently revoked by the system to keep your widgets running smoothly.
+ Got it
+ Recommended
+ All
+ Favorites
+ Native Android Live Updates
+ Transform notifications into standard Android 16 ongoing tasks instead of overriding them with custom Xiaomi Islands.
+ Native Live Updates
+ Preview
+ Notification Design
+ Xiaomi Featured Design
+ Modern HyperOS layout with Xiaomi intended design.
+ Live Update Design
+ Native android notification designs, works on CN ROM.
+ Engine Configuration
+ Preview
+ Notification Design
+ Good to know
+ • Navigation layouts will be fully integrated into the Theme Engine in an upcoming update.\n• When using the Native Live Update engine, only the Right Side content is visible.
+ Xiaomi Featured Notifications
+ Island Behavior
+
+ Engine
+ Live Updates vs Xiaomi Island
+ Do Not Disturb
+ Manage island visibility during DND
+ Pause Islands
+ Manually pause all islands until you turn this back on
+ Auto-detect System DND
+ Automatically pause islands when your phone is in Do Not Disturb mode
+
+
+ Global Behavior
+ Auto-hide Island
+ Hides after a set time
+ Visible until manually dismissed
+ Determines how long the island stays on screen before disappearing automatically.
+ Behavior managed by active theme.
+ Use the default value for the notification engine
+ Active Triggers
+ Select which global events trigger the island
+ Regular messages, emails, and alerts
+ Ongoing tasks, rendering, and operations
+ Downloads, uploads, and file transfers
+ Music and video playback controls
+ Turn-by-turn directions and ETA
+ Incoming and ongoing phone calls
+ Active timers and stopwatches
+ Select triggered events
+ Override Global Triggers
+ Set custom allowed notifications for this app
+ Trigger Override
+ Select which events trigger the island for this app.
+ Enable specific triggers
+ Open Source Licenses
+ Live updates
+ Remove original notification
+ Automatically dismiss the source notification when a Live Update is active.
+ You can change these options later in Settings and Theme Configurator.
+ Add standard Android widgets to your island for quick access.
+ Fully customize colors, shapes and icons of your island.
+ Expand the possibilities with different customization options
+ Determines how the island behaves.
+ Preview of the widget
+
+ CN ROM Support\n
+ • Enable the new Sui & Shizuku workaround to enjoy the Hyper Islands in your devices running HyperOS CN (Chinese) ROMs without native support.\n\n
+ Theme Engine Improvements\n
+ • Engine Selection: You can now choose animation engines per-app or globally.\n
+ • Shizuku Workaround: Toggle added directly to the Theme Editor Engine tab.\n
+ • Behavior Controls: Custom timeouts and popup logic are now fully supported.\n\n
+ Intelligent Permanent Island\n
+ • Always Visible: Keep the island on screen even without active notifications.\n
+ • Auto-Hide: The permanent island now automatically hides itself whenever an active widget is shown on the screen.\n\n
+ Smart Icon Tinting\n
+ • New detection system automatically analyzes extracted notification icons and intelligently tints them white if they are dark or monochrome.\n\n
+ Downloads & Progress\n
+ • New Dedicated Download Layout: Separated download and progress layouts with dedicated visual designs.\n
+ • Text Progress Support: Parsed percentage text (e.g. "12%") for app updates even without native progress bars.\n
+ • Smart Filtering: Restricted text-based progress checking to store and installer downloads to prevent false triggers on other status notifications.\n\n
+ Messaging Fixes\n
+ • Fixed missing sender avatars in apps like Telegram (now correctly extracts "sender_person" data).\n
+ • Resolved issues with grouped notifications (chat summaries) not triggering islands.\n
+ • Fixed island disappearance bugs caused by notification update race conditions.\n\n
+ System & Onboarding\n
+ • New Onboarding Screen: A brand new and easier setup process for critical permissions.\n
+ • Boot Fix: Service now starts immediately and reliably after restart.\n
+ • Smarter Alerts: Downloads and Media update silently, while messaging updates reliably trigger popup animations.\n
+ • Navigation: Faster and smoother app transitions.
+
+ The Live Update
+ If you are seeing this, something went wrong!
+ If you are seeing this, something went wrong! The system couldn\'t show the widget
+
+
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 04a2ab2..3664bc0 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -51,6 +51,11 @@
ConfiguraçãoConfiguração do SistemaCorrigir permissões e bateria
+
+ Permanent Island
+ Keep the Dynamic Island permanently visible on screen when there are no active notifications.
+ When enabled, an empty Dynamic Island will be shown permanently on your screen. It will automatically hide when real notifications arrive and reappear when all notifications are cleared. It will not be visible in your notification shade.
+ Island WidthSobreDesenvolvedorVisite d4viddf.com
@@ -81,6 +86,7 @@
Selecionar notificações ativasMensagens e GeralDownloads e Progresso
+ DownloadsMúsica e MídiaMapas e GPSChamadas
@@ -135,6 +141,7 @@
Aparência da IlhaPop-up FlutuanteMostra o banner completo da notificação brevemente antes de minimizar para a Ilha.
+ How long the heads-up popup stays visible.Painel de NotificaçãoMantém a notificação visível no painel do sistema. Se desativado, ela aparecerá apenas como uma Ilha.Delay para fechar automaticamente
@@ -143,11 +150,11 @@
PersonalizarSubstituir Configurações
- arrival
- arrive
+ chegada
+ chegareta
- reaching
- destination
+ chegando
+ destinoOpçõesHistórico de Versões
@@ -231,6 +238,14 @@ Novas notificações substituem as mais antigas automaticamente.DesconhecidoROM China DetectadaSeu dispositivo parece estar rodando uma firmware chinesa. A Xiaomi modifica significativamente as entranhas das notificações em ROMs CN. Os recursos podem ser instáveis ou não funcionar.
+ Enable Shizuku Workaround
+ If you are on a CN ROM, you must enable the Shizuku Workaround for notifications to trigger correctly.
+ Shizuku is running
+ Shizuku is not running
+ Permission Granted
+ Permission Denied
+ Grant Shizuku Permission
+ Shizuku app is not installed or running. Please install and start Shizuku first.Versão do SistemaChamada RecebidaChamada Ativa
@@ -246,7 +261,7 @@ Novas notificações substituem as mais antigas automaticamente.palestrante
- audio
+ áudioA Atualização GlobalNotificações Ocultas 👻\n
@@ -555,4 +570,112 @@ Por favor, verifique manualmente para garantir que o serviço funcione corretame
Nota: Esta atualização inclui mudanças significativas na arquitetura do app. Por favor reporte qualquer bug no GitHub!
+ Atualização de correção
+ •<b>Crash Fix:</b> Resolved a critical issue that caused the app to crash when opening the settings menu in Traditional Chinese.\n
+ •<b>Under the Hood:</b> General bug fixes and stability improvements to keep your Island running smoothly.
+
+ Matiz
+ Saturação
+ Luminosidade
+ Cores dinâmicas
+ Material You (Sistema)
+ Extrai as cores do seu papel de parede atual.
+ Mostrar apenas compatíveis com a ilha
+ Encerrar todos os widgets ativos
+ Aviso de atualização
+ HyperBridge atualizou! Por favor, certifique-se de que o auto início e as configurações de bateria não foram alteradas pelo sistema para manter seus widgets rodando tranquilamente.
+ Certo
+ Recomendado
+ Tudo
+ Favoritos
+ Native Android Live Updates
+
+ Native Live Updates
+ Preview
+ Notification Design
+ Xiaomi Featured Design
+ Modern HyperOS layout with Xiaomi intended design.
+ Live Update Design
+ Native android notification designs, works on CN ROM.
+ Engine Configuration
+ Preview
+ Notification Design
+ Good to know
+ • Navigation layouts will be fully integrated into the Theme Engine in an upcoming update.\n• When using the Native Live Update engine, only the Right Side content is visible.
+ Xiaomi Featured Notifications
+ Island Behavior
+
+ Engine
+ Live Updates vs Xiaomi Island
+ Do Not Disturb
+ Manage island visibility during DND
+ Pause Islands
+ Manually pause all islands until you turn this back on
+ Auto-detect System DND
+ Automatically pause islands when your phone is in Do Not Disturb mode
+
+
+ Global Behavior
+ Auto-hide Island
+ Hides after a set time
+ Visible until manually dismissed
+ Determines how long the island stays on screen before disappearing automatically.
+ Behavior managed by active theme.
+ Use the default value for the notification engine
+ Active Triggers
+ Select which global events trigger the island
+ Regular messages, emails, and alerts
+ Downloads, uploads, and ongoing tasks
+ Downloads, uploads, and file transfers
+ Music and video playback controls
+ Turn-by-turn directions and ETA
+ Incoming and ongoing phone calls
+ Active timers and stopwatches
+ Select triggered events
+ Override Global Triggers
+ Set custom allowed notifications for this app
+ Trigger Override
+ Select which events trigger the island for this app.
+ Enable specific triggers
+ Open Source Licenses
+ Live updates
+ Remove original notification
+ Automatically dismiss the source notification when a Live Update is active.
+ You can change these options later in Settings and Theme Configurator.
+ Add standard Android widgets to your island for quick access.
+ Fully customize colors, shapes and icons of your island.
+ Expand the possibilities with different customization options
+ Determines how the island behaves.
+ Preview of the widget
+
+ CN ROM Support\n
+ • Enable the new Sui & Shizuku workaround to enjoy the Hyper Islands in your devices running HyperOS CN (Chinese) ROMs without native support.\n\n
+ Theme Engine Improvements\n
+ • Engine Selection: You can now choose animation engines per-app or globally.\n
+ • Shizuku Workaround: Toggle added directly to the Theme Editor Engine tab.\n
+ • Behavior Controls: Custom timeouts and popup logic are now fully supported.\n\n
+ Intelligent Permanent Island\n
+ • Always Visible: Keep the island on screen even without active notifications.\n
+ • Auto-Hide: The permanent island now automatically hides itself whenever an active widget is shown on the screen.\n\n
+ Smart Icon Tinting\n
+ • New detection system automatically analyzes extracted notification icons and intelligently tints them white if they are dark or monochrome.\n\n
+ Downloads & Progress\n
+ • New Dedicated Download Layout: Separated download and progress layouts with dedicated visual designs.\n
+ • Text Progress Support: Parsed percentage text (e.g. "12%") for app updates even without native progress bars.\n
+ • Smart Filtering: Restricted text-based progress checking to store and installer downloads to prevent false triggers on other status notifications.\n\n
+ Messaging Fixes\n
+ • Fixed missing sender avatars in apps like Telegram (now correctly extracts "sender_person" data).\n
+ • Resolved issues with grouped notifications (chat summaries) not triggering islands.\n
+ • Fixed island disappearance bugs caused by notification update race conditions.\n\n
+ System & Onboarding\n
+ • New Onboarding Screen: A brand new and easier setup process for critical permissions.\n
+ • Boot Fix: Service now starts immediately and reliably after restart.\n
+ • Smarter Alerts: Downloads and Media update silently, while messaging updates reliably trigger popup animations.\n
+ • Navigation: Faster and smoother app transitions.
+
+ The Live Update
+ If you are seeing this, something went wrong!
+ If you are seeing this, something went wrong! The system couldn\'t show the widget
+
+
diff --git a/app/src/main/res/values-ru-rRU/strings.xml b/app/src/main/res/values-ru-rRU/strings.xml
index efe80bb..94da968 100644
--- a/app/src/main/res/values-ru-rRU/strings.xml
+++ b/app/src/main/res/values-ru-rRU/strings.xml
@@ -51,6 +51,11 @@
КонфигурацияНастройки системыИсправить разрешения и питание
+
+ Permanent Island
+ Keep the Dynamic Island permanently visible on screen when there are no active notifications.
+ When enabled, an empty Dynamic Island will be shown permanently on your screen. It will automatically hide when real notifications arrive and reappear when all notifications are cleared. It will not be visible in your notification shade.
+ Island WidthО приложенииРазработчикПосетить d4viddf.com
@@ -80,7 +85,8 @@
ДругоеВыберите активные уведомленияСообщения и общее
- Загрузки и прогресс
+ Progress & Tasks
+ DownloadsМузыка и медиаКарты и GPSЗвонки
@@ -135,6 +141,7 @@
Внешний вид ОстроваПлавающий ОстровПоказывать всплывающее уведомление.
+ How long the heads-up popup stays visible.Показывать в шторкеKeep the notification visible in the notification panel. If disabled, it only appears as an Island.Auto-Close delay
@@ -230,6 +237,14 @@
НеизвестноОбнаружена китайская прошивка (CN ROM)Похоже, ваше устройство работает на китайской прошивке, в которой Xiaomi значительно изменяет внутреннюю структуру уведомлений. Функции могут быть нестабильными или вообще не работать.
+ Enable Shizuku Workaround
+ If you are on a CN ROM, you must enable the Shizuku Workaround for notifications to trigger correctly.
+ Shizuku is running
+ Shizuku is not running
+ Permission Granted
+ Permission Denied
+ Grant Shizuku Permission
+ Shizuku app is not installed or running. Please install and start Shizuku first.Версия системыВходящий звонокАктивный звонок
@@ -558,4 +573,112 @@
Note: This release includes significant architectural changes. Please report any bugs on GitHub!
+ Hotfix Update
+ •<b>Crash Fix:</b> Resolved a critical issue that caused the app to crash when opening the settings menu in Traditional Chinese.\n
+ •<b>Under the Hood:</b> General bug fixes and stability improvements to keep your Island running smoothly.
+
+ Hue
+ Saturation
+ Lightness
+ Dynamic Colors
+ Material You (System)
+ Extracts colors from your current wallpaper.
+ Show Island Compatible Only
+ Kill Active Widgets
+ Update Notice
+ HyperBridge has been updated! Please ensure that your Autostart and Background Battery permissions haven\'t been silently revoked by the system to keep your widgets running smoothly.
+ Got it
+ Recommended
+ All
+ Favorites
+ Native Android Live Updates
+ Transform notifications into standard Android 16 ongoing tasks instead of overriding them with custom Xiaomi Islands.
+ Native Live Updates
+ Preview
+ Notification Design
+ Xiaomi Featured Design
+ Modern HyperOS layout with Xiaomi intended design.
+ Live Update Design
+ Native android notification designs, works on CN ROM.
+ Engine Configuration
+ Preview
+ Notification Design
+ Good to know
+ • Navigation layouts will be fully integrated into the Theme Engine in an upcoming update.\n• When using the Native Live Update engine, only the Right Side content is visible.
+ Xiaomi Featured Notifications
+ Island Behavior
+
+ Engine
+ Live Updates vs Xiaomi Island
+ Do Not Disturb
+ Manage island visibility during DND
+ Pause Islands
+ Manually pause all islands until you turn this back on
+ Auto-detect System DND
+ Automatically pause islands when your phone is in Do Not Disturb mode
+
+
+ Global Behavior
+ Auto-hide Island
+ Hides after a set time
+ Visible until manually dismissed
+ Determines how long the island stays on screen before disappearing automatically.
+ Behavior managed by active theme.
+ Use the default value for the notification engine
+ Active Triggers
+ Select which global events trigger the island
+ Regular messages, emails, and alerts
+ Ongoing tasks, rendering, and operations
+ Downloads, uploads, and file transfers
+ Music and video playback controls
+ Turn-by-turn directions and ETA
+ Incoming and ongoing phone calls
+ Active timers and stopwatches
+ Select triggered events
+ Override Global Triggers
+ Set custom allowed notifications for this app
+ Trigger Override
+ Select which events trigger the island for this app.
+ Enable specific triggers
+ Open Source Licenses
+ Live updates
+ Remove original notification
+ Automatically dismiss the source notification when a Live Update is active.
+ You can change these options later in Settings and Theme Configurator.
+ Add standard Android widgets to your island for quick access.
+ Fully customize colors, shapes and icons of your island.
+ Expand the possibilities with different customization options
+ Determines how the island behaves.
+ Preview of the widget
+
+ CN ROM Support\n
+ • Enable the new Sui & Shizuku workaround to enjoy the Hyper Islands in your devices running HyperOS CN (Chinese) ROMs without native support.\n\n
+ Theme Engine Improvements\n
+ • Engine Selection: You can now choose animation engines per-app or globally.\n
+ • Shizuku Workaround: Toggle added directly to the Theme Editor Engine tab.\n
+ • Behavior Controls: Custom timeouts and popup logic are now fully supported.\n\n
+ Intelligent Permanent Island\n
+ • Always Visible: Keep the island on screen even without active notifications.\n
+ • Auto-Hide: The permanent island now automatically hides itself whenever an active widget is shown on the screen.\n\n
+ Smart Icon Tinting\n
+ • New detection system automatically analyzes extracted notification icons and intelligently tints them white if they are dark or monochrome.\n\n
+ Downloads & Progress\n
+ • New Dedicated Download Layout: Separated download and progress layouts with dedicated visual designs.\n
+ • Text Progress Support: Parsed percentage text (e.g. "12%") for app updates even without native progress bars.\n
+ • Smart Filtering: Restricted text-based progress checking to store and installer downloads to prevent false triggers on other status notifications.\n\n
+ Messaging Fixes\n
+ • Fixed missing sender avatars in apps like Telegram (now correctly extracts "sender_person" data).\n
+ • Resolved issues with grouped notifications (chat summaries) not triggering islands.\n
+ • Fixed island disappearance bugs caused by notification update race conditions.\n\n
+ System & Onboarding\n
+ • New Onboarding Screen: A brand new and easier setup process for critical permissions.\n
+ • Boot Fix: Service now starts immediately and reliably after restart.\n
+ • Smarter Alerts: Downloads and Media update silently, while messaging updates reliably trigger popup animations.\n
+ • Navigation: Faster and smoother app transitions.
+
+ The Live Update
+ If you are seeing this, something went wrong!
+ If you are seeing this, something went wrong! The system couldn\'t show the widget
+
+
diff --git a/app/src/main/res/values-sk-rSK/strings.xml b/app/src/main/res/values-sk-rSK/strings.xml
index b8404f5..b9640e0 100644
--- a/app/src/main/res/values-sk-rSK/strings.xml
+++ b/app/src/main/res/values-sk-rSK/strings.xml
@@ -51,6 +51,11 @@
KonfiguráciaNastavenie systémuOprava povolení & batérie
+
+ Permanent Island
+ Keep the Dynamic Island permanently visible on screen when there are no active notifications.
+ When enabled, an empty Dynamic Island will be shown permanently on your screen. It will automatically hide when real notifications arrive and reappear when all notifications are cleared. It will not be visible in your notification shade.
+ Island WidthO násVývojárNavštívte d4viddf.com
@@ -80,7 +85,8 @@
OstatnéVybrať aktívne upozorneniaSprávy & Všeobecné
- Stiahnutia & Priebeh
+ Progress & Tasks
+ DownloadsHudba & MédiáMapy & GPSHovory
@@ -135,6 +141,7 @@
Vzhľad IslanduVyskakovacie okno s upozornenímKrátko zobrazí celý notifikačný banner pred minimalizáciou Islandu.
+ How long the heads-up popup stays visible.Panel oznámeníUdržuje upozornenie viditeľné v paneli oznámení. Ak je vypnuté, zobrazí sa iba ako Ostrov.Oneskorenie automatického zatvorenia
@@ -230,6 +237,14 @@
NeznámeZistená čínska ROMZdá sa, že vaše zariadenie používa čínsky firmvér. Xiaomi výrazne upravuje vnútorné funkcie upozornení na CN ROM. Funkcie môžu byť nestabilné alebo nemusia vôbec fungovať.
+ Enable Shizuku Workaround
+ If you are on a CN ROM, you must enable the Shizuku Workaround for notifications to trigger correctly.
+ Shizuku is running
+ Shizuku is not running
+ Permission Granted
+ Permission Denied
+ Grant Shizuku Permission
+ Shizuku app is not installed or running. Please install and start Shizuku first.Verzia systémuPrichádzajúci hovorAktívny hovor
@@ -557,4 +572,112 @@
Poznámka: Toto vydanie obsahuje významné architektonické zmeny. Prosím, nahláste akékoľvek chyby na GitHub!
+ Hotfix Update
+ •<b>Crash Fix:</b> Resolved a critical issue that caused the app to crash when opening the settings menu in Traditional Chinese.\n
+ •<b>Under the Hood:</b> General bug fixes and stability improvements to keep your Island running smoothly.
+
+ Hue
+ Saturation
+ Lightness
+ Dynamic Colors
+ Material You (System)
+ Extracts colors from your current wallpaper.
+ Show Island Compatible Only
+ Kill Active Widgets
+ Update Notice
+ HyperBridge has been updated! Please ensure that your Autostart and Background Battery permissions haven\'t been silently revoked by the system to keep your widgets running smoothly.
+ Got it
+ Recommended
+ All
+ Favorites
+ Native Android Live Updates
+ Transform notifications into standard Android 16 ongoing tasks instead of overriding them with custom Xiaomi Islands.
+ Native Live Updates
+ Preview
+ Notification Design
+ Xiaomi Featured Design
+ Modern HyperOS layout with Xiaomi intended design.
+ Live Update Design
+ Native android notification designs, works on CN ROM.
+ Engine Configuration
+ Preview
+ Notification Design
+ Good to know
+ • Navigation layouts will be fully integrated into the Theme Engine in an upcoming update.\n• When using the Native Live Update engine, only the Right Side content is visible.
+ Xiaomi Featured Notifications
+ Island Behavior
+
+ Engine
+ Live Updates vs Xiaomi Island
+ Do Not Disturb
+ Manage island visibility during DND
+ Pause Islands
+ Manually pause all islands until you turn this back on
+ Auto-detect System DND
+ Automatically pause islands when your phone is in Do Not Disturb mode
+
+
+ Global Behavior
+ Auto-hide Island
+ Hides after a set time
+ Visible until manually dismissed
+ Determines how long the island stays on screen before disappearing automatically.
+ Behavior managed by active theme.
+ Use the default value for the notification engine
+ Active Triggers
+ Select which global events trigger the island
+ Regular messages, emails, and alerts
+ Ongoing tasks, rendering, and operations
+ Downloads, uploads, and file transfers
+ Music and video playback controls
+ Turn-by-turn directions and ETA
+ Incoming and ongoing phone calls
+ Active timers and stopwatches
+ Select triggered events
+ Override Global Triggers
+ Set custom allowed notifications for this app
+ Trigger Override
+ Select which events trigger the island for this app.
+ Enable specific triggers
+ Open Source Licenses
+ Live updates
+ Remove original notification
+ Automatically dismiss the source notification when a Live Update is active.
+ You can change these options later in Settings and Theme Configurator.
+ Add standard Android widgets to your island for quick access.
+ Fully customize colors, shapes and icons of your island.
+ Expand the possibilities with different customization options
+ Determines how the island behaves.
+ Preview of the widget
+
+ CN ROM Support\n
+ • Enable the new Sui & Shizuku workaround to enjoy the Hyper Islands in your devices running HyperOS CN (Chinese) ROMs without native support.\n\n
+ Theme Engine Improvements\n
+ • Engine Selection: You can now choose animation engines per-app or globally.\n
+ • Shizuku Workaround: Toggle added directly to the Theme Editor Engine tab.\n
+ • Behavior Controls: Custom timeouts and popup logic are now fully supported.\n\n
+ Intelligent Permanent Island\n
+ • Always Visible: Keep the island on screen even without active notifications.\n
+ • Auto-Hide: The permanent island now automatically hides itself whenever an active widget is shown on the screen.\n\n
+ Smart Icon Tinting\n
+ • New detection system automatically analyzes extracted notification icons and intelligently tints them white if they are dark or monochrome.\n\n
+ Downloads & Progress\n
+ • New Dedicated Download Layout: Separated download and progress layouts with dedicated visual designs.\n
+ • Text Progress Support: Parsed percentage text (e.g. "12%") for app updates even without native progress bars.\n
+ • Smart Filtering: Restricted text-based progress checking to store and installer downloads to prevent false triggers on other status notifications.\n\n
+ Messaging Fixes\n
+ • Fixed missing sender avatars in apps like Telegram (now correctly extracts "sender_person" data).\n
+ • Resolved issues with grouped notifications (chat summaries) not triggering islands.\n
+ • Fixed island disappearance bugs caused by notification update race conditions.\n\n
+ System & Onboarding\n
+ • New Onboarding Screen: A brand new and easier setup process for critical permissions.\n
+ • Boot Fix: Service now starts immediately and reliably after restart.\n
+ • Smarter Alerts: Downloads and Media update silently, while messaging updates reliably trigger popup animations.\n
+ • Navigation: Faster and smoother app transitions.
+
+ The Live Update
+ If you are seeing this, something went wrong!
+ If you are seeing this, something went wrong! The system couldn\'t show the widget
+
+
diff --git a/app/src/main/res/values-tr-rTR/strings.xml b/app/src/main/res/values-tr-rTR/strings.xml
index 2387c9d..3306e4a 100644
--- a/app/src/main/res/values-tr-rTR/strings.xml
+++ b/app/src/main/res/values-tr-rTR/strings.xml
@@ -51,6 +51,11 @@
ConfigurationSystem SetupFix permissions & battery
+
+ Permanent Island
+ Keep the Dynamic Island permanently visible on screen when there are no active notifications.
+ When enabled, an empty Dynamic Island will be shown permanently on your screen. It will automatically hide when real notifications arrive and reappear when all notifications are cleared. It will not be visible in your notification shade.
+ Island WidthAboutDeveloperVisit d4viddf.com
@@ -80,7 +85,8 @@
OtherSelect active notificationsMessages & General
- Downloads & Progress
+ Progress & Tasks
+ DownloadsMusic & MediaMaps & GPSCalls
@@ -135,6 +141,7 @@
Island AppearanceHeads-up PopupShow the full notification banner briefly before minimizing to the Island.
+ How long the heads-up popup stays visible.Notification PanelKeep the notification visible in the notification panel. If disabled, it only appears as an Island.Auto-Close delay
@@ -230,6 +237,14 @@
UnknownChina ROM DetectedYour device appears to be running a Chinese firmware. Xiaomi modifies notification internals significantly on CN ROMs. Features might be unstable or not work at all.
+ Enable Shizuku Workaround
+ If you are on a CN ROM, you must enable the Shizuku Workaround for notifications to trigger correctly.
+ Shizuku is running
+ Shizuku is not running
+ Permission Granted
+ Permission Denied
+ Grant Shizuku Permission
+ Shizuku app is not installed or running. Please install and start Shizuku first.System VersionIncoming CallActive Call
@@ -547,4 +562,112 @@
Note: This release includes significant architectural changes. Please report any bugs on GitHub!
+ Hotfix Update
+ •<b>Crash Fix:</b> Resolved a critical issue that caused the app to crash when opening the settings menu in Traditional Chinese.\n
+ •<b>Under the Hood:</b> General bug fixes and stability improvements to keep your Island running smoothly.
+
+ Hue
+ Saturation
+ Lightness
+ Dynamic Colors
+ Material You (System)
+ Extracts colors from your current wallpaper.
+ Show Island Compatible Only
+ Kill Active Widgets
+ Update Notice
+ HyperBridge has been updated! Please ensure that your Autostart and Background Battery permissions haven\'t been silently revoked by the system to keep your widgets running smoothly.
+ Got it
+ Recommended
+ All
+ Favorites
+ Native Android Live Updates
+ Transform notifications into standard Android 16 ongoing tasks instead of overriding them with custom Xiaomi Islands.
+ Native Live Updates
+ Preview
+ Notification Design
+ Xiaomi Featured Design
+ Modern HyperOS layout with Xiaomi intended design.
+ Live Update Design
+ Native android notification designs, works on CN ROM.
+ Engine Configuration
+ Preview
+ Notification Design
+ Good to know
+ • Navigation layouts will be fully integrated into the Theme Engine in an upcoming update.\n• When using the Native Live Update engine, only the Right Side content is visible.
+ Xiaomi Featured Notifications
+ Island Behavior
+
+ Engine
+ Live Updates vs Xiaomi Island
+ Do Not Disturb
+ Manage island visibility during DND
+ Pause Islands
+ Manually pause all islands until you turn this back on
+ Auto-detect System DND
+ Automatically pause islands when your phone is in Do Not Disturb mode
+
+
+ Global Behavior
+ Auto-hide Island
+ Hides after a set time
+ Visible until manually dismissed
+ Determines how long the island stays on screen before disappearing automatically.
+ Behavior managed by active theme.
+ Use the default value for the notification engine
+ Active Triggers
+ Select which global events trigger the island
+ Regular messages, emails, and alerts
+ Ongoing tasks, rendering, and operations
+ Downloads, uploads, and file transfers
+ Music and video playback controls
+ Turn-by-turn directions and ETA
+ Incoming and ongoing phone calls
+ Active timers and stopwatches
+ Select triggered events
+ Override Global Triggers
+ Set custom allowed notifications for this app
+ Trigger Override
+ Select which events trigger the island for this app.
+ Enable specific triggers
+ Open Source Licenses
+ Live updates
+ Remove original notification
+ Automatically dismiss the source notification when a Live Update is active.
+ You can change these options later in Settings and Theme Configurator.
+ Add standard Android widgets to your island for quick access.
+ Fully customize colors, shapes and icons of your island.
+ Expand the possibilities with different customization options
+ Determines how the island behaves.
+ Preview of the widget
+
+ CN ROM Support\n
+ • Enable the new Sui & Shizuku workaround to enjoy the Hyper Islands in your devices running HyperOS CN (Chinese) ROMs without native support.\n\n
+ Theme Engine Improvements\n
+ • Engine Selection: You can now choose animation engines per-app or globally.\n
+ • Shizuku Workaround: Toggle added directly to the Theme Editor Engine tab.\n
+ • Behavior Controls: Custom timeouts and popup logic are now fully supported.\n\n
+ Intelligent Permanent Island\n
+ • Always Visible: Keep the island on screen even without active notifications.\n
+ • Auto-Hide: The permanent island now automatically hides itself whenever an active widget is shown on the screen.\n\n
+ Smart Icon Tinting\n
+ • New detection system automatically analyzes extracted notification icons and intelligently tints them white if they are dark or monochrome.\n\n
+ Downloads & Progress\n
+ • New Dedicated Download Layout: Separated download and progress layouts with dedicated visual designs.\n
+ • Text Progress Support: Parsed percentage text (e.g. "12%") for app updates even without native progress bars.\n
+ • Smart Filtering: Restricted text-based progress checking to store and installer downloads to prevent false triggers on other status notifications.\n\n
+ Messaging Fixes\n
+ • Fixed missing sender avatars in apps like Telegram (now correctly extracts "sender_person" data).\n
+ • Resolved issues with grouped notifications (chat summaries) not triggering islands.\n
+ • Fixed island disappearance bugs caused by notification update race conditions.\n\n
+ System & Onboarding\n
+ • New Onboarding Screen: A brand new and easier setup process for critical permissions.\n
+ • Boot Fix: Service now starts immediately and reliably after restart.\n
+ • Smarter Alerts: Downloads and Media update silently, while messaging updates reliably trigger popup animations.\n
+ • Navigation: Faster and smoother app transitions.
+
+ The Live Update
+ If you are seeing this, something went wrong!
+ If you are seeing this, something went wrong! The system couldn\'t show the widget
+
+
diff --git a/app/src/main/res/values-uk-rUA/strings.xml b/app/src/main/res/values-uk-rUA/strings.xml
index 34a08b1..9d6567e 100644
--- a/app/src/main/res/values-uk-rUA/strings.xml
+++ b/app/src/main/res/values-uk-rUA/strings.xml
@@ -51,6 +51,11 @@
КонфіґураціяНалаштування системиВиправити дозволи та батарею
+
+ Permanent Island
+ Keep the Dynamic Island permanently visible on screen when there are no active notifications.
+ When enabled, an empty Dynamic Island will be shown permanently on your screen. It will automatically hide when real notifications arrive and reappear when all notifications are cleared. It will not be visible in your notification shade.
+ Island WidthПро застосунокРозробникВідвідати d4viddf.com
@@ -81,6 +86,7 @@
Виберіть активні сповіщенняПовідомлення та загальнеЗавантаження та прогрес
+ DownloadsМузика та медіаКарти та GPSДзвінки
@@ -135,6 +141,7 @@
Вигляд островаПоказати плаваючий острівПоказувати спливаюче сповіщення
+ How long the heads-up popup stays visible.Показати в шторціKeep the notification visible in the notification panel. If disabled, it only appears as an Island.Auto-Close delay
@@ -230,6 +237,14 @@
НевідомоВиявлено китайську прошивку (CN ROM)Схоже, ваш пристрій працює на китайській прошивці, в якій Xiaomi значно змінює внутрішню структуру сповіщень. Функції можуть бути нестабільними або взагалі не працювати.
+ Enable Shizuku Workaround
+ If you are on a CN ROM, you must enable the Shizuku Workaround for notifications to trigger correctly.
+ Shizuku is running
+ Shizuku is not running
+ Permission Granted
+ Permission Denied
+ Grant Shizuku Permission
+ Shizuku app is not installed or running. Please install and start Shizuku first.Версія системиВхідний дзвінокАктивний дзвінок
@@ -557,4 +572,112 @@
Note: This release includes significant architectural changes. Please report any bugs on GitHub!
+ Hotfix Update
+ •<b>Crash Fix:</b> Resolved a critical issue that caused the app to crash when opening the settings menu in Traditional Chinese.\n
+ •<b>Under the Hood:</b> General bug fixes and stability improvements to keep your Island running smoothly.
+
+ Hue
+ Saturation
+ Lightness
+ Dynamic Colors
+ Material You (System)
+ Extracts colors from your current wallpaper.
+ Show Island Compatible Only
+ Kill Active Widgets
+ Update Notice
+ HyperBridge has been updated! Please ensure that your Autostart and Background Battery permissions haven\'t been silently revoked by the system to keep your widgets running smoothly.
+ Got it
+ Recommended
+ All
+ Favorites
+ Native Android Live Updates
+ Transform notifications into standard Android 16 ongoing tasks instead of overriding them with custom Xiaomi Islands.
+ Native Live Updates
+ Preview
+ Notification Design
+ Xiaomi Featured Design
+ Modern HyperOS layout with Xiaomi intended design.
+ Live Update Design
+ Native android notification designs, works on CN ROM.
+ Engine Configuration
+ Preview
+ Notification Design
+ Good to know
+ • Navigation layouts will be fully integrated into the Theme Engine in an upcoming update.\n• When using the Native Live Update engine, only the Right Side content is visible.
+ Xiaomi Featured Notifications
+ Island Behavior
+
+ Engine
+ Live Updates vs Xiaomi Island
+ Do Not Disturb
+ Manage island visibility during DND
+ Pause Islands
+ Manually pause all islands until you turn this back on
+ Auto-detect System DND
+ Automatically pause islands when your phone is in Do Not Disturb mode
+
+
+ Global Behavior
+ Auto-hide Island
+ Hides after a set time
+ Visible until manually dismissed
+ Determines how long the island stays on screen before disappearing automatically.
+ Behavior managed by active theme.
+ Use the default value for the notification engine
+ Active Triggers
+ Select which global events trigger the island
+ Regular messages, emails, and alerts
+ Ongoing tasks, rendering, and operations
+ Downloads, uploads, and file transfers
+ Music and video playback controls
+ Turn-by-turn directions and ETA
+ Incoming and ongoing phone calls
+ Active timers and stopwatches
+ Select triggered events
+ Override Global Triggers
+ Set custom allowed notifications for this app
+ Trigger Override
+ Select which events trigger the island for this app.
+ Enable specific triggers
+ Open Source Licenses
+ Live updates
+ Remove original notification
+ Automatically dismiss the source notification when a Live Update is active.
+ You can change these options later in Settings and Theme Configurator.
+ Add standard Android widgets to your island for quick access.
+ Fully customize colors, shapes and icons of your island.
+ Expand the possibilities with different customization options
+ Determines how the island behaves.
+ Preview of the widget
+
+ CN ROM Support\n
+ • Enable the new Sui & Shizuku workaround to enjoy the Hyper Islands in your devices running HyperOS CN (Chinese) ROMs without native support.\n\n
+ Theme Engine Improvements\n
+ • Engine Selection: You can now choose animation engines per-app or globally.\n
+ • Shizuku Workaround: Toggle added directly to the Theme Editor Engine tab.\n
+ • Behavior Controls: Custom timeouts and popup logic are now fully supported.\n\n
+ Intelligent Permanent Island\n
+ • Always Visible: Keep the island on screen even without active notifications.\n
+ • Auto-Hide: The permanent island now automatically hides itself whenever an active widget is shown on the screen.\n\n
+ Smart Icon Tinting\n
+ • New detection system automatically analyzes extracted notification icons and intelligently tints them white if they are dark or monochrome.\n\n
+ Downloads & Progress\n
+ • New Dedicated Download Layout: Separated download and progress layouts with dedicated visual designs.\n
+ • Text Progress Support: Parsed percentage text (e.g. "12%") for app updates even without native progress bars.\n
+ • Smart Filtering: Restricted text-based progress checking to store and installer downloads to prevent false triggers on other status notifications.\n\n
+ Messaging Fixes\n
+ • Fixed missing sender avatars in apps like Telegram (now correctly extracts "sender_person" data).\n
+ • Resolved issues with grouped notifications (chat summaries) not triggering islands.\n
+ • Fixed island disappearance bugs caused by notification update race conditions.\n\n
+ System & Onboarding\n
+ • New Onboarding Screen: A brand new and easier setup process for critical permissions.\n
+ • Boot Fix: Service now starts immediately and reliably after restart.\n
+ • Smarter Alerts: Downloads and Media update silently, while messaging updates reliably trigger popup animations.\n
+ • Navigation: Faster and smoother app transitions.
+
+ The Live Update
+ If you are seeing this, something went wrong!
+ If you are seeing this, something went wrong! The system couldn\'t show the widget
+
+
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index b4edef5..c2a42b4 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -4,7 +4,7 @@
返回下一步完成
- 已完成
+ 完成應用程式圖示設定資訊
@@ -14,8 +14,8 @@
歡迎使用\nHyper Bridge將 HyperIslands 整合至你的 HyperOS 使用體驗中。開始使用
- 運作方式
- 選擇你常用的應用程式(Spotify、地圖、計時器)。HyperBridge 會將它們的通知轉換為簡潔俐落的 HyperIsland。
+ 如何運作
+ 選擇你常用的應用程式(Spotify、地圖、計時器)。HyperBridge 會將它們的通知轉換為簡潔俐落的超級島。早期開發階段注意:此應用程式目前為 Beta 測試版,您可能會遇到程式錯誤 或顯示資訊不正確的情況。相容的裝置
@@ -33,7 +33,7 @@
開啟設定保持背景執行HyperOS 會自動關閉背景程式。請開啟「自啟動」並將省電策略設為「無限制」,以維持超級島運作。
- 1. 開啟自啟動
+ 1. 開啟後台啟動管理2. 將省電策略設為「無限制」已啟用資料庫
@@ -51,6 +51,11 @@
設定系統設定修正權限 & 電池設定
+
+ Permanent Island
+ Keep the Dynamic Island permanently visible on screen when there are no active notifications.
+ When enabled, an empty Dynamic Island will be shown permanently on your screen. It will automatically hide when real notifications arrive and reappear when all notifications are cleared. It will not be visible in your notification shade.
+ Island Width關於開發者造訪 d4viddf.com
@@ -80,7 +85,8 @@
其他選擇要顯示的通知訊息 & 一般通知
- 下載 & 進度
+ Progress & Tasks
+ Downloads音樂 & 媒體地圖 & GPS通話
@@ -104,10 +110,6 @@
完成成功完成
- 完成
- 結束
- 成功
- 已安裝下載完成正在下載
@@ -139,19 +141,20 @@
超級島外觀顯示於超級島顯示提前通知
+ 彈出式提示框的顯示時長。在通知欄中顯示在通知中心中保留逾時
- %ds
+ %d秒使用預設設定自訂覆蓋設定
- arrival
- arrive
+ 到達
+ 到達eta
- reaching
- destination
+ 到達
+ 目標選項版本紀錄
@@ -226,7 +229,7 @@
作業系統:%s需要 HyperOS 3.0 以上版本 (Android 15+)僅支援 小米/紅米/Poco 裝置
- 背景自動啟動
+ 背景自啟動重啟裝置後需要重新啟動服務。授予權限相容
@@ -234,6 +237,14 @@
未知偵測到中國版 ROM / 韌體您的裝置似乎正在執行中國版韌體。小米在中國版 ROM 上大幅修改了通知系統底層,功能可能不穩定或無法運作。
+ Enable Shizuku Workaround
+ If you are on a CN ROM, you must enable the Shizuku Workaround for notifications to trigger correctly.
+ Shizuku is running
+ Shizuku is not running
+ Permission Granted
+ Permission Denied
+ Grant Shizuku Permission
+ Shizuku app is not installed or running. Please install and start Shizuku first.系統版本來電通話中
@@ -249,7 +260,7 @@
擴音
- audio
+ 聲音預設功能更新隱藏通知 👻\n
@@ -546,4 +557,112 @@
備註:本次版本包含重大架構調整,如發現任何問題,請至 GitHub 回報!
+ 修補更新
+ •</b>崩潰修復:</b>修正了在繁體中文環境下,開啟設定選單會導致應用程式閃退的嚴重問題。\n
+ •<b>底層最佳化:</b> 一般錯誤的修正與穩定性提升,確保您的超級島順暢運作。
+
+ 色調
+ 飽和度
+ 亮度
+ 超級島顏色
+ Material You (系統初始設定)
+ 從目前壁紙中提取顏色。
+ 僅顯示相容超級島的程式
+ 停止運作中的小工具
+ 更新通知
+ HyperBridge 已更新!請確保您的自啟動和後台運作權限未被系統移除,以確保您的小工具正常運作。
+ 明白了
+ 建議
+ 全部
+ 我的最愛
+ 原生安卓即時更新
+ 將通知轉換為原生 Android 16 即時更新,而不是用自訂的小米超級島來覆蓋它們。
+ 原生即時更新
+ 預覽
+ 通知設計
+ 小米特色設計
+ 現代HyperOS介面,採用小米的設計理念。
+ 即時更新設計
+ 原生安卓通知設計,適用於中國版 ROM。
+ 儲存配置
+ 預覽
+ 通知設計
+ 太好了
+ • 導航佈局會在即將推出的更新中完全整合到主題引擎中。 \n• 使用原生即時更新引擎時,僅右側內容可見。
+ 小米特色通知
+ 超級島行為
+
+ 引擎
+ 即時更新 vs. 小米超級島
+ Do Not Disturb
+ Manage island visibility during DND
+ Pause Islands
+ Manually pause all islands until you turn this back on
+ Auto-detect System DND
+ Automatically pause islands when your phone is in Do Not Disturb mode
+
+
+ 預設行為
+ 自動隱藏超級島
+ 在特定的時間後隱藏
+ 手動關閉前一直可見
+ 決定超級島在螢幕上停留的時間,之後會自動消失。
+ 由活動主題管理的行為。
+ 使用預設值來當作通知引擎
+ 執行中的程式通知
+ 選擇哪一個全域條件來觸發超級島
+ 預設的訊息、郵件及鬧鐘
+ Ongoing tasks, rendering, and operations
+ Downloads, uploads, and file transfers
+ 音樂及影片的播放控制
+ 逐嚮導航和預計到達時間
+ 來電與通話
+ 運作中的計時器和碼錶
+ 選擇觸發條件
+ 覆蓋預設觸發器
+ 為此應用程式設自訂允許的通知
+ 覆蓋觸發器
+ 為應用程式選擇一個條件觸發器上超級島。
+ 啟用特定的觸發器
+ 開放原始碼授權
+ 即時更新
+ 移除預設通知
+ 當即時更新處於啟用狀態時,自動關閉通知來源。
+ 您稍後可以在「設定」和「主題配置器」中變更這些選項。
+ 在您的超級島上添加Android 小工具,以便快速存取。
+ 完全自訂超級島的顏色、形狀和圖示。
+ 利用不同的自訂選項拓展可能性
+ 決定超級島的呈現方式。
+ 預覽小工具
+
+ CN ROM Support\n
+ • Enable the new Sui & Shizuku workaround to enjoy the Hyper Islands in your devices running HyperOS CN (Chinese) ROMs without native support.\n\n
+ Theme Engine Improvements\n
+ • Engine Selection: You can now choose animation engines per-app or globally.\n
+ • Shizuku Workaround: Toggle added directly to the Theme Editor Engine tab.\n
+ • Behavior Controls: Custom timeouts and popup logic are now fully supported.\n\n
+ Intelligent Permanent Island\n
+ • Always Visible: Keep the island on screen even without active notifications.\n
+ • Auto-Hide: The permanent island now automatically hides itself whenever an active widget is shown on the screen.\n\n
+ Smart Icon Tinting\n
+ • New detection system automatically analyzes extracted notification icons and intelligently tints them white if they are dark or monochrome.\n\n
+ Downloads & Progress\n
+ • New Dedicated Download Layout: Separated download and progress layouts with dedicated visual designs.\n
+ • Text Progress Support: Parsed percentage text (e.g. "12%") for app updates even without native progress bars.\n
+ • Smart Filtering: Restricted text-based progress checking to store and installer downloads to prevent false triggers on other status notifications.\n\n
+ Messaging Fixes\n
+ • Fixed missing sender avatars in apps like Telegram (now correctly extracts "sender_person" data).\n
+ • Resolved issues with grouped notifications (chat summaries) not triggering islands.\n
+ • Fixed island disappearance bugs caused by notification update race conditions.\n\n
+ System & Onboarding\n
+ • New Onboarding Screen: A brand new and easier setup process for critical permissions.\n
+ • Boot Fix: Service now starts immediately and reliably after restart.\n
+ • Smarter Alerts: Downloads and Media update silently, while messaging updates reliably trigger popup animations.\n
+ • Navigation: Faster and smoother app transitions.
+
+ 即時更新
+ 如果您看到此訊息,說明出現錯誤!
+ 如果您看到此訊息,表示出錯了!系統無法顯示該小工具
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 120be53..a3e0ba5 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -33,14 +33,14 @@
Permission GrantedAllow Notifications
- Read Data
+ Read NotificationsTo bridge your apps, we need to read their notifications (Music, Maps, Timers).Open SettingsKeep AliveHyperOS kills background apps. Enable Autostart and No Restrictions to keep the island active.
- 1. Enable Autostart
- 2. Set Battery \'No Restrictions\'
+ Allow Autostart
+ Disable RestrictionsActiveLibrary
@@ -62,6 +62,12 @@
System SetupFix permissions & battery
+
+ Permanent Island
+ Keep the Dynamic Island permanently visible on screen when there are no active notifications.
+ When enabled, an empty Dynamic Island will be shown permanently on your screen. It will automatically hide when real notifications arrive and reappear when all notifications are cleared. It will not be visible in your notification shade.
+ Island Width
+
AboutDeveloperVisit d4viddf.com
@@ -94,7 +100,8 @@
Select active notificationsMessages & General
- Downloads & Progress
+ Progress & Tasks
+ DownloadsMusic & MediaMaps & GPSCalls
@@ -157,6 +164,7 @@
Island AppearanceHeads-up PopupShow the full notification banner briefly before minimizing to the Island.
+ How long the heads-up popup stays visible.Notification PanelKeep the notification visible in the notification panel. If disabled, it only appears as an Island.
@@ -198,8 +206,8 @@
• <b>Localization:</b> Added Spanish support.
- New: Control the Island
- HyperBridge can now manage how multiple notifications behave when the 9-island limit is reached.\n\n<b>Default: Most Recent</b>\nNew notifications replace the oldest ones automatically.
+ Control the Island
+ Choose how the island should behave when multiple notifications are active at the same time.Keep DefaultConfigure Now
@@ -275,6 +283,16 @@
China ROM DetectedYour device appears to be running a Chinese firmware. Xiaomi modifies notification internals significantly on CN ROMs. Features might be unstable or not work at all.
+ Enable Shizuku Workaround
+ Enable Workaround
+ If you are on a CN ROM, you must enable the Shizuku Workaround for notifications to trigger correctly.
+ Shizuku is running
+ Shizuku is not running
+ Permission Granted
+ Permission Denied
+ Grant Shizuku Permission
+ Shizuku app is not installed or running. Please install and start Shizuku first.
+
System VersionIncoming Call
@@ -675,4 +693,94 @@
Native Android Live UpdatesTransform notifications into standard Android 16 ongoing tasks instead of overriding them with custom Xiaomi Islands.Native Live Updates
+
+ Preview
+ Notification Design
+ Xiaomi Featured Design
+ Modern HyperOS layout with Xiaomi intended design.
+ Live Update Design
+ Native android notification designs, works on CN ROM.
+
+ Engine Configuration
+ Preview
+ Notification Design
+ Good to know
+ • Navigation layouts will be fully integrated into the Theme Engine in an upcoming update.\n• When using the Native Live Update engine, only the Right Side content is visible.
+ Xiaomi Featured Notifications
+ Island Behavior
+
+ Engine
+ Live Updates vs Xiaomi Island
+
+ Do Not Disturb
+ Manage island visibility during DND
+ Pause Islands
+ Manually pause all islands until you turn this back on
+ Auto-detect System DND
+ Automatically pause islands when your phone is in Do Not Disturb mode
+
+
+ Global Behavior
+ Auto-hide Island
+ Hides after a set time
+ Visible until manually dismissed
+ Determines how long the island stays on screen before disappearing automatically.
+ Behavior managed by active theme.
+ Use the default value for the notification engine
+ Active Triggers
+ Select which global events trigger the island
+ Regular messages, emails, and alerts
+ Ongoing tasks, rendering, and operations
+ Downloads, uploads, and file transfers
+ Music and video playback controls
+ Turn-by-turn directions and ETA
+ Incoming and ongoing phone calls
+ Active timers and stopwatches
+ Select triggered events
+ Override Global Triggers
+ Set custom allowed notifications for this app
+ Trigger Override
+ Select which events trigger the island for this app.
+ Enable specific triggers
+ Open Source Licenses
+ Live updates
+ Remove original notification
+ Automatically dismiss the source notification when a Live Update is active.
+ You can change these options later in Settings and Theme Configurator.
+ Add standard Android widgets to your island for quick access.
+ Fully customize colors, shapes and icons of your island.
+ Expand the possibilities with different customization options
+ Determines how the island behaves.
+ Preview of the widget
+
+
+ CN ROM Support\n
+ • Enable the new Sui & Shizuku workaround to enjoy the Hyper Islands in your devices running HyperOS CN (Chinese) ROMs without native support.\n\n
+ Theme Engine Improvements\n
+ • Engine Selection: You can now choose animation engines per-app or globally.\n
+ • Shizuku Workaround: Toggle added directly to the Theme Editor Engine tab.\n
+ • Behavior Controls: Custom timeouts and popup logic are now fully supported.\n\n
+ Intelligent Permanent Island\n
+ • Always Visible: Keep the island on screen even without active notifications.\n
+ • Auto-Hide: The permanent island now automatically hides itself whenever an active widget is shown on the screen.\n\n
+ Smart Icon Tinting\n
+ • New detection system automatically analyzes extracted notification icons and intelligently tints them white if they are dark or monochrome.\n\n
+ Downloads & Progress\n
+ • New Dedicated Download Layout: Separated download and progress layouts with dedicated visual designs.\n
+ • Text Progress Support: Parsed percentage text (e.g. "12%") for app updates even without native progress bars.\n
+ • Smart Filtering: Restricted text-based progress checking to store and installer downloads to prevent false triggers on other status notifications.\n\n
+ Messaging Fixes\n
+ • Fixed missing sender avatars in apps like Telegram (now correctly extracts "sender_person" data).\n
+ • Resolved issues with grouped notifications (chat summaries) not triggering islands.\n
+ • Fixed island disappearance bugs caused by notification update race conditions.\n\n
+ System & Onboarding\n
+ • New Onboarding Screen: A brand new and easier setup process for critical permissions.\n
+ • Boot Fix: Service now starts immediately and reliably after restart.\n
+ • Smarter Alerts: Downloads and Media update silently, while messaging updates reliably trigger popup animations.\n
+ • Navigation: Faster and smoother app transitions.
+
+ The Live Update
+ If you are seeing this, something went wrong!
+ If you are seeing this, something went wrong! The system couldn\'t show the widget
+
diff --git a/build.gradle.kts b/build.gradle.kts
index f185ebf..77f9cdf 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,7 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
- alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
alias(libs.plugins.ksp)
}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 20e2a01..04993af 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -20,4 +20,10 @@ kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
-android.nonTransitiveRClass=true
\ No newline at end of file
+android.nonTransitiveRClass=true
+android.uniquePackageNames=false
+android.dependency.useConstraints=true
+android.r8.strictFullModeForKeepRules=false
+android.builtInKotlin=true
+android.newDsl=true
+android.disallowKotlinSourceSets=false
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index bb2b667..f4181ee 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,31 +1,34 @@
[versions]
-agp = "8.13.2"
-datastorePreferences = "1.2.0"
-gson = "2.13.2"
+agp = "9.2.1"
+api = "13.1.5"
+datastorePreferences = "1.2.1"
+gson = "2.14.0"
hyperislandKit = "0.4.4"
-kotlin = "2.3.0"
-kotlinxSerializationJson = "1.9.0"
-ksp = "2.1.21-2.0.1"
-coreKtx = "1.17.0"
+kotlin = "2.3.21"
+kotlinxSerializationJson = "1.11.0"
+ksp = "2.3.2"
+coreKtx = "1.18.0"
junit = "4.13.2"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
lifecycleRuntimeKtx = "2.10.0"
-activityCompose = "1.12.2"
-composeBom = "2025.11.01"
+activityCompose = "1.13.0"
+composeBom = "2026.05.01"
materialIconsExtended = "1.7.8"
-ui = "1.10.0"
+ui = "1.11.2"
roomCompiler = "2.8.4"
roomRuntime = "2.8.4"
roomKtx = "2.8.4"
appcompat = "1.7.1"
paletteKtx = "1.0.0"
-material3 = "1.4.0"
+nav3 = "1.1.2"
+lifecycleViewmodelNav3 = "2.10.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" }
+api = { module = "dev.rikka.shizuku:api", version.ref = "api" }
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
hyperisland-kit = { module = "io.github.d4viddf:hyperisland_kit", version.ref = "hyperislandKit" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
@@ -40,7 +43,7 @@ androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-toolin
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
-androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version = "1.5.0-alpha11" }
+androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version = "1.5.0-alpha15" }
androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "materialIconsExtended" }
androidx-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "ui" }
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "roomCompiler" }
@@ -49,10 +52,12 @@ androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref =
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
androidx-palette-ktx = { group = "androidx.palette", name = "palette-ktx", version.ref = "paletteKtx" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
-androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" }
+androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "nav3" }
+androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "nav3" }
+androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycleViewmodelNav3" }
+provider = { module = "dev.rikka.shizuku:provider", version.ref = "api" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 1b94922..414a2d7 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,7 +1,7 @@
#Sun Nov 23 16:10:02 CET 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 3b11355..9ab95e7 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -11,6 +11,9 @@ pluginManagement {
gradlePluginPortal()
}
}
+plugins {
+ id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
+}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
From 48198e458925c585f9c66b1b8b8e493b55defe7d Mon Sep 17 00:00:00 2001
From: david
Date: Wed, 3 Jun 2026 16:31:12 +0200
Subject: [PATCH 02/26] chore: bump version code and update localizations for
v0.5.0
- **Build**: Bumped `versionCode` to 24 in `app/build.gradle.kts`.
- **Localization**:
- **Traditional Chinese (ZH-TW) & French (FR)**: Completed full localization for Permanent Island, Do Not Disturb mode settings, and the Shizuku workaround.
- **Spanish (ES)**: Refined translations for "Downloads" and "Progress" notification types.
- **Changelog**: Updated and formatted the v0.5.0 "The Live Update" release notes across all supported locales (ID, TR, CS, IT, UK, DE, ZH, FR, PT, RU, SK, ES, PL, KO, HU), ensuring consistent HTML styling and escaped characters.
- **General**: Added the missing `shizuku_enable_workaround` string to multiple language resources.
- **Tooling**: Updated Crowdin plugin configuration to suppress confirmation dialogs during synchronization.
---
.idea/CrowdinSettingsPlugin.xml | 1 +
app/build.gradle.kts | 2 +-
app/src/main/res/values-cs-rCZ/strings.xml | 10 +--
app/src/main/res/values-de-rDE/strings.xml | 10 +--
app/src/main/res/values-es-rES/strings.xml | 20 ++---
app/src/main/res/values-fr-rFR/strings.xml | 98 +++++++++++-----------
app/src/main/res/values-hu-rHU/strings.xml | 10 +--
app/src/main/res/values-in-rID/strings.xml | 10 +--
app/src/main/res/values-it-rIT/strings.xml | 10 +--
app/src/main/res/values-ko-rKR/strings.xml | 10 +--
app/src/main/res/values-pl-rPL/strings.xml | 10 +--
app/src/main/res/values-pt-rBR/strings.xml | 10 +--
app/src/main/res/values-ru-rRU/strings.xml | 10 +--
app/src/main/res/values-sk-rSK/strings.xml | 10 +--
app/src/main/res/values-tr-rTR/strings.xml | 10 +--
app/src/main/res/values-uk-rUA/strings.xml | 12 ++-
app/src/main/res/values-zh-rTW/strings.xml | 97 +++++++++++----------
17 files changed, 154 insertions(+), 186 deletions(-)
diff --git a/.idea/CrowdinSettingsPlugin.xml b/.idea/CrowdinSettingsPlugin.xml
index 018093b..abd84f1 100644
--- a/.idea/CrowdinSettingsPlugin.xml
+++ b/.idea/CrowdinSettingsPlugin.xml
@@ -4,6 +4,7 @@