Compare commits

..
145 Commits
Author SHA1 Message Date
darken ac477113cf Release: 4.0.1-rc0 2026-03-12 13:42:29 +01:00
darken 5e21941765 General: Update app translations from Crowdin 2026-03-12 12:12:40 +00:00
darken e5032ee35a General: Update fastlane translations from Crowdin 2026-03-12 12:12:40 +00:00
darken 1ceac349ab chore: Add .claude/.gitignore for worktrees 2026-03-12 11:59:07 +00:00
darken 2db01b410b fix(billing): Prevent crash when billing service disconnects 2026-03-12 11:59:07 +00:00
darken 193a1030fb feat: Start BLE scanning with only core permissions granted
Allow the app to scan for AirPods when only BLE scan permissions are granted, without waiting for all optional permissions (notifications, overlay, etc.).

Add isScanBlocking flag to Permission enum. Gate monitor service and pod scanning on scan permissions only. Show scan-blocking permission cards with error color and sorted first. Wrap BLUETOOTH_CONNECT-dependent calls in try-catch for graceful degradation. Fix POST_NOTIFICATIONS minApiLevel from S (31) to TIRAMISU (33).
2026-03-10 16:37:43 +00:00
darken 12e35631d6 fix: Scope ViewModels to navigation entries via NavDisplay decorator 2026-03-10 15:11:10 +00:00
darken 67474a30da fix(debug): Rename recorder screen button from Close to Keep
The keep button's onClick handler was already named onKeep but the label said Close. Renamed to Keep for clarity alongside the Delete/Share buttons. Added dedicated string resource with translations for all 75 locales.
2026-03-10 07:50:36 +00:00
darken 44cd0eec2a test: Add missing AutoConnectCondition round-trip and Model encode tests 2026-03-09 20:29:19 +00:00
darken a79f6a0535 refactor: Replace Moshi with kotlinx.serialization
Remove Moshi dependency entirely, completing the migration to kotlinx.serialization. All JSON serialization now uses kotlinx with explicit @SerialName annotations for wire format stability.

- Migrate PodDeviceCache from Moshi to kotlinx Json injection

- Add MapIntByteArrayBase64Serializer for BleScanResult cache compat

- Strip @JsonClass/@Json annotations from all dual-annotated classes

- Delete Moshi adapters, ProGuard rules, and build config

- Convert compat tests to pure kotlinx round-trip tests
2026-03-09 20:29:19 +00:00
darken 8675d73a98 fix(debug): Persist recording session across app restarts
The debug recording trigger file now stores the session directory path and start timestamp. On app restart, the recorder resumes into the same session directory and log file instead of creating a new one, preserving the original start time so the short-recording guard doesn't reset.
2026-03-09 17:34:29 +00:00
darken 3423786f06 chore: Remove redundant package name from manifest 2026-03-09 17:41:08 +01:00
darken 00083707c6 fix(debug): Harden debug session recording and sharing 2026-03-09 16:39:56 +00:00
darken 0cdd2a4f7a Release: 4.0.0-rc0 2026-03-09 16:55:58 +01:00
darken 8e3b235c81 ui: Improve PodGauge layout alignment in DualPodsCard 2026-03-09 16:46:25 +01:00
darken 879ec1b69c General: Update app translations from Crowdin 2026-03-09 14:33:56 +00:00
darken c833fd2c92 General: Update fastlane translations from Crowdin 2026-03-09 14:33:56 +00:00
darken d7d9d53ff3 refactor: Replace MaterialAlertDialogBuilder with Compose AlertDialog
Replace all 11 instances of MaterialAlertDialogBuilder across 5 files with Compose AlertDialog. Use sealed dialog state per screen to prevent dialog stacking. Add shared ConfirmationDialog composable. Delete dead ErrorDialog.kt.
2026-03-09 12:46:23 +00:00
darken cba239f411 General: Update app translations from Crowdin 2026-03-09 12:40:01 +00:00
darken 853e877a59 General: Update fastlane translations from Crowdin 2026-03-09 12:40:01 +00:00
darken de89c51457 fix(debug): Use human-readable UTC timestamp in debug session directory names
Parse creation time from the embedded filename timestamp instead of relying on filesystem attributes, which are non-deterministic and caused flaky test failures in CI.
2026-03-09 11:57:42 +00:00
darken 7f8daddfe0 feat(debug): Show confirmation dialog after sharing debug logs
After sharing a debug log or sending a contact form email, show a dialog on return asking if the send was successful. Confirming deletes the log session and closes/navigates back.
2026-03-09 11:17:50 +00:00
darken a76e43ac04 chore: Use List in Zipper and add global ExperimentalMaterial3Api opt-in
Replace Array<String> with List<String> in Zipper.zip().

Add global -opt-in flag for ExperimentalMaterial3Api in build.gradle.kts

and remove per-file @OptIn annotations from 12 Compose screens.
2026-03-09 04:53:01 +00:00
darken fd0dc49396 test(debug): Add session ID round-trip and edge case tests 2026-03-09 04:53:01 +00:00
darken a5f5feacae fix(debug): Fix deadlock risk, missing IO dispatcher, and thread safety in session manager
Replace sessions.first() inside fsMutex.withLock with synchronous volatile

read to avoid potential deadlock. Wrap file deletions in IO dispatcher.

Log partial delete failures. Make pendingAutoZips thread-safe via

Collections.synchronizedSet. Add test for ext/cache same-basename IDs.
2026-03-09 04:53:01 +00:00
darken 70a3114876 chore(crowdin): Update Crowdin CLI binary 2026-03-09 04:53:01 +00:00
darken 557ee9b3ed fix(test): Fix flaky sort test that relied on filesystem creation time 2026-03-09 04:53:01 +00:00
darken 58bd0f91f5 refactor(debug): Use Instant for createdAt, extract scanSessions helpers, add pendingAutoZips guard 2026-03-09 04:53:01 +00:00
darken b2f8586dc9 fix(recorder): Use red delete button and rename Done to Close 2026-03-09 04:53:01 +00:00
darken 62797e8ea2 test(support): Add ContactFormViewModel.State unit tests 2026-03-09 04:53:01 +00:00
darken c614e7bd4f test(debug): Add unit tests for Zipper, DebugLogZipper, RecorderModule.State, and scanSessions edge cases 2026-03-09 04:53:01 +00:00
darken 62e39ce3d2 refactor(support): Add zipFile/compressedSize to Ready, harden session manager
Add zipFile and compressedSize fields to DebugSession.Ready, populated during scan. Simplify RecorderActivityVM by reading compressed size directly from session model. Apply code review fixes: fsMutex in zipSessionAsync, CancellationException handling, ZipOutputStream use{}, atomic file moves, dedup deriveSessionId, LifecycleResumeEffect for session refresh.
2026-03-09 04:53:01 +00:00
darken 7df74e3b44 refactor(support): Unify debug log session model and add session manager bottom sheet
Introduce DebugSession sealed interface as single source of truth for session lifecycle (Recording, Compressing, Ready, Failed). Auto-compress after recording stops. Replace scattered clear/delete actions with a session manager bottom sheet on the Support screen.
2026-03-09 04:53:01 +00:00
darken 06e5954ef5 fix(upgrade): Reduce FOSS sponsor unlock timer from 10s to 5s 2026-03-07 14:26:35 +00:00
darken 450a187b65 test(reaction): Add ViewModel and reaction logic unit tests
Extract pure decision functions from AutoConnect and PopUpReaction for testability, following the existing PlayPause pattern. Add FakeDataStoreValue test helper.

Fix reversed Duration.between args in PopUpReaction connection monitor that caused the false-positive age filter to never trigger.
2026-03-07 14:19:17 +00:00
darken da6a6f6ff2 fix(widget): Broaden work-runtime keep rule and add widget error handling
R8 full mode strips no-arg constructors from work-runtime classes accessed via

Class.newInstance() reflection (WorkDatabase_Impl, OverwritingInputMerger, etc).

Replace narrow WorkDatabase_Impl keep rule with broad androidx.work.** rule.

Also add @Keep to Hilt WidgetEntryPoint and wrap provideGlance setup in

try-catch so the widget shows an error message instead of spinning forever.
2026-03-06 18:47:46 +00:00
darken d37c8aa3a5 fix(proguard): Add keep rule for WorkDatabase_Impl stripped by R8 full mode 2026-03-06 18:47:46 +00:00
darken 5cd97805b4 test(upgrade): Add unit tests for Google Play billing and upgrade logic 2026-03-05 13:41:34 +00:00
darken e35bce4d19 feat(upgrade): Add yearly subscription with trial to Google Play billing
Rewrite SKU type system from data class to interface hierarchy supporting both IAP and subscriptions. Add subscription query, offer matching, and billing flow launch for yearly plans with optional free trial.

FOSS: Add sponsor-gate with 10-second timer and snackbar nudge.
2026-03-04 22:38:23 +00:00
darken 1a196689db fix(test): Move FossUpgrade tests to testFoss source set for gplay compat
FossUpgrade is foss-flavor-only, so tests referencing it must live in testFoss/ to avoid compilation failures in testGplayDebugUnitTest.
2026-03-04 15:23:34 +00:00
darken 8ada7b071d test(settings): Add missing DataStore serialization and migration compat tests
Cover round-trip tests for ThemeStyle, ThemeColor, MonitorMode, ScannerMode, AutoConnectCondition, FossUpgrade, and InstantEpochMillisSerializer. Add Moshi decode tests for ThemeStyle, ThemeColor, AutoConnectCondition, FossUpgrade, and FossUpgrade.Reason annotation parity.
2026-03-04 15:23:34 +00:00
darken a92e364ca6 refactor(settings): Migrate preferences from SharedPreferences to DataStore
Replace FlowPreference<T> wrapping SharedPreferences with DataStoreValue<T> wrapping AndroidX DataStore. Includes SharedPreferencesMigration for preserving existing user data, kotlinx-serialization for complex types (replacing Moshi for preferences), and comprehensive unit tests for the new infrastructure.
2026-03-04 15:23:34 +00:00
darken 6326f4a5b9 fix(monitor): Gate BLE scanning on permission state and stop retrying on SecurityException
PodMonitor's retryWhen always returned true, causing infinite 3-second retry loops when BLUETOOTH_SCAN permission was missing (e.g. on emulators). Now the scan flow checks missingPermissions before starting, and SecurityException aborts retries since it requires user action.
2026-03-03 20:52:32 +00:00
darken abb189f43c feat(upgrade): Replace upgrade dialog with full-screen upgrade screen
Add dedicated Compose upgrade screens for both FOSS and gplay flavors, presenting pro benefits before the purchase/sponsor action. Migrate all upgrade dialog callers to navigate to the new screen instead.
2026-03-03 20:35:32 +00:00
darken e80bf1db6c feat(support): Add wiki link to support screen 2026-03-03 17:22:04 +00:00
darken cd6488a713 refactor(ui): Replace waitForState and shareLatest with lifecycle-aware state collection
Add collectAsStateWithLifecycle for all Compose state collection (except Glance widgets). Add asLiveState() extension to ViewModel2 as a simpler replacement for shareLatest(scope = vmScope). Remove custom waitForState helper and shareLatest utility.
2026-03-03 17:21:55 +00:00
darken e13331dfcf feat(monitor): Show disable hint in foreground notification when extra notification is enabled 2026-03-03 15:26:10 +00:00
darken dcf2ade9ec feat(dashboard): Reorder top bar icons and use star for donate 2026-03-03 15:16:50 +00:00
darken 40f0aba462 docs: Add pull request guidelines with user-facing changelog conventions 2026-03-03 15:24:05 +01:00
darken c760d5758d feat(support): Polish support screen, recorder UI, and settings
Reorder support screen items with category headers, add log session count to clear action, fix contact form hint strings, filter active recordings from log picker, convert RecorderActivity to Compose with proper system bar insets and Material 3 color tokens, refresh log metadata on resume, add wiki link to settings, and move support entry under Other category
2026-03-03 13:20:31 +01:00
darken b609c1aced feat(support): Add contact support form and enhanced debug recorder
Add structured contact form with category selection (Question/Feature/Bug), description with word count validation, expected behavior field for bugs, and debug log picker with inline recording.

Enhance RecorderActivity with full-screen hero layout, file list, Share/Keep/Discard actions. Migrate debug logs from flat files to session directories in external files dir. Add DebugLogZipper, EmailTool attachment support, log folder size display, and clear stored logs.
2026-03-03 13:20:31 +01:00
darken b3b55f1ca3 feat(settings): Make upgrade prompt a clickable button inline with appearance header 2026-03-02 16:36:52 +01:00
darken 073de3cf35 feat(settings): Add pro gating for theme customization
Gate theme mode, style, and color settings behind pro/upgrade status.

Add Material Design 3 surface container color tokens to all color palettes.

Move upgrade prompt string to main resources with generic wording.

Disable theme preference items for non-pro users instead of late rejection.
2026-03-02 16:36:52 +01:00
darken 8c278f2a3c fix(settings): Point changelog to project website instead of GitHub releases 2026-03-01 15:57:11 +01:00
darken c103c6adc9 feat(widget): Migrate from RemoteViews to Jetpack Glance
Replace the legacy AppWidgetProvider/RemoteViews implementation with Glance AppWidget for reactive updates, proper centering, and a unified render state model.

- Add BatteryGlanceWidget with collectAsState for live device/profile/upgrade data

- Add WidgetRenderState sealed class and WidgetRenderStateMapper

- Add GlanceWidgetContent (Glance composables) and ComposeWidgetPreview (config preview)

- Fix centering by passing spacing via modifier param (no trailing padding on last item)

- Remove old XML widget layouts and RemoteViews rendering code
2026-03-01 15:56:43 +01:00
darken 4db2edd29a refactor(monitor): Clean up collapsed notification layouts
Normalize root containers to FrameLayout, remove weight-based spacers,

standardize icon sizes to 16dp, and use consistent margins.
2026-03-01 00:15:42 +01:00
darken eefe740b6f feat(monitor): Add expanded notification with battery progress bars
Add distinct expanded (big) notification layouts with visual battery

progress bars for dual pods, single pods, and unknown devices.

Collapsed views remain unchanged. Status icon containers use fixed

width to ensure uniform progress bar lengths across rows.
2026-03-01 00:15:42 +01:00
darken a47addc8bc Adjust enabled plugins 2026-02-28 21:21:42 +01:00
darken 9cc5a82adf fix(widget): Sync window background with Compose theme in config activity
The widget configuration activity used enableEdgeToEdge() but never synced the Android window background with the Compose Material theme. When the system dark/light mode disagreed with the app's theme setting, text became invisible (dark on dark). Also extend the bottom bar Surface under the navigation bar to eliminate the white gap.
2026-02-26 12:50:28 +01:00
darken bec24bfd6f chore(screenshots): Regenerate profile creation screenshots 2026-02-26 01:12:22 +01:00
darken e6eb0d818f docs: Add screenshot pipeline documentation 2026-02-26 01:12:22 +01:00
darken 25e3e7d792 feat(profiles): Modernize profile creation/edit screen UI 2026-02-26 00:05:24 +01:00
darken ebcb997bdd feat(screenshots): Generate full localized Play Store screenshots (544 images)
- 67 locales × 8 screens at Pixel 8 resolution (1080×2400, 428dpi)

- Reduce batch size to 2 to avoid layoutlib OOM at 1080p (~10MB per render)
2026-02-25 22:57:34 +01:00
darken 76c55bf3be feat(screenshots): Add case pop-up screenshot, reorder Play Store sequence
- Add CasePopUp as screenshot 3 (popup shown on case open)

- Reorder: WidgetConfiguration moves to 4, app screens follow at 5-8

- Add Widget Configuration screen title heading

- Fix appearance card reset button to always wrap below title

- Balance padding on requires-Pro notice in widget config bottom bar
2026-02-25 22:57:34 +01:00
darken fc9bcafae9 feat(screenshots): Set up localized Play Store screenshot pipeline
- Add Compose screenshot test infrastructure (build config, screenshotTest source set)

- Add 7 localized screenshots covering all key app screens

- Device spec set to Pixel 8 (1080x2400, 428dpi); batch size 4 for 1080p memory budget

- Add screen title heading and fix appearance card header layout in WidgetConfigurationScreen

- Balance padding on requires-Pro notice in widget config bottom bar
2026-02-25 22:57:34 +01:00
darken c4e58917da Add instruction manual for automated screenshots via debugbadger 2026-02-25 22:57:34 +01:00
Matthias Urhahn 3730a78e7a Update links and badges in README.md 2026-02-25 10:27:01 +01:00
Matthias Urhahn 222a6c43c9 Update README.md 2026-02-25 10:25:38 +01:00
Matthias Urhahn b1fe8490d3 Merge pull request #412 from d4rken-org/fix/settings-category-ordering
fix(settings): Move monitor and scanner mode out of appearance category
2026-02-25 10:20:29 +01:00
darken a0fe20e296 fix(settings): Move monitor and scanner mode above appearance category
Monitor mode and scanner mode are functional settings, not appearance
settings. Give them their own "Monitoring" category header and place
them at the top of the general settings screen.
2026-02-25 10:18:04 +01:00
Matthias Urhahn e2d127dae9 Merge pull request #411 from d4rken-org/fix/popup-flicker-on-case-open
fix(popup): Fix double popup flicker when opening AirPods case
2026-02-25 09:55:14 +01:00
darken b5c48be08e fix(popup): Update popup content in place instead of recreating overlay
When two popup triggers fire in rapid succession (e.g. connection + case open),
the overlay is now updated via Compose MutableState rather than torn down and
recreated, eliminating the visual flicker.
2026-02-25 00:34:19 +01:00
Matthias Urhahn 90f0c89b79 Merge pull request #410 from d4rken-org/feat/modernize-pod-cards
feat(ui): Modernize pod cards with circular battery gauges
2026-02-25 00:20:20 +01:00
darken 8dd165e1c8 feat(ui): Modernize pod cards with circular battery gauges
Replace LinearProgressIndicator bars with circular progress rings
around pod icons for a more visual, glanceable battery display.
2026-02-25 00:17:51 +01:00
Matthias Urhahn 119139cbc5 Merge pull request #409 from d4rken-org/feat/modernize-widget-config-screen
feat(widget): Modernize widget configuration screen
2026-02-24 23:36:44 +01:00
darken 5159e4c21b feat(widget): Modernize widget configuration screen with card layout and animations
Add card-based section grouping, animated transitions for custom mode,
animated color swatch selection, preset chip color indicators, and
Surface-based bottom bar and preview wrapper.
2026-02-24 23:35:07 +01:00
Matthias Urhahn 1c94354831 Merge pull request #408 from d4rken-org/fix/profile-unsaved-changes-false-positive
Fix false-positive unsaved changes dialog and popup notifications
2026-02-24 23:17:49 +01:00
darken 75d2bf0169 fix(popup): Prevent false-positive popups from BLE signal oscillation
Use profile-based identity matching and cooldown keys so two BLE
signals for the same AirPods share one cooldown timer. Revert
connection monitor to show-once-per-connection behavior.
2026-02-24 23:08:16 +01:00
darken 7e73777469 fix(profiles): Pass profileId from NavKey and reset state on exit
Navigation 3 doesn't auto-populate SavedStateHandle from NavKey args
like Navigation 2.x did. Pass profileId explicitly from the entry
lambda through the ScreenHost to the ViewModel via initialize().

Reset initialized flag on every exit path so re-entering a profile
reloads fresh data from the repository.
2026-02-24 20:38:04 +01:00
darken 3d07c284b9 fix(profiles): Don't show unsaved changes dialog when nothing was edited
In create mode, the init block pre-filled _currentState with a default
name but left _initialState empty. The hasUnsavedChanges() check saw
the non-blank name as a change, triggering the dialog on back press
even without user edits.

Set _initialState to match _currentState defaults in create mode and
unify the comparison logic to always use current != initial.
2026-02-24 20:38:04 +01:00
darken f6b1c69edc fix(profiles): Pass profileId from NavKey and reset state on exit
Navigation 3 doesn't auto-populate SavedStateHandle from NavKey args
like Navigation 2.x did. Pass profileId explicitly from the entry
lambda through the ScreenHost to the ViewModel via initialize().

Reset initialized flag on every exit path so re-entering a profile
reloads fresh data from the repository.
2026-02-24 20:33:54 +01:00
darken f421deca49 fix(profiles): Don't show unsaved changes dialog when nothing was edited
In create mode, the init block pre-filled _currentState with a default
name but left _initialState empty. The hasUnsavedChanges() check saw
the non-blank name as a change, triggering the dialog on back press
even without user edits.

Set _initialState to match _currentState defaults in create mode and
unify the comparison logic to always use current != initial.
2026-02-24 18:48:32 +01:00
darken e19608f567 chore: Remove stale artifacts from Compose migration
Delete dead XML layouts, unused drawable icons, the orphaned
RecyclerView adapter infrastructure, and Fragment-specific
extension functions left behind by the Compose migration.
2026-02-24 18:45:55 +01:00
darken 06f4115808 feat(ui): Add theme mode, style, and color settings
Add user-facing theme preferences with three independent axes:
- Theme mode (System/Dark/Light)
- Theme style (Default/Material You/Medium Contrast/High Contrast)
- Theme color (Blue/Green/Amber)

Includes safe Moshi enum fallback for corrupted preference values,
color palettes for all combinations, and window background sync
to prevent flash during navigation transitions.
2026-02-24 18:33:33 +01:00
darken 2ca8f686c8 feat(ui): Add Compose previews for all UI components and screens
Add @Preview2 multi-preview annotation (light/dark), mock data provider
with realistic device scenarios, and ~45 preview functions across cards,
screens, popup, and settings components.
2026-02-24 17:19:44 +01:00
darken 679250f7da fix(ui): Determine start destination before composing backstack
Check onboarding state in MainActivity before creating the NavBackStack,
so the correct screen is shown from the first frame. Removes the
redundant async check from OverviewViewModel that caused the dashboard
to briefly flash before redirecting to onboarding on fresh installs.
2026-02-24 16:50:05 +01:00
darken 8df2d78070 refactor(ui): Replace drawable icons with Material TwoTone Compose icons
Switch all Compose screens from Icons.Default/Filled and painterResource
drawable references to Icons.TwoTone for a consistent two-tone icon style.
Delete 24 drawable XML files that are no longer referenced.
2026-02-24 15:23:47 +01:00
darken 63692595a8 refactor(ui): Migrate from Fragments to Jetpack Compose
Replace Fragment-based UI with Jetpack Compose screens, adopting
Navigation3 for type-safe routing and Material3 theming throughout.

- Replace all Fragments/ViewHolders with Compose screens and cards
- Introduce NavigationController with Navigation3 runtime
- Add CapodTheme with Material3 design system
- Migrate PopUpWindow overlay to ComposeView with proper lifecycle
- Rewrite WidgetConfigurationActivity in Compose
- Add reusable settings composables (switch, slider, list preference)
- Update Kotlin to 2.2.10, add Compose BOM 2025.06.01
- Fix reactive state for unmatched devices toggle
- Add safety timeout for BT device enumeration
- Fix profile name validation error message
2026-02-24 15:05:09 +01:00
darken 576c6db8d0 Release: 3.1.0-rc0 2026-02-23 09:56:40 +01:00
darken 524cac8d30 General: Update app translations from Crowdin 2026-02-23 09:53:52 +01:00
darken 8939f788ad General: Update app translations from Crowdin
Pull latest string translations from Crowdin.
Validated and reverted any problematic changes.
2026-02-23 00:38:50 +01:00
darken 9ad3f3c180 refactor(monitor): Remove shared mutable builder from MonitorNotifications
Replace shared NotificationCompat.Builder + Mutex with a stateless
design where each method builds a fresh notification. Introduces a
baseBuilder() helper to deduplicate common setup. Removes builderLock
and suspend modifiers from getNotification/getNotificationConnected.
2026-02-22 06:32:12 +01:00
darken 5b59ddab0f fix(monitor): Prevent crash when OS denies foreground service start
Add promoteToForeground() helper that catches ForegroundServiceStartNotAllowedException
(Android 12+) and SecurityException, allowing graceful service exit instead of crashing.
2026-02-22 06:24:51 +01:00
darken ab24c48024 General: Update app translations from Crowdin
Pull latest string translations from Crowdin.
Validated and reverted any problematic changes.
2026-02-21 20:09:52 +01:00
darken a6482c7d76 feat(popup): Move signal quality indicator to top-right corner
Always show signal quality when available instead of gating on debug
mode. Position icon + percentage text in the top-right corner of the
card, aligned with the title. Remove unused DebugSettings dependency.
2026-02-21 19:22:07 +01:00
darken 99aa44b448 chore: Remove obsolete vector XML drawables replaced by PNGs 2026-02-21 19:22:07 +01:00
darken a801f2d701 feat(popup): Add product photo icons for all device models
Replace vector silhouette placeholders with actual product photo PNGs
for 14 device families across 5 DPI buckets. Fix drawable naming prefix
(devic_ -> device_), add per-model icon overrides for popup left/right/case
views, disable image tinting for color PNGs, fix inverted signal visibility
logic, and use monochrome icon for notification small icon.
2026-02-21 19:22:07 +01:00
darken 8405406d22 feat(popup): Redesign connection popup with Apple-inspired layout
Large centered device graphics (64dp), vertically stacked battery info,
adaptive width, rounded card with tonal close button.
2026-02-21 19:10:56 +01:00
darken 9785b5ea7f fix(playpause): Improve test coverage and clarify code comments
Add missing test cases for single-pod one-pod-mode pause direction,
EarDetectionState single-pod properties, and normal-mode steady-state.
Fix stale test comments that incorrectly claimed tests would fail.
Clarify onePodMode flow combine pattern and isWorn field semantics.
2026-02-21 06:21:07 +01:00
darken fd7f9f8ab3 Refactor PlayPause logic and fix behavior for SinglePods (e.g. AirPods Max)
Fix edge case where removing a pod while both were in ears didn't trigger pause in one-pod mode. The aggregate boolean (isEitherPodInEar) stayed true, masking the individual pod removal.

See thread by `ZV`:
https://discord.com/channels/548521543039189022/1437499888068726814
2026-02-21 06:21:07 +01:00
darken 46bb8c98de feat(widget): Add theme customization to widget configuration
Add color presets, custom color picker, transparency slider, and device
label toggle to the widget configuration screen. Widget preview updates
in real-time. Use profile label as device name in both preview and
actual widget.
2026-02-20 12:22:55 +01:00
darken b3ead17b12 Update Claude settings to remove various Bash command permissions 2026-02-20 10:16:21 +01:00
darken a794d59276 chore: Update Romanian app title in Fastlane metadata 2026-02-19 09:01:03 +01:00
darken 584c5c70d0 chore: Update Ruby dependencies in Gemfile.lock
Updated fastlane (2.232.1), aws-sdk, and google-cloud gems. Added several standard library shims (csv, logger, mutex_m, etc.) to support newer Ruby environments.
2026-02-19 09:01:03 +01:00
darken 5a55796518 General: Update app translations from Crowdin
Pull latest string translations from Crowdin.
Validated and reverted any problematic changes.
2026-02-19 09:01:03 +01:00
darken bdfdd93735 General: Update fastlane translations from Crowdin
Pull latest translations for app store listings from Crowdin.
Validated and reverted any problematic changes.
2026-02-19 09:01:03 +01:00
Matthias Urhahn e9a937e4f6 Remove duplicate RB Status badge
Removed duplicate RB Status badge from README.
2026-02-19 08:42:38 +01:00
Matthias Urhahn 0a8bae46a9 Add RB Status badge to README
Added RB Status badge to README.md
2026-02-19 08:30:01 +01:00
darken 7efcafbc71 chore: Bump Android Gradle Plugin to 9.0.1 2026-02-19 00:20:43 +01:00
darken 6b3f431e9e Release: 3.0.5-rc0 2026-02-16 10:38:54 +01:00
darken 6014004c0f Update translations (part 2) 2026-02-16 10:38:09 +01:00
darken ebbc5394a7 Update translations 2026-02-16 10:38:09 +01:00
darken e36868a2fc chore: Update app translations from Crowdin 2026-02-16 10:38:09 +01:00
darken 1c6fb8ed31 Update Claude settings to allow more Git and Crowdin commands 2026-02-15 17:42:42 +01:00
darken e780fd17d2 Update configuration paths for signing and Crowdin
Move local config files from `~/.appconfig/` to `~/.config/projects/`.
2026-02-14 15:36:16 +01:00
darken 86d3f3bc61 fix: Call startForeground() before Hilt DI to prevent timing crash
Move startForeground() before super.onCreate() in MonitorService to
avoid ForegroundServiceDidNotStartInTimeException when Hilt DI is slow
on backgrounded cold starts. An early minimal notification is shown
immediately, then replaced with the full one after DI completes.
2026-02-13 14:39:12 +01:00
darken ba554c8787 Release: 3.0.4-rc1 2026-02-09 16:51:10 +01:00
darken 968829072a chore: Update app translations from Crowdin
Pull latest string translations from Crowdin.
Validated and reverted any problematic changes.
2026-02-09 16:48:43 +01:00
darken 0d1f331551 Release: 3.0.4-rc0 2026-02-09 16:22:31 +01:00
darken a8bb42d40b fix(ci): Narrow APK glob to avoid uploading duplicates
The copyTo-based APK renaming (from AGP 9 upgrade) leaves both the
original and renamed APK in the output directory. Narrow the glob to
only match renamed APKs.
2026-02-09 16:16:18 +01:00
darken 9a3fb11ef7 fix: Use correct labels for upgrade and donate menu items
Both menu items were using settings_general_label ("Settings") instead
of their proper labels, affecting accessibility and long-press tooltips.
2026-02-09 16:07:25 +01:00
darken 12d2c4dd06 fix: Pre-release cleanup of manifest, resource leaks, and dependencies
- Fix BleScanResultReceiver package name in manifest (.bluetooth → .common.bluetooth)
- Fix HandlerThread leak in BluetoothManager2 when registerReceiver() throws
- Cache battery values in MonitorNotifications to match hardening pattern
- Remove duplicate fragment-ktx dependency and align fragment-testing version
- Remove deprecated lifecycle-extensions dependency
2026-02-09 15:50:39 +01:00
darken cfed733fa7 fix: Guard MonitorService start against missing Bluetooth permissions 2026-02-09 14:24:45 +01:00
darken 1af8743533 chore: Remove redundant gradle.properties after AGP 9 upgrade 2026-02-09 07:50:36 +01:00
darken 2f3f1f3f70 fix(ci): Remove non-debug unit test variants for AGP 9 compatibility 2026-02-08 17:31:19 +01:00
darken 6223524a1e chore: Upgrade to AGP 9.0.0 and Gradle 9.3.1 2026-02-08 17:31:19 +01:00
darken ebfffb1536 fix: Replace WorkManager with ForegroundService to eliminate excessive wakelocks
Replace MonitorWorker (CoroutineWorker) with MonitorService (ForegroundService)
to eliminate persistent ProcessorForegroundLck wakelock reported in Android Vitals.
Add BootCompletedReceiver for post-reboot auto-start.
Remove WorkManager dependency entirely.
2026-02-08 14:19:11 +01:00
darken 5aa36b148b fix: Harden battery display against native crashes
Remove thread-unsafe shared RelativeDateTimeFormatter instance and cache
battery percentage values to local variables at all call sites.
2026-02-08 04:25:30 +01:00
darken 29391feaf8 chore: Remove stale CLAUDE.md exclusion from _config.yml 2026-02-07 20:17:51 +01:00
darken 4e279ca44d chore: Restructure Claude Code config into modular rules
Migrate monolithic root CLAUDE.md into .claude/rules/ structure
with focused topic files and glob-based contextual loading.
Clean up leftover helper scripts in .claude/tmp/.
2026-02-07 19:31:28 +01:00
darken 9ff8d9e04e fix: Handle display cutouts in landscape mode
EdgeToEdgeHelper now considers both systemBars() and displayCutout()
insets when applying padding. This prevents camera cutouts from
obscuring UI content when the device is in landscape orientation.
2026-01-17 14:20:21 +01:00
darken 10d5a823fc fix: Make widget clicks unique
Use the widget ID as the request code for `PendingIntent`. This ensures that each widget instance has a distinct `PendingIntent`, preventing them from overwriting each other and allowing clicks on multiple widgets to work correctly.
2025-11-11 08:37:37 +01:00
darken 54afadb136 fix: Prevent memory leak in WidgetConfigurationActivity
Uses the application context instead of the activity context when updating the widget. This avoids leaking the activity instance if the update operation outlives the configuration screen.
2025-11-11 08:37:37 +01:00
darken e4db9387ca Update translations 2025-11-10 08:38:12 +01:00
darken 4834eb276a Widget: Fix layout for devices with single charge detection
The instance check was being done on the wrong value, so it was never `true`.
2025-11-09 15:03:04 +01:00
darken 5860bbffb6 Release: 3.0.3-rc0 2025-10-29 15:52:54 +01:00
darken da53d68190 Update translations 2025-10-29 15:52:04 +01:00
darken b397bf190c Update translations 2025-10-29 15:52:04 +01:00
darken 15d664c361 feat: Make connectedDevices flow more robust
Adds retry and catch logic to handle failures gracefully.
2025-10-29 15:51:48 +01:00
darken d2f24d8b95 fix: Improve BluetoothManager stability
Enhances robustness by adding extensive error handling around broadcast receiver registration, profile event processing, and service disconnections to prevent crashes. Also ensures device flow is cleared when Bluetooth is disabled.
2025-10-29 15:51:48 +01:00
darken b5436659b6 refactor: Make connectedDevices a hot StateFlow
Convert `connectedDevices()` from a function returning a cold Flow to a property that is a hot `StateFlow`. This simplifies call sites and improves efficiency by sharing the underlying subscription.
2025-10-29 15:51:48 +01:00
darken 1cfc4611cb Improve Bluetooth device connection monitoring
Switched from monitoring generic ACL events to specific Headset profile state changes for more reliable device connection and disconnection detection.

This fixes a race condition where CAPod thinks no device is connected because we triggered too early, before "connectedDevices" on the HEADSET profile contains our target device.

This fixes #313
2025-10-29 15:14:45 +01:00
darken bda0a20743 Release: 3.0.2-rc0 2025-10-11 08:02:51 +02:00
darken 09bad90c0c feat: Add and update translations for Dutch and Mexican Spanish 2025-10-11 07:50:32 +02:00
darken 34856607f8 Fix crash on start due to R8
Closes #341
2025-10-11 07:45:42 +02:00
1529 changed files with 41625 additions and 12511 deletions
+1
View File
@@ -0,0 +1 @@
worktrees/
+49
View File
@@ -0,0 +1,49 @@
# CAPod - Companion for AirPods
Android app that detects and monitors AirPods via Bluetooth LE. Displays battery levels, triggers popup notifications on case open, and provides home screen widgets.
## Project Structure
| Module | Description |
|--------|-------------|
| `app/` | Main Android app (FOSS and Google Play flavors) |
| `app-common/` | Shared code between phone and Wear OS apps |
## Build Flavors
- **FOSS** (`foss`): Open-source, no Google Play dependencies
- **Google Play** (`gplay`): Includes billing client for IAP
Quick build check: `./gradlew assembleFossDebug`
## Key Locations
| Path | Contains |
|------|----------|
| `app/src/main/java/` | Main app source (activities, fragments, services) |
| `app-common/src/main/java/` | Shared logic (monitor, bluetooth, models) |
| `app/src/main/res/` | Layouts, drawables, strings |
| `app-common/src/test/` | Unit tests |
| `app/build.gradle.kts` | App build config, dependencies, flavors |
| `app/src/debug/java/.../screenshots/` | Play Store screenshot content composables |
| `fastlane/` | Screenshot generation scripts, Play Store metadata |
## Development Tips
- Use `assembleFossDebug` as the fastest build variant for iteration
- Shared code goes in `app-common/`, app-specific code in `app/`
- Follow existing patterns — the codebase uses MVVM + Hilt + Coroutines
- Always use string resources for user-facing text (see localization rules)
- Check `git log --oneline -20` for commit message style before committing
## Rules Reference
Detailed guidelines are in `.claude/rules/`:
- `architecture.md` — Module structure, key components, data flow, dependencies
- `build-commands.md` — Build, test, lint, and release commands
- `localization.md` — String resource naming conventions
- `commit-guidelines.md` — Commit message format and prefixes
- `pull-requests.md` — PR title and description conventions
- `agent-instructions.md` — Sub-agent delegation and critical thinking
- `screenshots.md` — Play Store screenshot pipeline, commands, adding new screens
+39
View File
@@ -0,0 +1,39 @@
---
description: Instructions for Claude Code sub-agents and task delegation
globs:
- "**"
---
# Agent Instructions
## Critical Thinking
- Do not blindly accept information at face value
- Verify assumptions against actual code before proceeding
- When encountering unexpected behavior, investigate root causes rather than applying workarounds
- If something seems wrong, it probably is — dig deeper
## Explore vs. Implement
- **Explore first**: Before making changes, understand the existing code structure and patterns
- **Read before writing**: Always read relevant files before modifying them
- **Follow existing patterns**: Match the code style and architecture already in use
- **Minimal changes**: Only change what's necessary to accomplish the task
## Sub-Agent Delegation
When using Task tool to spawn sub-agents:
- Provide complete context — sub-agents don't share your conversation history unless noted
- Be specific about what you need: research only, or research + implementation
- Use `Explore` agent type for codebase investigation
- Use `Bash` agent type for running builds and tests
- Parallelize independent sub-agent tasks for efficiency
## Common Pitfalls
- Don't create new files when editing existing ones would suffice
- Don't add features beyond what was requested
- Don't refactor surrounding code when fixing a bug
- Don't add comments or documentation to code you didn't change
- Don't guess at file paths — use Glob/Grep to find them
+84
View File
@@ -0,0 +1,84 @@
---
description: Architecture overview, module structure, key components, data flow, and dependencies
globs:
- "app/**/*.kt"
- "app-common/**/*.kt"
- "**/*.gradle.kts"
---
# Architecture
## Multi-Module Structure
- **app/**: Main Android application with FOSS and Google Play flavors
- **app-common/**: Shared code between main app and Wear OS app
## Core Patterns
- **MVVM**: ViewModels with LiveData/StateFlow for UI state management
- **Dependency Injection**: Hilt/Dagger for dependency management
- **Coroutines**: Kotlin coroutines for async operations
- **Repository Pattern**: Data layer abstraction for monitoring and settings
## Key Components
### PodMonitor System
- `PodMonitor`: Core service that detects and tracks AirPods via Bluetooth LE
- `MonitorControl`: Manages MonitorService lifecycle
- `MonitorService`: Foreground service that continuously scans for AirPods
- `BluetoothEventReceiver`: Handles system Bluetooth events
### Reaction System
- `ReactionSettingsFragment`: Configuration for popup notifications
- `PopUpWindow`: Displays AirPods status when case is opened
- `PopUpPodViewFactory`: Creates UI components for different pod models
### Common Utilities
- `EdgeToEdgeHelper`: Handles Android edge-to-edge display insets
## Build Configuration
### Flavors
- **FOSS**: Open-source version without Google Play dependencies
- **Google Play (gplay)**: Version with billing client for in-app purchases
### Build Types
- **debug**: Unobfuscated, full logging, no minification
- **beta**: Obfuscated, production-ready with strict lint checks
- **release**: Fully optimized for production distribution
## Data Flow
The app follows a unidirectional data flow:
1. `BluetoothEventReceiver` detects Bluetooth events
2. `MonitorService` scans for AirPods beacon data
3. `PodMonitor` processes and stores device information
4. ViewModels observe monitor data via repositories
5. UI components react to ViewModel state changes
6. `ReactionSystem` triggers popups and notifications
## Bluetooth LE Implementation
The app uses Android's Bluetooth LE APIs to scan for Apple device advertisements. The core scanning logic is in `MonitorService` which runs as a foreground service.
## Multi-Platform Considerations
Code shared between phone and Wear OS apps is placed in `app-common`. When modifying shared functionality, ensure compatibility across both platforms.
## Testing Strategy
- **Unit Tests**: Located in `app-common/src/test/` for shared logic
- **Test Flavors**: Separate test configurations for FOSS and Google Play variants
## Key Dependencies
- **Hilt**: Dependency injection framework
- **AndroidX Navigation**: Fragment navigation with SafeArgs
- **kotlinx.serialization**: JSON serialization for configuration and caching
- **Material Design**: UI components following Material Design guidelines
+71
View File
@@ -0,0 +1,71 @@
---
description: Build, test, lint, and release commands for Gradle
globs:
- "**/*.gradle.kts"
- "**/*.gradle"
- "gradle/**"
---
# Build Commands
## Build
```bash
# Build debug version
./gradlew assembleDebug
# Build all variants (FOSS and Google Play flavors)
./gradlew assemble
# Build specific flavor and type
./gradlew assembleFossDebug
./gradlew assembleGplayRelease
# Build app bundles for Play Store
./gradlew bundleGplayRelease
```
## Testing
```bash
# Run all unit tests
./gradlew test
# Run unit tests for specific variant
./gradlew testFossDebugUnitTest
# Run instrumentation tests (requires connected device/emulator)
./gradlew connectedAndroidTest
./gradlew connectedFossDebugAndroidTest
# Run all checks (lint + tests)
./gradlew check
```
## Code Quality
```bash
# Run lint for all variants
./gradlew lint
# Run lint for specific variant
./gradlew lintFossDebug
# Auto-fix lint issues where possible
./gradlew lintFix
# Update lint baseline
./gradlew updateLintBaseline
```
## Release
```bash
./gradlew assembleFossRelease assembleGplayRelease
```
## Notes
- Use `assembleFossDebug` as the default quick-check build (fastest variant)
- Run `./gradlew check` before submitting changes to catch lint and test issues
- Instrumentation tests require a connected device or running emulator
+41
View File
@@ -0,0 +1,41 @@
---
description: Git commit message format and conventions
globs:
- "**"
---
# Commit Guidelines
## Format
```
<prefix>: <Short summary>
```
Summary line should be concise and describe the change. No period at the end.
## Prefixes
Use the existing commit history as reference. Common prefixes:
- **fix**: Bug fixes (e.g., `fix: Handle display cutouts in landscape mode`)
- **feat**: New features
- **refactor**: Code restructuring without behavior change
- **chore**: Maintenance, dependency updates, build config
- **docs**: Documentation changes
## Component Scope (optional)
When a change is scoped to a specific area, include it after the prefix:
- `fix(widget): Fix layout for devices with single charge detection`
- `feat(monitor): Add battery level caching`
- `refactor(popup): Extract pod view factory`
## Rules
- Keep the summary line under 72 characters
- Use imperative mood ("Add feature" not "Added feature")
- Reference issue numbers when applicable
- Do not include `Co-authored-by` trailers
- Look at recent `git log` output to match the project's existing style
+21
View File
@@ -0,0 +1,21 @@
---
description: Guidelines for adding and naming Android string resources
globs:
- "**/res/values*/strings.xml"
---
# Localization Guidelines
When adding new user-facing strings:
- **Always use string resources**: Never hardcode user-facing text in layouts or code
- **Follow naming conventions**: Use descriptive, hierarchical naming (e.g., `profiles_name_default`, `settings_bluetooth_enabled`)
- **Provide context**: String names should indicate usage and location
- **Consider pluralization**: Use Android plural resources (`<plurals>`) when quantities vary
## Naming Examples
- `profiles_create_title` (screen title)
- `profiles_name_label` (form field label)
- `profiles_delete_confirmation` (dialog message)
- `error_network_unavailable` (error message)
+78
View File
@@ -0,0 +1,78 @@
---
description: Pull request naming and description conventions
globs:
- "**"
---
# Pull Request Guidelines
## PR Title Format
```
<Category>: <Short user-facing summary>
```
PR titles appear in auto-generated changelogs and are read by users. Use **ELI5, user-facing language** — no internal class names, library names, or implementation details.
## Category Prefixes
| Prefix | Covers |
|--------|--------|
| **Widget** | Home screen widget |
| **Reaction** | Case-open popup, auto-play/pause, notification triggers |
| **Device** | AirPods detection, compatibility, Bluetooth scanning, battery reading, device profiles |
| **General** | Dashboard, settings, notifications, themes, onboarding, support, app-wide UI |
| **Fix** | Bug fixes spanning multiple areas |
### Title Examples
- `Widget: Add color themes and transparency slider`
- `Reaction: Fix popup appearing twice when opening AirPods case`
- `Device: Add support for AirPods 4 with ANC`
- `General: Add dark mode and color theme settings`
- `Fix: Fix battery display stuck at 0% after reconnecting`
### Bad Titles (too technical)
- `refactor(settings): Migrate preferences to AndroidX DataStore`
- `feat(widget): Migrate to Jetpack Glance`
- `refactor(ui): Migrate from Fragments to Jetpack Compose`
## PR Description Format
### What changed
User-friendly explanation of what this PR does. Describe the problem that was fixed or the feature that was added from the user's perspective. No internal class or method names.
For non-user-facing PRs (refactors, tests, CI, dependency bumps): write "No user-facing behavior change" followed by a brief internal description.
### Technical Context
Explain what's hard to extract from the diff alone. Focus on:
- **Why** this approach was chosen (and alternatives considered/rejected)
- **Root cause** for bug fixes (the diff shows the fix, not what caused it)
- **Non-obvious side effects** or behavioral changes not apparent from reading the code
- **Review guidance** — what's tricky or deserves close attention
Keep it scannable with bullet points. Don't restate what's visible in the diff (file names, class renames, line-level changes).
### Example
```markdown
## What changed
Fixed a crash that could happen when the AirPods case is opened while Bluetooth is turning off.
## Technical Context
- Root cause: `MonitorService` continued processing scan results during Bluetooth adapter state change, hitting a null adapter reference
- Chose to gate on adapter state in the scan callback rather than adding a separate BroadcastReceiver, since the service already observes adapter state for restart logic
- The timing window is ~200ms between ACTION_STATE_CHANGING and ACTION_STATE_OFF — only reproducible on Pixel devices with aggressive Bluetooth power management
```
## Conventions
- **Issue references**: Use "Closes #123", "Fixes #123", or "Resolves #123"
- **Breaking changes**: Mark with "BREAKING:" prefix if applicable
- **No `Co-authored-by` trailers** (per project convention)
+68
View File
@@ -0,0 +1,68 @@
# Play Store Screenshot Pipeline
## Overview
Localized screenshots are generated using Compose Preview Screenshot Testing (alpha), rendered offline (no device needed), and sorted into fastlane metadata directories for Play Store upload.
## Pipeline
```
ScreenshotContent.kt (mock data + composables)
→ PlayStoreScreenshots.kt (@PreviewTest entry points)
→ PlayStoreLocales.kt (multi-preview locale annotations, auto-generated per batch)
→ generate_screenshots.sh (batched Gradle runs to avoid OOM)
→ copy_screenshots.sh (sort PNGs into fastlane locale dirs)
→ fastlane/metadata/android/{locale}/images/phoneScreenshots/
```
## Key Files
| File | Purpose |
|------|---------|
| `app/src/debug/java/.../screenshots/ScreenshotContent.kt` | Mock data composables for each screen (8 screens) |
| `app/src/screenshotTest/kotlin/.../screenshots/PlayStoreScreenshots.kt` | `@PreviewTest` functions that wire content to locale annotations |
| `app/src/screenshotTest/kotlin/.../screenshots/PlayStoreLocales.kt` | Multi-preview annotations (auto-generated by batch script) |
| `fastlane/generate_screenshots.sh` | Batched generation across 68 locales |
| `fastlane/copy_screenshots.sh` | Copies rendered PNGs into fastlane structure |
## Commands
```bash
# Full run — all 68 locales, ~12 batches, ~7 minutes
./fastlane/generate_screenshots.sh
# Smoke test — 6 locales (en, de, ja, ar, zh-CN, pt-BR), single batch
./fastlane/generate_screenshots.sh --smoke
# Copy into fastlane directories (run after generate)
./fastlane/copy_screenshots.sh
# Clean copy (removes old screenshots first)
./fastlane/copy_screenshots.sh --clean
```
## Adding a New Screenshot
1. Add a composable content function in `ScreenshotContent.kt` (e.g. `NewScreenContent()`)
2. Add a `@PreviewTest` function in `PlayStoreScreenshots.kt` that calls it
3. Add the function name → filename mapping in `copy_screenshots.sh` `SCREEN_MAP`
4. Update the expected count in `generate_screenshots.sh` (composables per locale)
5. Run the full pipeline: `generate_screenshots.sh` then `copy_screenshots.sh`
## After UI Changes
When modifying a screen that appears in screenshots (check `ScreenshotContent.kt`), regenerate:
```bash
./fastlane/generate_screenshots.sh
./fastlane/copy_screenshots.sh --clean
```
## Technical Notes
- Batch size defaults to 2 locales (16 renders) to avoid layoutlib memory leak (~10MB/image)
- Gradle daemon is stopped between batches to release memory
- `PlayStoreLocales.kt` is temporarily rewritten per batch and restored via trap
- Device spec: 1080x2400px @ 428 DPI (Pixel-class phone)
- Uses `com.android.compose.screenshot` plugin v0.0.1-alpha13
- Output: `app/src/screenshotTestGplayDebug/reference/`
+8 -13
View File
@@ -2,20 +2,15 @@
"permissions": {
"allow": [
"mcp__ide__getDiagnostics",
"Bash(./gradlew tasks:*)",
"Bash(./gradlew:*)",
"Bash(find:*)",
"Bash(ls:*)",
"Bash(grep:*)",
"Bash(rg:*)",
"WebSearch",
"WebFetch(domain:support.google.com)",
"WebFetch(domain:github.com)",
"WebFetch(domain:mvnrepository.com)",
"WebFetch(domain:kotlinlang.org)",
"WebFetch(domain:developer.android.com)",
"WebFetch(domain:issuetracker.google.com)"
"WebSearch"
],
"deny": []
},
"enabledPlugins": {
"android-translation@claude-code-cafe": true,
"debugbadger@claude-code-cafe": true,
"jvm-tools@claude-code-cafe": true,
"frontend-design@claude-plugins-official": true,
"kotlin-lsp@claude-plugins-official": true
}
}
-59
View File
@@ -1,59 +0,0 @@
#!/usr/bin/env python3
"""
Script to fix import statements after removing app-common module.
This script fixes R class imports from app-common to app module.
"""
import os
import re
from pathlib import Path
def fix_r_import(file_path):
"""Fix R import in a single file."""
print(f"Fixing R import in: {file_path}")
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Replace the import statement
updated_content = re.sub(
r'import eu\.darken\.capod\.common\.R$',
'import eu.darken.capod.R',
content,
flags=re.MULTILINE
)
if updated_content != content:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(updated_content)
print(f" ✓ Updated import in {file_path}")
return True
else:
print(f" - No changes needed in {file_path}")
return False
def main():
"""Main function to fix all import statements."""
project_root = Path.cwd()
app_src = project_root / "app" / "src"
# Find all Kotlin files that import eu.darken.capod.common.R
files_to_fix = []
for kt_file in app_src.rglob("*.kt"):
with open(kt_file, 'r', encoding='utf-8') as f:
content = f.read()
if re.search(r'import eu\.darken\.capod\.common\.R$', content, re.MULTILINE):
files_to_fix.append(kt_file)
print(f"Found {len(files_to_fix)} files with incorrect R imports")
success_count = 0
for file_path in files_to_fix:
if fix_r_import(file_path):
success_count += 1
print(f"\nFixed imports in {success_count} files")
return 0
if __name__ == "__main__":
exit(main())
-58
View File
@@ -1,58 +0,0 @@
#!/usr/bin/env python3
"""
Script to fix fully qualified R references in code.
This script finds and replaces eu.darken.capod.common.R with R.
"""
import os
import re
from pathlib import Path
def fix_qualified_r_references(file_path):
"""Fix fully qualified R references in a single file."""
print(f"Fixing qualified R references in: {file_path}")
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Replace fully qualified references
updated_content = re.sub(
r'\beu\.darken\.capod\.common\.R\.',
'R.',
content
)
if updated_content != content:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(updated_content)
print(f" ✓ Fixed qualified R references in {file_path}")
return True
else:
print(f" - No changes needed in {file_path}")
return False
def main():
"""Main function to fix all qualified R references."""
project_root = Path.cwd()
app_src = project_root / "app" / "src"
# Find all Kotlin files that have qualified R references
files_to_fix = []
for kt_file in app_src.rglob("*.kt"):
with open(kt_file, 'r', encoding='utf-8') as f:
content = f.read()
if re.search(r'\beu\.darken\.capod\.common\.R\.', content):
files_to_fix.append(kt_file)
print(f"Found {len(files_to_fix)} files with qualified R references")
success_count = 0
for file_path in files_to_fix:
if fix_qualified_r_references(file_path):
success_count += 1
print(f"\nFixed qualified R references in {success_count} files")
return 0
if __name__ == "__main__":
exit(main())
-87
View File
@@ -1,87 +0,0 @@
#!/usr/bin/env python3
"""
Script to fix remaining R import issues.
This script finds files that use R.* but don't have R imports and adds them.
"""
import os
import re
from pathlib import Path
def needs_r_import(file_path):
"""Check if file uses R.* but doesn't import R."""
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Check if file uses R.something
uses_r = re.search(r'\bR\.[a-zA-Z_]', content)
if not uses_r:
return False
# Check if file already imports R
has_import = re.search(r'import.*\.R$', content, re.MULTILINE)
if has_import:
return False
return True
def add_r_import(file_path):
"""Add R import to a file."""
print(f"Adding R import to: {file_path}")
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Find the package line
package_match = re.search(r'^package\s+[^\n]+$', content, re.MULTILINE)
if not package_match:
print(f" Error: No package declaration found in {file_path}")
return False
# Find existing imports
import_pattern = r'^import\s+[^\n]+$'
existing_imports = list(re.finditer(import_pattern, content, re.MULTILINE))
if existing_imports:
# Insert after the last import
last_import = existing_imports[-1]
insert_pos = last_import.end()
updated_content = (content[:insert_pos] +
"\nimport eu.darken.capod.R" +
content[insert_pos:])
else:
# Insert after package line
insert_pos = package_match.end()
updated_content = (content[:insert_pos] +
"\n\nimport eu.darken.capod.R" +
content[insert_pos:])
with open(file_path, 'w', encoding='utf-8') as f:
f.write(updated_content)
print(f" ✓ Added R import to {file_path}")
return True
def main():
"""Main function to fix all R import issues."""
project_root = Path.cwd()
app_src = project_root / "app" / "src"
# Find all Kotlin files that need R import
files_to_fix = []
for kt_file in app_src.rglob("*.kt"):
if needs_r_import(kt_file):
files_to_fix.append(kt_file)
print(f"Found {len(files_to_fix)} files that need R imports")
success_count = 0
for file_path in files_to_fix:
if add_r_import(file_path):
success_count += 1
print(f"\nAdded R imports to {success_count} files")
return 0
if __name__ == "__main__":
exit(main())
-105
View File
@@ -1,105 +0,0 @@
#!/usr/bin/env python3
"""
Script to merge strings.xml files from app-common module to app module.
This script merges all string entries from app-common into the corresponding app module files.
"""
import os
import re
import xml.etree.ElementTree as ET
from pathlib import Path
def merge_strings_xml(app_common_file, app_file):
"""Merge strings from app-common file into app file."""
print(f"Processing: {app_common_file.name} -> {app_file.name}")
# Read the app-common strings.xml content
with open(app_common_file, 'r', encoding='utf-8') as f:
common_content = f.read()
# Read the app strings.xml content
with open(app_file, 'r', encoding='utf-8') as f:
app_content = f.read()
# Extract string entries from app-common (including comments)
# Find everything between <resources> and </resources> tags, excluding the tags themselves
common_match = re.search(r'<resources[^>]*>(.*?)</resources>', common_content, re.DOTALL)
if not common_match:
print(f" Warning: No resources found in {app_common_file}")
return False
common_strings = common_match.group(1).strip()
if not common_strings:
print(f" Warning: No string content found in {app_common_file}")
return False
# Find the insertion point in the app file (before </resources>)
app_match = re.search(r'(.*?)(\s*</resources>)', app_content, re.DOTALL)
if not app_match:
print(f" Error: Invalid XML structure in {app_file}")
return False
# Merge the content
before_closing = app_match.group(1)
closing_tag = app_match.group(2)
# Add the common strings with proper spacing
merged_content = f"{before_closing}\n\n <!-- Strings from app-common -->\n{common_strings}\n{closing_tag}"
# Write the merged content back to the app file
with open(app_file, 'w', encoding='utf-8') as f:
f.write(merged_content)
print(f" ✓ Merged successfully")
return True
def main():
"""Main function to merge all strings.xml files."""
project_root = Path.cwd()
app_common_res = project_root / "app-common" / "src" / "main" / "res"
app_res = project_root / "app" / "src" / "main" / "res"
if not app_common_res.exists():
print(f"Error: app-common resources directory not found: {app_common_res}")
return 1
if not app_res.exists():
print(f"Error: app resources directory not found: {app_res}")
return 1
# Find all strings.xml files in app-common
common_strings_files = list(app_common_res.glob("*/strings.xml"))
if not common_strings_files:
print("Error: No strings.xml files found in app-common")
return 1
print(f"Found {len(common_strings_files)} strings.xml files to merge")
success_count = 0
error_count = 0
for common_file in sorted(common_strings_files):
# Determine the corresponding app file
locale_dir = common_file.parent.name
app_file = app_res / locale_dir / "strings.xml"
if not app_file.exists():
print(f" Error: Corresponding app file does not exist: {app_file}")
error_count += 1
continue
if merge_strings_xml(common_file, app_file):
success_count += 1
else:
error_count += 1
print(f"\nMerge complete:")
print(f" ✓ Successfully merged: {success_count}")
print(f" ✗ Errors: {error_count}")
return 0 if error_count == 0 else 1
if __name__ == "__main__":
exit(main())
+1 -1
View File
@@ -48,7 +48,7 @@ jobs:
strategy:
fail-fast: false
matrix:
variant: [ Debug,Beta,Release ]
variant: [ Debug ]
flavor: [ testFoss,testGplay ]
runs-on: ubuntu-22.04
steps:
+2 -2
View File
@@ -65,7 +65,7 @@ jobs:
tag_name: ${{ steps.tagger.outputs.tag }}
name: ${{ steps.tagger.outputs.tag }}
generate_release_notes: true
files: app/build/outputs/apk/foss/beta/*.apk
files: app/build/outputs/apk/foss/beta/eu.darken.capod-*.apk
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -77,7 +77,7 @@ jobs:
tag_name: ${{ steps.tagger.outputs.tag }}
name: ${{ steps.tagger.outputs.tag }}
generate_release_notes: true
files: app/build/outputs/apk/foss/release/*.apk
files: app/build/outputs/apk/foss/release/eu.darken.capod-*.apk
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+3 -1
View File
@@ -14,4 +14,6 @@
/fastlane/report.xml
/fastlane/Appfile
/fastlane/README.md
.kotlin
.kotlin
# Screenshot test reference images (ephemeral, regenerated on demand)
app/src/screenshotTest*/reference/
-158
View File
@@ -1,158 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Commands
### Build Commands
```bash
# Build debug version
./gradlew assembleDebug
# Build all variants (FOSS and Google Play flavors)
./gradlew assemble
# Build specific flavor and type
./gradlew assembleFossDebug
./gradlew assembleGplayRelease
# Build app bundles for Play Store
./gradlew bundleGplayRelease
```
### Testing Commands
```bash
# Run all unit tests
./gradlew test
# Run unit tests for specific variant
./gradlew testFossDebugUnitTest
# Run instrumentation tests (requires connected device/emulator)
./gradlew connectedAndroidTest
./gradlew connectedFossDebugAndroidTest
# Run all checks (lint + tests)
./gradlew check
```
### Code Quality Commands
```bash
# Run lint for all variants
./gradlew lint
# Run lint for specific variant
./gradlew lintFossDebug
# Auto-fix lint issues where possible
./gradlew lintFix
# Update lint baseline
./gradlew updateLintBaseline
```
### Release Commands
```bash
./gradlew assembleFossRelease assembleGplayRelease
```
## Architecture Overview
### Multi-Module Structure
- **app/**: Main Android application with FOSS and Google Play flavors
- **app-common/**: Shared code between main app
### Core Architecture Patterns
- **MVVM**: ViewModels with LiveData/StateFlow for UI state management
- **Dependency Injection**: Hilt/Dagger for dependency management
- **Coroutines**: Extensive use of Kotlin coroutines for async operations
- **Repository Pattern**: Data layer abstraction for monitoring and settings
### Key Components
#### PodMonitor System
- `PodMonitor`: Core service that detects and tracks AirPods via Bluetooth LE
- `MonitorControl`: Manages background monitoring worker lifecycle
- `MonitorWorker`: Background worker that continuously scans for AirPods
- `BluetoothEventReceiver`: Handles system Bluetooth events
#### Reaction System
- `ReactionSettingsFragment`: Configuration for popup notifications
- `PopUpWindow`: Displays AirPods status when case is opened
- `PopUpPodViewFactory`: Creates UI components for different pod models
#### Common Utilities
- `EdgeToEdgeHelper`: Handles Android edge-to-edge display insets
### Build Configuration
#### Flavors
- **FOSS**: Open-source version without Google Play dependencies
- **Google Play**: Version with billing client for in-app purchases
#### Build Types
- **debug**: Unobfuscated, full logging, no minification
- **beta**: Obfuscated, production-ready with strict lint checks
- **release**: Fully optimized for production distribution
### Data Flow Architecture
The app follows a unidirectional data flow:
1. `BluetoothEventReceiver` detects Bluetooth events
2. `MonitorWorker` scans for AirPods beacon data
3. `PodMonitor` processes and stores device information
4. ViewModels observe monitor data via repositories
5. UI components react to ViewModel state changes
6. `ReactionSystem` triggers popups and notifications
### Testing Strategy
- **Unit Tests**: Located in `app-common/src/test/` for shared logic
- **Test Flavors**: Separate test configurations for FOSS and Google Play variants
### Key Dependencies
- **Hilt**: Dependency injection framework
- **AndroidX Navigation**: Fragment navigation with SafeArgs
- **WorkManager**: Background task scheduling for monitoring
- **Moshi**: JSON serialization for configuration and debugging
- **Material Design**: UI components following Material Design guidelines
## Development Notes
### Bluetooth LE Implementation
The app uses Android's Bluetooth LE APIs to scan for Apple device advertisements. The core scanning logic is in
`MonitorWorker` which runs as a long-lived background task.
### Multi-Platform Considerations
Code shared between phone and Wear OS apps is placed in `app-common`. When modifying shared functionality, ensure
compatibility across both platforms.
### Localization Guidelines
When adding new user-facing strings:
- **Always use string resources**: Never hardcode user-facing text in layouts or code
- **Follow naming conventions**: Use descriptive, hierarchical naming (e.g., `profiles_name_default`, `settings_bluetooth_enabled`)
- **Provide context**: String names should indicate usage and location
- **Consider pluralization**: Use Android plural resources (`<plurals>`) when quantities vary
Examples of correct string naming:
- `profiles_create_title` (screen title)
- `profiles_name_label` (form field label)
- `profiles_delete_confirmation` (dialog message)
- `error_network_unavailable` (error message)
+70 -59
View File
@@ -1,48 +1,49 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.7)
base64
nkf
rexml
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
CFPropertyList (3.0.8)
abbrev (0.1.2)
addressable (2.8.8)
public_suffix (>= 2.0.2, < 8.0)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.2)
aws-partitions (1.1109.0)
aws-sdk-core (3.224.1)
aws-eventstream (1.4.0)
aws-partitions (1.1216.0)
aws-sdk-core (3.242.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
bigdecimal
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.101.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sdk-kms (1.122.0)
aws-sdk-core (~> 3, >= 3.241.4)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.188.0)
aws-sdk-core (~> 3, >= 3.224.1)
aws-sdk-s3 (1.213.0)
aws-sdk-core (~> 3, >= 3.241.4)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.11.0)
aws-sigv4 (1.12.1)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.3.0)
base64 (0.2.0)
benchmark (0.5.0)
bigdecimal (4.0.1)
claide (1.1.0)
colored (1.2)
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
csv (3.3.5)
declarative (0.0.20)
digest-crc (0.7.0)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
domain_name (0.6.20240107)
dotenv (2.8.1)
emoji_regex (3.2.3)
excon (0.109.0)
faraday (1.10.4)
excon (0.112.0)
faraday (1.10.5)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
@@ -54,14 +55,14 @@ GEM
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.7)
faraday-cookie_jar (0.0.8)
faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
http-cookie (>= 1.0.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.1)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.1.1)
faraday-multipart (1.2.0)
multipart-post (~> 2.0)
faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0)
@@ -71,15 +72,19 @@ GEM
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.4.0)
fastlane (2.228.0)
fastlane (2.232.1)
CFPropertyList (>= 2.3, < 4.0.0)
abbrev (~> 0.1.2)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
aws-sdk-s3 (~> 1.197)
babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
base64 (~> 0.2.0)
benchmark (>= 0.1.0)
bundler (>= 1.17.3, < 5.0.0)
colored (~> 1.2)
commander (~> 4.6)
csv (~> 3.3)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
@@ -91,16 +96,20 @@ GEM
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-env (>= 1.6.0, < 2.0.0)
google-cloud-env (>= 1.6.0, <= 2.1.1)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
http-cookie (~> 1.0.5)
json (< 3.0.0)
jwt (>= 2.1.0, < 3)
logger (>= 1.6, < 2.0)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (>= 2.0.0, < 3.0.0)
mutex_m (~> 0.3.0)
naturally (~> 2.2)
nkf (~> 0.2.0)
optparse (>= 0.1.1, < 1.0.0)
ostruct (>= 0.1.0)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.5)
@@ -116,38 +125,40 @@ GEM
fastlane-sirp (1.0.0)
sysrandom (~> 1.0)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.54.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-core (0.11.3)
google-apis-androidpublisher_v3 (0.96.0)
google-apis-core (>= 0.15.0, < 2.a)
google-apis-core (0.18.0)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
googleauth (~> 1.9)
httpclient (>= 2.8.3, < 3.a)
mini_mime (~> 1.0)
mutex_m
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
rexml
google-apis-iamcredentials_v1 (0.17.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-playcustomapp_v1 (0.13.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.29.0)
google-apis-core (>= 0.11.0, < 2.a)
google-cloud-core (1.6.1)
google-apis-iamcredentials_v1 (0.26.0)
google-apis-core (>= 0.15.0, < 2.a)
google-apis-playcustomapp_v1 (0.17.0)
google-apis-core (>= 0.15.0, < 2.a)
google-apis-storage_v1 (0.60.0)
google-apis-core (>= 0.15.0, < 2.a)
google-cloud-core (1.8.0)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.3.1)
google-cloud-storage (1.45.0)
google-cloud-env (2.1.1)
faraday (>= 1.0, < 3.a)
google-cloud-errors (1.5.0)
google-cloud-storage (1.58.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.29.0)
google-apis-core (>= 0.18, < 2)
google-apis-iamcredentials_v1 (~> 0.18)
google-apis-storage_v1 (>= 0.42)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
googleauth (~> 1.9)
mini_mime (~> 1.0)
googleauth (1.8.1)
faraday (>= 0.17.3, < 3.a)
googleauth (1.11.2)
faraday (>= 1.0, < 3.a)
google-cloud-env (~> 2.1)
jwt (>= 1.4, < 3.0)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
@@ -158,37 +169,38 @@ GEM
httpclient (2.9.0)
mutex_m
jmespath (1.6.2)
json (2.7.6)
jwt (2.10.1)
json (2.18.1)
jwt (2.10.2)
base64
logger (1.7.0)
mini_magick (4.13.2)
mini_mime (1.1.5)
multi_json (1.15.0)
multi_json (1.19.1)
multipart-post (2.4.1)
mutex_m (0.3.0)
nanaimo (0.4.0)
naturally (2.3.0)
nkf (0.2.0)
optparse (0.6.0)
optparse (0.8.1)
os (1.1.4)
ostruct (0.6.3)
plist (3.7.2)
public_suffix (5.1.1)
rake (13.3.0)
public_suffix (7.0.2)
rake (13.3.1)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.4.1)
retriable (3.2.0)
rexml (3.4.4)
rouge (3.28.0)
ruby2_keywords (0.0.5)
rubyzip (2.4.1)
security (0.1.5)
signet (0.18.0)
signet (0.21.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
jwt (>= 1.5, < 4.0)
multi_json (~> 1.10)
simctl (1.6.10)
CFPropertyList
@@ -203,7 +215,6 @@ GEM
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
uber (0.1.0)
unf (0.2.0)
unicode-display_width (2.6.0)
word_wrap (1.0.0)
xcodeproj (1.27.0)
+7 -5
View File
@@ -3,6 +3,7 @@
# Companion App for AirPods (CAPod)
[![GitHub release (latest SemVer including pre-releases)](https://img.shields.io/github/v/release/d4rken-org/capod?include_prereleases)](https://github.com/d4rken-org/capod/releases/latest)
[![RB Status](https://shields.rbtlog.dev/simple/eu.darken.capod)](https://shields.rbtlog.dev/eu.darken.capod)
[![Code tests & eval](https://github.com/d4rken-org/capod/actions/workflows/code-checks.yml/badge.svg)](https://github.com/d4rken/capod/actions/workflows/code-checks.yml)
[![Crowdin](https://badges.crowdin.net/capod/localized.svg)](https://crowdin.com/project/capod)
[![Github Downloads](https://img.shields.io/github/downloads/d4rken-org/capod/total.svg?label=GitHub%20Downloads&logo=github)](https://github.com/d4rken-org/capod/edit/main/README.md#download)
@@ -51,11 +52,12 @@ Currently supported models:
| Source | Status |
|------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [Google Play](https://play.google.com/store/apps/details?id=eu.darken.capod) | [![](https://img.shields.io/endpoint?color=green&logo=google-play&logoColor=green&url=https%3A%2F%2Fplay.cuzi.workers.dev%2Fplay%3Fi%3Deu.darken.capod%26l%3DGoogle%2520Play%26m%3D%24version)](https://play.google.com/store/apps/details?id=eu.darken.capod) |
| [Google Play Beta](https://play.google.com/apps/testing/eu.darken.capod) | [![](https://img.shields.io/badge/Google%20Play-Beta-yellowgreen?style=flat&logo=google-play)](https://play.google.com/apps/testing/eu.darken.capod) | |
| [Github Releases](https://github.com/d4rken-org/capod/releases) | [![GitHub release (latest SemVer including pre-releases)](https://img.shields.io/github/v/release/d4rken-org/capod?include_prereleases&label=GitHub)](https://github.com/d4rken-org/capod/releases/latest) |
| [F-Droid](https://f-droid.org/en/packages/eu.darken.capod/) | [![](https://img.shields.io/f-droid/v/eu.darken.capod)](https://f-droid.org/en/packages/eu.darken.capod/) |
| [F-Droid (IzzyOnDroid)](https://apt.izzysoft.de/packages/eu.darken.capod/) | [![](https://img.shields.io/endpoint?url=https://apt.izzysoft.de/fdroid/api/v1/shield/eu.darken.capod)](https://apt.izzysoft.de/packages/eu.darken.capod/) |
| [Google Play](https://play.google.com/store/apps/details?id=eu.darken.capod) | [![https://play.google.com/store/apps/details?id=eu.darken.capod](https://img.shields.io/endpoint?color=green&logo=google-play&logoColor=green&url=https%3A%2F%2Fplay.cuzi.workers.dev%2Fplay%3Fi%3Deu.darken.capod%26l%3DGoogle%2520Play%26m%3D%24version)](https://play.google.com/store/apps/details?id=eu.darken.capod) |
| [Google Play Beta](https://play.google.com/apps/testing/eu.darken.capod) | [![https://play.google.com/apps/testing/eu.darken.capod](https://img.shields.io/badge/Google%20Play-Beta-yellowgreen?style=flat&logo=google-play)](https://play.google.com/apps/testing/eu.darken.capod) | |
| [Github (Release)](https://github.com/d4rken-org/capod/releases) | ![https://github.com/d4rken-org/capod/releases](https://img.shields.io/github/v/release/d4rken-org/capod?display_name=release&logo=github&label=GitHub%20(Release)) ![](https://img.shields.io/github/downloads/d4rken-org/capod/latest/total?label=%20) |
| [Github (Pre-Release)](https://github.com/d4rken-org/capod/releases) | ![https://github.com/d4rken-org/capod/releases](https://img.shields.io/github/v/release/d4rken-org/capod?include_prereleases&display_name=release&logo=github&label=GitHub%20(Pre-Release)) ![](https://img.shields.io/github/downloads-pre/d4rken-org/capod/latest/total?label=%20) |
| [F-Droid](https://f-droid.org/en/packages/eu.darken.capod/) | [![https://f-droid.org/en/packages/eu.darken.capod/](https://img.shields.io/f-droid/v/eu.darken.capod)](https://f-droid.org/en/packages/eu.darken.capod/) |
| [F-Droid (IzzyOnDroid)](https://apt.izzysoft.de/packages/eu.darken.capod/) | [![https://apt.izzysoft.de/packages/eu.darken.capod/](https://img.shields.io/endpoint?url=https://apt.izzysoft.de/fdroid/api/v1/shield/eu.darken.capod)](https://apt.izzysoft.de/packages/eu.darken.capod/) |
## Support the project
+1 -1
View File
@@ -1 +1 @@
3.0.1-rc0 30001000
4.0.1-rc0 40001000
-1
View File
@@ -26,4 +26,3 @@ exclude:
- app
- app-common
- CONTRIBUTING.md
- CLAUDE.md
+54 -18
View File
@@ -3,11 +3,12 @@ plugins {
id("com.android.application")
id("kotlin-android")
id("com.google.devtools.ksp")
id("kotlin-kapt")
id("kotlin-parcelize")
id("org.jetbrains.kotlin.plugin.compose")
id("org.jetbrains.kotlin.plugin.serialization")
id("com.android.compose.screenshot") version "0.0.1-alpha13"
}
apply(plugin = "dagger.hilt.android.plugin")
apply(plugin = "androidx.navigation.safeargs.kotlin")
android {
compileSdk = projectConfig.compileSdk
@@ -35,7 +36,7 @@ android {
}
signingConfigs {
val basePath = File(System.getProperty("user.home"), ".appconfig/${projectConfig.packageName}")
val basePath = File(System.getProperty("user.home"), ".config/projects/${projectConfig.packageName}")
create("releaseFoss") {
setupCredentials(File(basePath, "signing-foss.properties"))
}
@@ -94,21 +95,12 @@ android {
}
}
buildOutputs.all {
val variantOutputImpl = this as com.android.build.gradle.internal.api.BaseVariantOutputImpl
val variantName: String = variantOutputImpl.name
if (listOf("release", "beta").any { variantName.lowercase().contains(it) }) {
val outputFileName = projectConfig.packageName +
"-v${defaultConfig.versionName}-${defaultConfig.versionCode}" +
"-${variantName.uppercase()}.apk"
variantOutputImpl.outputFileName = outputFileName
}
}
experimentalProperties["android.experimental.enableScreenshotTest"] = true
buildFeatures {
viewBinding = true
buildConfig = true
compose = true
}
compileOptions {
@@ -126,6 +118,7 @@ android {
"-opt-in=kotlin.time.ExperimentalTime",
"-opt-in=kotlin.RequiresOptIn",
"-opt-in=kotlin.ExperimentalUnsignedTypes",
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
"-Xannotation-default-target=param-property"
)
}
@@ -141,6 +134,42 @@ android {
}
}
androidComponents {
onVariants { variant ->
val buildType = variant.buildType ?: return@onVariants
if (buildType != "release" && buildType != "beta") return@onVariants
val formattedVariantName = variant.name
.replace(Regex("([a-z])([A-Z])"), "$1-$2")
.uppercase()
val apkFolder = variant.artifacts.get(com.android.build.api.artifact.SingleArtifact.APK)
val loader = variant.artifacts.getBuiltArtifactsLoader()
val packageName = projectConfig.packageName
val renameTask = tasks.register("rename${variant.name.replaceFirstChar { it.uppercase() }}Apk") {
inputs.files(apkFolder)
outputs.upToDateWhen { false }
doLast {
val builtArtifacts = loader.load(apkFolder.get()) ?: return@doLast
builtArtifacts.elements.forEach { element ->
val apkFile = File(element.outputFile)
val outputFileName = "$packageName-v${element.versionName}-${element.versionCode}-$formattedVariantName.apk"
if (apkFile.exists() && apkFile.name != outputFileName) {
apkFile.copyTo(File(apkFile.parentFile, outputFileName), overwrite = true)
}
}
}
}
tasks.matching { it.name == "assemble${variant.name.replaceFirstChar { it.uppercase() }}" }.configureEach {
finalizedBy(renameTask)
}
}
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
@@ -148,8 +177,6 @@ dependencies {
addDagger()
addMoshi()
addOkio()
addBaseAndroid()
@@ -158,10 +185,19 @@ dependencies {
implementation("androidx.core:core-splashscreen:1.0.0-alpha02")
addNavigation()
addBaseWorkManager()
addCompose()
addGlance()
addDataStore()
addNavigation3()
addSerialization()
addTesting()
"gplayImplementation"("com.android.billingclient:billing:8.0.0")
"gplayImplementation"("com.android.billingclient:billing-ktx:8.0.0")
"screenshotTestImplementation"(platform("androidx.compose:compose-bom:${Versions.Compose.bom}"))
"screenshotTestImplementation"("com.android.tools.screenshot:screenshot-validation-api:0.0.1-alpha13")
"screenshotTestImplementation"("androidx.compose.ui:ui-tooling")
}
-3
View File
@@ -1,3 +0,0 @@
-keepclassmembernames @com.squareup.moshi.JsonClass class * extends java.lang.Enum {
<fields>;
}
+7 -22
View File
@@ -1,23 +1,8 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
-keep class eu.darken.capod.BuildConfig { *; }
-dontobfuscate
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-dontobfuscate
# work-runtime 2.7.1 (pulled by Glance) uses Class.newInstance() reflection throughout.
# R8 full mode strips no-arg constructors not reachable by static analysis.
# Keep all constructors in the work package. Remove when Glance upgrades to work-runtime 2.10+.
# See: https://issuetracker.google.com/issues/243257364
-keep class androidx.work.** { <init>(...); }
@@ -0,0 +1,274 @@
package eu.darken.capod.screenshots
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.graphics.toArgb
import eu.darken.capod.R
import eu.darken.capod.common.compose.PreviewWrapper
import eu.darken.capod.main.ui.widget.ComposeWidgetPreview
import eu.darken.capod.main.ui.widget.WidgetRenderState
import eu.darken.capod.reaction.ui.popup.PopUpContent as PopUpCard
import eu.darken.capod.main.ui.widget.WidgetConfigurationScreen
import eu.darken.capod.main.ui.widget.WidgetConfigurationViewModel
import eu.darken.capod.main.ui.widget.WidgetTheme
import eu.darken.capod.common.compose.preview.MOCK_NOW
import eu.darken.capod.common.compose.preview.MockPodDataProvider
import eu.darken.capod.common.theming.CapodTheme
import eu.darken.capod.main.ui.overview.OverviewScreen
import eu.darken.capod.main.ui.overview.OverviewViewModel
import eu.darken.capod.main.ui.settings.SettingsScreen
import eu.darken.capod.main.ui.settings.SettingsViewModel
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.profiles.ui.DeviceManagerScreen
import eu.darken.capod.profiles.ui.DeviceManagerViewModel
import eu.darken.capod.profiles.ui.creation.DeviceProfileCreationScreen
import eu.darken.capod.profiles.ui.creation.DeviceProfileCreationViewModel
import eu.darken.capod.reaction.core.autoconnect.AutoConnectCondition
import eu.darken.capod.reaction.ui.ReactionSettingsScreen
import eu.darken.capod.reaction.ui.ReactionSettingsViewModel
internal const val DS = "spec:width=1080px,height=2400px,dpi=428"
@Composable
internal fun DashboardContent() = PreviewWrapper {
OverviewScreen(
state = OverviewViewModel.State(
now = MOCK_NOW,
permissions = emptySet(),
devices = listOf(
MockPodDataProvider.airPodsProMixed(),
MockPodDataProvider.airPodsMax(),
MockPodDataProvider.unknownDevice(),
),
isDebugMode = false,
isBluetoothEnabled = true,
profiles = listOf(
MockPodDataProvider.profile("My AirPods Pro", PodDevice.Model.AIRPODS_PRO2),
MockPodDataProvider.profile("AirPods Max", PodDevice.Model.AIRPODS_MAX),
),
upgradeInfo = MockPodDataProvider.gplayInfo(isPro = true),
showUnmatchedDevices = false,
),
onRequestPermission = {},
onManageDevices = {},
onSettings = {},
onUpgrade = {},
onToggleUnmatched = {},
)
}
@Composable
internal fun DeviceProfilesContent() = PreviewWrapper {
DeviceManagerScreen(
state = DeviceManagerViewModel.State(
profiles = listOf(
MockPodDataProvider.profile("Work AirPods", PodDevice.Model.AIRPODS_PRO2),
MockPodDataProvider.profile("AirPods Max", PodDevice.Model.AIRPODS_MAX),
MockPodDataProvider.profile("Gym Beats", PodDevice.Model.POWERBEATS_PRO),
),
),
onBack = {},
onAddDevice = {},
onEditProfile = {},
)
}
@Composable
internal fun AddProfileContent() = PreviewWrapper {
DeviceProfileCreationScreen(
state = DeviceProfileCreationViewModel.State(
isEditMode = false,
name = "",
nameError = null,
selectedModel = null,
availableModels = PodDevice.Model.entries.filter { it != PodDevice.Model.UNKNOWN },
identityKey = null,
encryptionKey = null,
selectedDevice = null,
bondedDevices = emptyList(),
minimumSignalQuality = 0.15f,
canSave = false,
),
onBack = {},
onSave = {},
onDelete = {},
onNameChange = {},
onModelChange = {},
onDeviceChange = {},
onIdentityKeyChange = {},
onEncryptionKeyChange = {},
onSignalQualityChange = {},
onKeyGuide = {},
)
}
@Composable
internal fun SettingsIndexContent() = PreviewWrapper {
SettingsScreen(
state = SettingsViewModel.State(isPro = true, sponsorUrl = "https://example.com"),
onNavigateUp = {},
onGeneralSettings = {},
onDeviceManager = {},
onReactions = {},
onSupport = {},
onWiki = {},
onChangelog = {},
onHelpTranslate = {},
onAcknowledgements = {},
onPrivacyPolicy = {},
onSponsor = {},
)
}
@Composable
internal fun ReactionSettingsContent() = PreviewWrapper {
ReactionSettingsScreen(
state = ReactionSettingsViewModel.State(
isPro = true,
onePodMode = false,
autoPlay = true,
autoPause = true,
autoConnect = false,
autoConnectCondition = AutoConnectCondition.WHEN_SEEN,
showPopUpOnCaseOpen = true,
showPopUpOnConnection = false,
),
onNavigateUp = {},
onOnePodModeChanged = {},
onAutoPlayChanged = {},
onAutoPauseChanged = {},
onAutoConnectChanged = {},
onAutoConnectConditionSelected = {},
onShowPopUpOnCaseOpenChanged = {},
onShowPopUpOnConnectionChanged = {},
)
}
@Composable
internal fun WidgetConfigurationContent() = PreviewWrapper {
val profiles = listOf(
MockPodDataProvider.profile("My AirPods Pro", PodDevice.Model.AIRPODS_PRO2),
MockPodDataProvider.profile("AirPods Max", PodDevice.Model.AIRPODS_MAX),
)
WidgetConfigurationScreen(
state = WidgetConfigurationViewModel.State(
profiles = profiles,
selectedProfile = profiles.first().id,
isPro = true,
theme = WidgetTheme.DEFAULT,
activePreset = WidgetTheme.Preset.MATERIAL_YOU,
isCustomMode = false,
),
onSelectProfile = {},
onSelectPreset = {},
onEnterCustomMode = { _, _ -> },
onSetBackgroundColor = {},
onSetForegroundColor = {},
onSetBackgroundAlpha = {},
onSetShowDeviceLabel = {},
onReset = {},
onConfirm = {},
onCancel = {},
)
}
@Composable
internal fun CasePopUpContent() {
CapodTheme {
Box(
modifier = Modifier
.fillMaxSize()
.background(
Brush.verticalGradient(
colors = listOf(
Color(0xFF1A237E),
Color(0xFF0D47A1),
Color(0xFF006064),
)
)
),
contentAlignment = Alignment.Center,
) {
PopUpCard(
device = MockPodDataProvider.airPodsProMixed(),
onClose = {},
modifier = Modifier.padding(horizontal = 32.dp),
)
}
}
}
@Composable
internal fun HomescreenWidgetContent() {
CapodTheme {
Box(
modifier = Modifier
.fillMaxSize()
.background(
Brush.verticalGradient(
colors = listOf(
Color(0xFF1A237E),
Color(0xFF0D47A1),
Color(0xFF006064),
)
)
),
contentAlignment = Alignment.Center,
) {
ComposeWidgetPreview(
state = WidgetRenderState.previewDualPod(
bgColor = MaterialTheme.colorScheme.surface.toArgb(),
textColor = MaterialTheme.colorScheme.onSurface.toArgb(),
iconColor = MaterialTheme.colorScheme.onSurface.toArgb(),
),
)
}
}
}
@Preview(name = "1 - Dashboard Light", locale = "en", device = DS, showSystemUi = true)
@Composable
private fun PreviewDashboardLight() = DashboardContent()
@Preview(name = "2 - Dashboard Dark", locale = "en", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES, showSystemUi = true)
@Composable
private fun PreviewDashboardDark() = DashboardContent()
@Preview(name = "3 - Case Pop-up", device = DS, showSystemUi = true)
@Composable
private fun PreviewCasePopUp() = CasePopUpContent()
@Preview(name = "4 - Widget Configuration", locale = "en", device = DS, showSystemUi = true)
@Composable
private fun PreviewWidgetConfiguration() = WidgetConfigurationContent()
@Preview(name = "5 - Device Profiles", locale = "en", device = DS, showSystemUi = true)
@Composable
private fun PreviewDeviceProfiles() = DeviceProfilesContent()
@Preview(name = "6 - Add Profile", locale = "en", device = DS, showSystemUi = true)
@Composable
private fun PreviewAddProfile() = AddProfileContent()
@Preview(name = "7 - Settings", locale = "en", device = DS, showSystemUi = true)
@Composable
private fun PreviewSettingsIndex() = SettingsIndexContent()
@Preview(name = "8 - Reaction Settings", locale = "en", device = DS, showSystemUi = true)
@Composable
private fun PreviewReactionSettings() = ReactionSettingsContent()
@Preview(name = "9 - Homescreen Widget", device = DS, showSystemUi = true)
@Composable
private fun PreviewHomescreenWidget() = HomescreenWidgetContent()
@@ -1,24 +1,35 @@
package eu.darken.capod.common.upgrade.core
import android.content.Context
import com.squareup.moshi.Moshi
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.SharedPreferencesMigration
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore
import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.common.preferences.createFlowPreference
import eu.darken.capod.common.datastore.createValue
import eu.darken.capod.common.serialization.SerializationCapod
import kotlinx.serialization.json.Json
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class FossCache @Inject constructor(
@ApplicationContext context: Context,
moshi: Moshi
@SerializationCapod json: Json,
) {
private val preferences = context.getSharedPreferences("settings_foss", Context.MODE_PRIVATE)
val upgrade = preferences.createFlowPreference<FossUpgrade?>(
key = "foss.upgrade",
moshi = moshi,
defaultValue = null,
private val Context.dataStore by preferencesDataStore(
name = "settings_foss",
produceMigrations = { ctx -> listOf(SharedPreferencesMigration(ctx, "settings_foss")) }
)
}
private val dataStore: DataStore<Preferences> = context.dataStore
val upgrade = dataStore.createValue<FossUpgrade?>(
key = "foss.upgrade",
defaultValue = null,
json = json,
onErrorFallbackToDefault = true,
)
}
@@ -1,18 +1,19 @@
package eu.darken.capod.common.upgrade.core
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import eu.darken.capod.common.serialization.InstantEpochMillisSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.time.Instant
@JsonClass(generateAdapter = true)
@Serializable
data class FossUpgrade(
val upgradedAt: Instant,
val reason: Reason
@SerialName("upgradedAt") @Serializable(with = InstantEpochMillisSerializer::class) val upgradedAt: Instant,
@SerialName("reason") val reason: Reason
) {
@JsonClass(generateAdapter = false)
@Serializable
enum class Reason {
@Json(name = "foss.upgrade.reason.donated") DONATED,
@Json(name = "foss.upgrade.reason.alreadydonated") ALREADY_DONATED,
@Json(name = "foss.upgrade.reason.nomoney") NO_MONEY;
@SerialName("foss.upgrade.reason.donated") DONATED,
@SerialName("foss.upgrade.reason.alreadydonated") ALREADY_DONATED,
@SerialName("foss.upgrade.reason.nomoney") NO_MONEY;
}
}
@@ -1,21 +1,16 @@
package eu.darken.capod.common.upgrade.core
import android.app.Activity
import android.widget.Toast
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import eu.darken.capod.R
import eu.darken.capod.common.WebpageTool
import eu.darken.capod.common.upgrade.UpgradeRepo
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import java.time.Instant
import javax.inject.Inject
import javax.inject.Singleton
import eu.darken.capod.common.datastore.valueBlocking
@Singleton
class UpgradeControlFoss @Inject constructor(
private val fossCache: FossCache,
private val webpageTool: WebpageTool,
) : UpgradeRepo {
override val upgradeInfo: Flow<UpgradeRepo.Info> = fossCache.upgrade.flow.map { data ->
@@ -30,34 +25,11 @@ class UpgradeControlFoss @Inject constructor(
}
}
override fun launchBillingFlow(activity: Activity) {
MaterialAlertDialogBuilder(activity).apply {
setIcon(R.drawable.ic_heart)
setTitle(R.string.upgrade_capod_label)
setMessage(R.string.upgrade_capod_description)
setPositiveButton(R.string.foss_upgrade_donate_label) { _, _ ->
fossCache.upgrade.value = FossUpgrade(
upgradedAt = Instant.now(),
reason = FossUpgrade.Reason.DONATED
)
webpageTool.open("https://github.com/sponsors/d4rken")
Toast.makeText(activity, R.string.general_thank_you_label, Toast.LENGTH_SHORT).show()
}
setNegativeButton(R.string.foss_upgrade_alreadydonated_label) { _, _ ->
fossCache.upgrade.value = FossUpgrade(
upgradedAt = Instant.now(),
reason = FossUpgrade.Reason.ALREADY_DONATED
)
Toast.makeText(activity, R.string.general_thank_you_label, Toast.LENGTH_SHORT).show()
}
setNeutralButton(R.string.foss_upgrade_no_money_label) { _, _ ->
fossCache.upgrade.value = FossUpgrade(
upgradedAt = Instant.now(),
reason = FossUpgrade.Reason.NO_MONEY
)
Toast.makeText(activity, "¯\\_(ツ)_/¯", Toast.LENGTH_SHORT).show()
}
}.show()
fun upgrade(reason: FossUpgrade.Reason) {
fossCache.upgrade.valueBlocking = FossUpgrade(
upgradedAt = Instant.now(),
reason = reason
)
}
data class Info(
@@ -0,0 +1,26 @@
package eu.darken.capod.upgrade.ui
import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavKey
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.IntoSet
import eu.darken.capod.common.navigation.Nav
import eu.darken.capod.common.navigation.NavigationEntry
import javax.inject.Inject
class UpgradeNavigation @Inject constructor() : NavigationEntry {
override fun EntryProviderScope<NavKey>.setup() {
entry<Nav.Main.Upgrade> { UpgradeScreenHost() }
}
@Module
@InstallIn(SingletonComponent::class)
abstract class Mod {
@Binds
@IntoSet
abstract fun bind(entry: UpgradeNavigation): NavigationEntry
}
}
@@ -0,0 +1,263 @@
package eu.darken.capod.upgrade.ui
import androidx.compose.foundation.Image
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.WindowInsets
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.systemBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.rememberScrollState
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.twotone.ArrowBack
import androidx.compose.material.icons.automirrored.twotone.Message
import androidx.compose.material.icons.twotone.BluetoothConnected
import androidx.compose.material.icons.twotone.Favorite
import androidx.compose.material.icons.twotone.Palette
import androidx.compose.material.icons.twotone.PlayCircle
import androidx.compose.material.icons.twotone.Widgets
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import eu.darken.capod.R
import eu.darken.capod.common.compose.Preview2
import eu.darken.capod.common.compose.PreviewWrapper
import eu.darken.capod.common.error.ErrorEventHandler
import eu.darken.capod.common.navigation.NavigationEventHandler
@Composable
fun UpgradeScreenHost(vm: UpgradeViewModel = hiltViewModel()) {
ErrorEventHandler(vm)
NavigationEventHandler(vm)
val snackbarHostState = remember { SnackbarHostState() }
val returnedEarlyMessage = stringResource(R.string.upgrade_foss_sponsor_returned_early)
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) {
vm.onResume()
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
}
LaunchedEffect(Unit) {
vm.sponsorEvents.collect { event ->
when (event) {
UpgradeViewModel.SponsorEvent.ReturnedTooEarly -> {
snackbarHostState.showSnackbar(returnedEarlyMessage)
}
}
}
}
UpgradeScreen(
snackbarHostState = snackbarHostState,
onNavigateUp = { vm.navUp() },
onSponsor = { vm.sponsor() },
)
}
private data class Benefit(val icon: ImageVector, val textRes: Int)
@Composable
fun UpgradeScreen(
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
onNavigateUp: () -> Unit,
onSponsor: () -> Unit,
) {
val benefits = listOf(
Benefit(Icons.TwoTone.Palette, R.string.upgrade_benefit_themes),
Benefit(Icons.TwoTone.PlayCircle, R.string.upgrade_benefit_autoplay),
Benefit(Icons.TwoTone.BluetoothConnected, R.string.upgrade_benefit_autoconnect),
Benefit(Icons.AutoMirrored.TwoTone.Message, R.string.upgrade_benefit_popups),
Benefit(Icons.TwoTone.Widgets, R.string.upgrade_benefit_widgets),
Benefit(Icons.TwoTone.Favorite, R.string.upgrade_benefit_support),
)
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) },
containerColor = MaterialTheme.colorScheme.surface,
) { paddingValues ->
Box(modifier = Modifier
.windowInsetsPadding(WindowInsets.systemBars)
.padding(paddingValues)
) {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Spacer(modifier = Modifier.height(16.dp))
Box(contentAlignment = Alignment.Center) {
Surface(
modifier = Modifier.size(120.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.5f),
) {}
Image(
painter = painterResource(R.drawable.splash_graphic2),
contentDescription = null,
modifier = Modifier.size(80.dp),
)
}
Spacer(modifier = Modifier.height(16.dp))
Text(
text = buildAnnotatedString {
append("CAPod ")
withStyle(SpanStyle(color = colorResource(R.color.brand_secondary), fontWeight = FontWeight.Bold)) {
append("FOSS")
}
},
style = MaterialTheme.typography.headlineLarge,
color = MaterialTheme.colorScheme.onSurface,
)
Spacer(modifier = Modifier.height(24.dp))
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer,
),
modifier = Modifier.fillMaxWidth(),
) {
Text(
text = stringResource(R.string.upgrade_foss_preamble),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(16.dp),
)
}
Spacer(modifier = Modifier.height(16.dp))
Card(
modifier = Modifier.fillMaxWidth(),
) {
Column(modifier = Modifier.padding(vertical = 4.dp)) {
benefits.forEach { benefit ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Surface(
shape = RoundedCornerShape(8.dp),
color = MaterialTheme.colorScheme.secondaryContainer,
modifier = Modifier.size(28.dp),
) {
Box(contentAlignment = Alignment.Center) {
Icon(
imageVector = benefit.icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSecondaryContainer,
modifier = Modifier.size(16.dp),
)
}
}
Spacer(modifier = Modifier.width(12.dp))
Text(
text = stringResource(benefit.textRes),
style = MaterialTheme.typography.bodyLarge,
)
}
}
}
}
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = onSponsor,
modifier = Modifier
.fillMaxWidth()
.height(52.dp),
shape = RoundedCornerShape(12.dp),
) {
Icon(
imageVector = Icons.TwoTone.Favorite,
contentDescription = null,
modifier = Modifier.size(20.dp),
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(R.string.upgrade_foss_sponsor_action),
style = MaterialTheme.typography.titleMedium,
)
}
Text(
text = stringResource(R.string.upgrade_foss_sponsor_subtitle),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 8.dp),
)
Spacer(modifier = Modifier.height(24.dp))
}
IconButton(
onClick = onNavigateUp,
modifier = Modifier.padding(4.dp),
) {
Icon(
imageVector = Icons.AutoMirrored.TwoTone.ArrowBack,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurface,
)
}
}
}
}
@Preview2
@Composable
private fun UpgradeScreenPreview() = PreviewWrapper {
UpgradeScreen(
onNavigateUp = {},
onSponsor = {},
)
}
@@ -0,0 +1,48 @@
package eu.darken.capod.upgrade.ui
import android.os.SystemClock
import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.lifecycle.HiltViewModel
import eu.darken.capod.common.WebpageTool
import eu.darken.capod.common.coroutine.DispatcherProvider
import eu.darken.capod.common.flow.SingleEventFlow
import eu.darken.capod.common.uix.ViewModel4
import eu.darken.capod.common.upgrade.core.FossUpgrade
import eu.darken.capod.common.upgrade.core.UpgradeControlFoss
import javax.inject.Inject
@HiltViewModel
class UpgradeViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
dispatcherProvider: DispatcherProvider,
private val upgradeControlFoss: UpgradeControlFoss,
private val webpageTool: WebpageTool,
) : ViewModel4(dispatcherProvider) {
sealed interface SponsorEvent {
data object ReturnedTooEarly : SponsorEvent
}
val sponsorEvents = SingleEventFlow<SponsorEvent>()
fun sponsor() {
savedStateHandle[KEY_SPONSOR_OPENED_AT] = SystemClock.elapsedRealtime()
webpageTool.open("https://github.com/sponsors/d4rken")
}
fun onResume() {
val openedAt = savedStateHandle.get<Long>(KEY_SPONSOR_OPENED_AT) ?: return
savedStateHandle.remove<Long>(KEY_SPONSOR_OPENED_AT)
if (SystemClock.elapsedRealtime() - openedAt >= 5_000L) {
upgradeControlFoss.upgrade(FossUpgrade.Reason.DONATED)
navUp()
} else {
sponsorEvents.tryEmit(SponsorEvent.ReturnedTooEarly)
}
}
companion object {
private const val KEY_SPONSOR_OPENED_AT = "sponsor_opened_at"
}
}
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Skenk</string>
<string name="foss_upgrade_alreadydonated_label">Ek het reeds geskenk</string>
<string name="foss_upgrade_no_money_label">Ek spandeer al my geld op AirPods</string>
<string name="upgrade_foss_preamble">CAPod FOSS is gratis en oopbron. As u dit nuttig vind, oorweeg dit om ontwikkeling te befonds om die projek aan die gang te hou.</string>
<string name="upgrade_foss_sponsor_action">Borg ontwikkeling</string>
<string name="upgrade_foss_sponsor_subtitle">Geen advertensies. Geen opsporing. Geen Google Play-binding.</string>
<string name="upgrade_foss_sponsor_returned_early">Alreeds terug? U ondersteuning hou CAPod aan die lewe.</string>
</resources>
+4
View File
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">ልገሳ</string>
<string name="foss_upgrade_alreadydonated_label">አስቀድሜ ልገሳ ፈጽሜአለሁ</string>
<string name="foss_upgrade_no_money_label">ገንዘቤን በሙሉ በAirPods ላይ አውጥቻለሁ</string>
<string name="upgrade_foss_preamble">CAPod FOSS ነፃ እና ክፍት ምንጭ ነው። ግኾሚ ሆኖ ካግኝት፣ ፕሮጀክቱ ቀጥሎ እንደይይድ ለመርዳት ላማቱን ማስደገፏ ያስቡ።</string>
<string name="upgrade_foss_sponsor_action">ልማትን ይደግፉ</string>
<string name="upgrade_foss_sponsor_subtitle">ምንም ማስታወቂያ የሉም። ምንም ክትትል የሉም። ምንም Google Play ቆለፋ የሉም።</string>
<string name="upgrade_foss_sponsor_returned_early">ቀድሞ ተመለሱ፡ ድጋፏዎ CAPodን ህያው ያቆይላል።</string>
</resources>
+4
View File
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">تبرُّع</string>
<string name="foss_upgrade_alreadydonated_label">أنا متبرع بالفعل</string>
<string name="foss_upgrade_no_money_label">أُنفق كل أموالي على AirPods</string>
<string name="upgrade_foss_preamble">CAPod FOSS مجاني ومفتوح المصدر. إذا وجدته مفيداً، فكر في دعم التطوير للمساعدة في استمرار المشروع.</string>
<string name="upgrade_foss_sponsor_action">ادعم التطوير</string>
<string name="upgrade_foss_sponsor_subtitle">لا إعلانات. لا تتبع. لا قيود على Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">عدت بسرعة؟ دعمك يبقي CAPod حياً.</string>
</resources>
+4
View File
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">İanə ver</string>
<string name="foss_upgrade_alreadydonated_label">Artıq ianə vermişəm</string>
<string name="foss_upgrade_no_money_label">Bütün pulumu AirPods-lara xərcləyirəm</string>
<string name="upgrade_foss_preamble">CAPod FOSS pulsuzdur və açıq mənbəlidir. Faydalı tapırsınızsa, layihənin davamına kömək etmək üçün inkişafı maliyyələşdirməyi düşünün.</string>
<string name="upgrade_foss_sponsor_action">İnkişafı sponsor edin</string>
<string name="upgrade_foss_sponsor_subtitle">Reklam yoxdur. İzləmə yoxdur. Google Play bağlılığı yoxdur.</string>
<string name="upgrade_foss_sponsor_returned_early">Artıq geri qayıtdınız? Dəstəyiniz CAPod-u yaşadır.</string>
</resources>
+4
View File
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Ахвяраваць</string>
<string name="foss_upgrade_alreadydonated_label">Я ўжо ахвяраваў</string>
<string name="foss_upgrade_no_money_label">Я патраціў усе грошы на AirPods</string>
<string name="upgrade_foss_preamble">CAPod FOSS бясплатны і з адкрытым зыходным кодам. Калі вы лічыце яго карысным, разгледзьце магчымасць спансіравання распрацоўкі, каб дапамагчы трымаць праект ў жывых.</string>
<string name="upgrade_foss_sponsor_action">Падтрымаць распрацоўку</string>
<string name="upgrade_foss_sponsor_subtitle">Ніякай рэкламы. Ніякай адсочкі. Ніякай залежнасці ад Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Ужо вяртаецеся? Ваша падтрымка трымае CAPod ў жывых.</string>
</resources>
+4
View File
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Дарение</string>
<string name="foss_upgrade_alreadydonated_label">Вече съм дарил</string>
<string name="foss_upgrade_no_money_label">Харча всичките си пари за AirPods</string>
<string name="upgrade_foss_preamble">CAPod FOSS е безплатен и с отворен код. Ако ви е полезен, размислете да подкрепите разработката, за да помогнете проектът да продължи.</string>
<string name="upgrade_foss_sponsor_action">Спонсорирайте разработката</string>
<string name="upgrade_foss_sponsor_subtitle">Без реклами. Без следене. Без заключване в Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Вече ли? Вашата подкрепа поддържа CAPod жив.</string>
</resources>
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">দান করুন</string>
<string name="foss_upgrade_alreadydonated_label">আমি ইতিমধ্যে দান করেছি</string>
<string name="foss_upgrade_no_money_label">আমি আমার সব টাকা AirPods এর জন্য খরচ করি</string>
<string name="upgrade_foss_preamble">CAPod FOSS বিনামূল্যে এবং মুক্ত উৎস। আপনি যদি এটি উপযোগী মনে করেন, প্রকল্পটি চালু রাখতে সাহায্য করতে ডেভেলপমেন্ট স্পনসর করার কথা বিবেচনা করুন।</string>
<string name="upgrade_foss_sponsor_action">স্পনসর ডেভলপমেন্ট</string>
<string name="upgrade_foss_sponsor_subtitle">কোনো বিজ্ঞাপন নেই। কোনো ট্র্যাকিং নেই। Google Play-এর সাথে বাঁধানো নেই।</string>
<string name="upgrade_foss_sponsor_returned_early">ইতোমধ্যে ফিরে গেলেন? আপনার সমর্থন CAPodকে বাঁচিয়ে রাখে।</string>
</resources>
+4
View File
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Donacions</string>
<string name="foss_upgrade_alreadydonated_label">Ja he donat</string>
<string name="foss_upgrade_no_money_label">Gasto tots els meus diners en AirPods</string>
<string name="upgrade_foss_preamble">El CAPod FOSS és gratuït i de codi obert. Si el trobeu útil, considereu patrocinar el desenvolupament per ajudar a mantenir el projecte en marxa.</string>
<string name="upgrade_foss_sponsor_action">Patrocina el desenvolupament</string>
<string name="upgrade_foss_sponsor_subtitle">Sense anuncis. Sense seguiment. Sense dependència del Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Ja heu tornat? El vostre suport manté viu el CAPod.</string>
</resources>
+4
View File
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Přispět</string>
<string name="foss_upgrade_alreadydonated_label">Již jsem přispěl/a</string>
<string name="foss_upgrade_no_money_label">Všechny peníze jsem utratil/a za AirPods</string>
<string name="upgrade_foss_preamble">Aplikace CAPod FOSS je zdarma a open source. Pokud vám přijde užitečná, zvažte sponzorování vývoje, abyste pomohli udržet projekt v chodu.</string>
<string name="upgrade_foss_sponsor_action">Sponzorovat vývoj</string>
<string name="upgrade_foss_sponsor_subtitle">Žádné reklamy. Žádné sledování. Žádná závislost na Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Už jste zpět? Vaše podpora udržuje CAPod naživu.</string>
</resources>
+4
View File
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Doner</string>
<string name="foss_upgrade_alreadydonated_label">Jeg har allerede doneret</string>
<string name="foss_upgrade_no_money_label">Jeg bruger alle mine penge på AirPods</string>
<string name="upgrade_foss_preamble">CAPod FOSS er gratis og open source. Hvis du finder det nüttigt, kan du overveje at sponsorere udviklingen for at hjælpe projektet med at fortsætte.</string>
<string name="upgrade_foss_sponsor_action">Sponsorudvikling</string>
<string name="upgrade_foss_sponsor_subtitle">Ingen annoncer. Ingen sporing. Ingen Google Play-lås.</string>
<string name="upgrade_foss_sponsor_returned_early">Allerede tilbage? Din støtte holder CAPod i live.</string>
</resources>
+4
View File
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Spenden</string>
<string name="foss_upgrade_alreadydonated_label">Ich habe schon gespendet</string>
<string name="foss_upgrade_no_money_label">Hab alles für AirPods ausgegeben</string>
<string name="upgrade_foss_preamble">CAPod FOSS ist kostenlos und Open Source. Wenn du es nützlich findest, erwäge, die Entwicklung zu sponsern, um das Projekt am Laufen zu halten.</string>
<string name="upgrade_foss_sponsor_action">Fördere Entwicklung</string>
<string name="upgrade_foss_sponsor_subtitle">Keine Werbung. Kein Tracking. Keine Google-Play-Bindung.</string>
<string name="upgrade_foss_sponsor_returned_early">Schon zurück? Deine Unterstützung hält CAPod am Leben.</string>
</resources>
+4
View File
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Δωρεά</string>
<string name="foss_upgrade_alreadydonated_label">Ήδη δώρισα</string>
<string name="foss_upgrade_no_money_label">Ξόδεψα όλα τα χρήματά μου στα AirPods</string>
<string name="upgrade_foss_preamble">Το CAPod FOSS είναι δωρεάν και ανοιχτού κώδικα. Αν το βρίσκετε χρήσιμο, εξετάστε το ενδεχόμενο να χρηματοδοτήσετε την ανάπτυξη για να συνεχίσει το έργο.</string>
<string name="upgrade_foss_sponsor_action">Χορηγήστε την ανάπτυξη</string>
<string name="upgrade_foss_sponsor_subtitle">Χωρίς διαφημίσεις. Χωρίς παρακολούθηση. Χωρίς δέσμευση στο Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Επιστρέψατε ήδη; Η υποστήριξή σας κρατά το CAPod ζωντανό.</string>
</resources>
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Donar</string>
<string name="foss_upgrade_alreadydonated_label">Ya doné</string>
<string name="foss_upgrade_no_money_label">Gasto todo mi dinero en AirPods</string>
<string name="upgrade_foss_preamble">CAPod FOSS es gratuito y de código abierto. Si lo encontrás útil, considerá patrocinar el desarrollo para ayudar a mantener el proyecto en marcha.</string>
<string name="upgrade_foss_sponsor_action">Sponsorear el desarrollo</string>
<string name="upgrade_foss_sponsor_subtitle">Sin publicidad. Sin rastreo. Sin dependencia de Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">¿Ya te vas? Tu apoyo mantiene CAPod vivo.</string>
</resources>
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Donar</string>
<string name="foss_upgrade_alreadydonated_label">Ya doné</string>
<string name="foss_upgrade_no_money_label">Gasto todo mi dinero en AirPods</string>
<string name="upgrade_foss_preamble">CAPod FOSS es gratuito y de código abierto. Si lo encuentras útil, considera patrocinar el desarrollo para ayudar a mantener el proyecto.</string>
<string name="upgrade_foss_sponsor_action">Patrocinar el desarrollo</string>
<string name="upgrade_foss_sponsor_subtitle">Sin anuncios. Sin rastreo. Sin dependencia de Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">¿Ya de vuelta? Tu apoyo mantiene CAPod con vida.</string>
</resources>
+4
View File
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Donar</string>
<string name="foss_upgrade_alreadydonated_label">Ya he donado</string>
<string name="foss_upgrade_no_money_label">Gasto todo mi dinero en AirPods</string>
<string name="upgrade_foss_preamble">CAPod FOSS es gratuito y de código abierto. Si lo encuentras útil, considera patrocinar el desarrollo para ayudar a mantener el proyecto en marcha.</string>
<string name="upgrade_foss_sponsor_action">Patrocinadores</string>
<string name="upgrade_foss_sponsor_subtitle">Sin anuncios. Sin rastreo. Sin dependencia de Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">¿Ya de vuelta? Tu apoyo mantiene CAPod vivo.</string>
</resources>
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Anneta</string>
<string name="foss_upgrade_alreadydonated_label">Juba annetasin</string>
<string name="foss_upgrade_no_money_label">Kulutasin kogu raha AirPodsidele</string>
<string name="upgrade_foss_preamble">CAPod FOSS on tasuta ja avatud lähtekoodiga. Kui sellest on sulle kasu, kaalu arenduse rahastamist, et aidata projekti arendada.</string>
<string name="upgrade_foss_sponsor_action">Rahasta arendamist</string>
<string name="upgrade_foss_sponsor_subtitle">Reklaami pole. Jälgimist pole. Google Play lukustust pole.</string>
<string name="upgrade_foss_sponsor_returned_early">Juba tagasi? Sinu toetus hoiab CAPodi elus.</string>
</resources>
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Eman dohaintza</string>
<string name="foss_upgrade_alreadydonated_label">Dagoeneko eman dut dohaintza</string>
<string name="foss_upgrade_no_money_label">Nire diru guztia AirPods-etan gastatzen dut</string>
<string name="upgrade_foss_preamble">CAPod FOSS doakoa eta kode irekikoa da. Erabilgarria iruditzen bazaizu, kontuan hartu garapena babestu proiektua aurrera jarraitzeko.</string>
<string name="upgrade_foss_sponsor_action">Garapena babestu</string>
<string name="upgrade_foss_sponsor_subtitle">Iragarkirik ez. Jarraipenik ez. Google Play-ren lotura ez.</string>
<string name="upgrade_foss_sponsor_returned_early">Dagoeneko itzuli? Zure laguntzak CAPod bizirik mantentzen du.</string>
</resources>
+4
View File
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">کمک مالی</string>
<string name="foss_upgrade_alreadydonated_label">کمک مالی کرده‌ام</string>
<string name="foss_upgrade_no_money_label">من تمام پولم را خرج ایرپاد می‌کنم</string>
<string name="upgrade_foss_preamble">CAPod FOSS رایگان و متن-باز است. اگر آن را مفید یافتید، حمایت مالی از توسعه را در نظر بگیرید تا پروژه ادامه یابد.</string>
<string name="upgrade_foss_sponsor_action">حامی توسعه دهنده</string>
<string name="upgrade_foss_sponsor_subtitle">بدون تبلیغ. بدون ردیابی. بدون وابستگی به Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">زود برگشتیدی؟ حمایت شما CAPod را زنده نگه می‌دارد.</string>
</resources>
+4
View File
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Lahjoita</string>
<string name="foss_upgrade_alreadydonated_label">Olen jo lahjoittanut</string>
<string name="foss_upgrade_no_money_label">Käytän kaikki rahani AirPodeihin</string>
<string name="upgrade_foss_preamble">CAPod FOSS on ilmainen ja avoimen lähdekoodin ohjelma. Jos löydät sen hyödylliseksi, harkitse kehityksen tukemista projektin elässä pitämiseksi.</string>
<string name="upgrade_foss_sponsor_action">Sponsoroi kehitystä</string>
<string name="upgrade_foss_sponsor_subtitle">Ei mainoksia. Ei seurantaa. Ei Google Play -riippuvuutta.</string>
<string name="upgrade_foss_sponsor_returned_early">Jo takaisin? Tukesi pitää CAPod-sovelluksen hengissä.</string>
</resources>
+4
View File
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Mag-donate</string>
<string name="foss_upgrade_alreadydonated_label">Nakapag-donate na ako</string>
<string name="foss_upgrade_no_money_label">Ginagastos ko lahat ng pera ko sa AirPods</string>
<string name="upgrade_foss_preamble">Ang CAPod FOSS ay libre at open source. Kung nakita mong kapaki-pakinabang ito, isaalang-alang ang pag-sponsor ng development para mapanatiling aktibo ang proyekto.</string>
<string name="upgrade_foss_sponsor_action">Mag-sponsor ng development</string>
<string name="upgrade_foss_sponsor_subtitle">Walang ads. Walang tracking. Walang Google Play lock-in.</string>
<string name="upgrade_foss_sponsor_returned_early">Bumalik na? Ang iyong suporta ay nagpapanatiling buhay ng CAPod.</string>
</resources>
+5 -1
View File
@@ -2,5 +2,9 @@
<resources>
<string name="foss_upgrade_donate_label">Faire un don</string>
<string name="foss_upgrade_alreadydonated_label">Jai déjà fait un don</string>
<string name="foss_upgrade_no_money_label">Jai dépensé tout mon argent sur les AirPods</string>
<string name="foss_upgrade_no_money_label">Jai dépensé tout mon argent sur des AirPods</string>
<string name="upgrade_foss_preamble">CAPod FOSS est gratuite et à code source ouvert. Si vous la trouvez utile, pensez à soutenir le développement pour contribuer à la pérennité du projet.</string>
<string name="upgrade_foss_sponsor_action">Soutenir le développement</string>
<string name="upgrade_foss_sponsor_subtitle">Pas de publicités. Pas de suivi à la trace. Pas de dépendance à Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Déjà de retour? Votre soutien maintient CAPod en vie.</string>
</resources>
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Doar</string>
<string name="foss_upgrade_alreadydonated_label">Xa doei</string>
<string name="foss_upgrade_no_money_label">Gasto todos os meus cartos en AirPods</string>
<string name="upgrade_foss_preamble">CAPod FOSS é gratuito e de código aberto. Se o atopas útil, considera patrocinar o desenvolvemento para axudar a manter o proxecto en marcha.</string>
<string name="upgrade_foss_sponsor_action">Patrocinar desenvolvemento</string>
<string name="upgrade_foss_sponsor_subtitle">Sen anuncios. Sen rastrexo. Sen dependencia de Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">És rápido/a! O teu apoio mantén CAPod vivo.</string>
</resources>
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">दान करें</string>
<string name="foss_upgrade_alreadydonated_label">मैंने पहले ही दान कर दिया है</string>
<string name="foss_upgrade_no_money_label">मैं अपना सारा पैसा AirPods पर खर्च करता हूं</string>
<string name="upgrade_foss_preamble">CAPod FOSS मुफ्त और ओपन सोर्स है। यदि आपको यह उपयोगी लगता है, तो प्रोजेक्ट को जारी रखने में मदद के लिए विकास को प्रायोजित करने पर विचार करें।</string>
<string name="upgrade_foss_sponsor_action">विकास को प्रायोजक करें</string>
<string name="upgrade_foss_sponsor_subtitle">कोई विज्ञापन नहीं। कोई ट्रैकिंग नहीं। Google Play पर निर्भरता नहीं।</string>
<string name="upgrade_foss_sponsor_returned_early">अभी वापस? आपका समर्थन CAPod को जीवित रखता है।</string>
</resources>
+4
View File
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Donirajte</string>
<string name="foss_upgrade_alreadydonated_label">Već sam donirao</string>
<string name="foss_upgrade_no_money_label">Sav svoj novac trošim na AirPods</string>
<string name="upgrade_foss_preamble">CAPod FOSS je besplatan i otvorenog koda. Ako vam je koristan, razmislite o sponzoriranju razvoja kako bi projekt ostao živ.</string>
<string name="upgrade_foss_sponsor_action">Podržite razvoj</string>
<string name="upgrade_foss_sponsor_subtitle">Bez oglasa. Bez praćenja. Bez zaključanosti na Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Već se vraćate? Vaša podrška čuva CAPod na životu.</string>
</resources>
+4
View File
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Adományoz</string>
<string name="foss_upgrade_alreadydonated_label">Már adományoztam</string>
<string name="foss_upgrade_no_money_label">Minden pénzemet az AirPods-ra költöttem</string>
<string name="upgrade_foss_preamble">A CAPod FOSS ingyenes és nyílt forráskódú. Ha hasznosnak találod, fontold meg a fejlesztés támogatását, hogy a projekt tovább élhessen.</string>
<string name="upgrade_foss_sponsor_action">Szponzori fejlesztés</string>
<string name="upgrade_foss_sponsor_subtitle">Nincs hirdetés. Nincs nyomkövetés. Nincs Google Play-függőség.</string>
<string name="upgrade_foss_sponsor_returned_early">Már visszamész? A támogatásodnak köszönhetően él tovább a CAPod.</string>
</resources>
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Նվիրաբերել</string>
<string name="foss_upgrade_alreadydonated_label">Ես արդեն նվիրաբերել եմ</string>
<string name="foss_upgrade_no_money_label">Ես իմ ամբողջ գումարը ծախսում եմ AirPods-ի վրա</string>
<string name="upgrade_foss_preamble">CAPod FOSS անվճար և բաց կոդով: Այն ոգտակար էք, խնդրում զարգացությունը կենդանի պահելու համար:</string>
<string name="upgrade_foss_sponsor_action">Հովանավորել զարգացումը</string>
<string name="upgrade_foss_sponsor_subtitle">Լրատնություն չկա: հետքխկություն չկա: Google Play-ի կապվածություն չկա:</string>
<string name="upgrade_foss_sponsor_returned_early">Արդևն հետ? ձևր աիկտակցությունը CAPod-ին կենդանի պահում:</string>
</resources>
+4
View File
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Menyumbang</string>
<string name="foss_upgrade_alreadydonated_label">Saya telah berdonasi</string>
<string name="foss_upgrade_no_money_label">Saya menghabiskan semua uang saya untuk AirPods</string>
<string name="upgrade_foss_preamble">CAPod FOSS gratis dan sumber terbuka. Jika Anda merasa bermanfaat, pertimbangkan untuk mensponsori pengembangan guna menjaga proyek ini berjalan.</string>
<string name="upgrade_foss_sponsor_action">Pengembangan sponsor</string>
<string name="upgrade_foss_sponsor_subtitle">Tanpa iklan. Tanpa pelacakan. Tanpa ketergantungan Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Sudah kembali? Dukungan Anda menjaga CAPod tetap hidup.</string>
</resources>
+4
View File
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Gefa</string>
<string name="foss_upgrade_alreadydonated_label">Ég hef þegar gefið</string>
<string name="foss_upgrade_no_money_label">Ég eyði öllum peningunum mínum í AirPods</string>
<string name="upgrade_foss_preamble">CAPod FOSS er ókeypis og opinn kóði. Ef þú finnur það gagnlegt, þakkaðu þér fyrir að stuðla að þróun til að hjálpa til við að halda verkefninu gangandi.</string>
<string name="upgrade_foss_sponsor_action">Styrkja þróun</string>
<string name="upgrade_foss_sponsor_subtitle">Engar auglysíngar. Engin rakning. Ekkert Google Play-lás.</string>
<string name="upgrade_foss_sponsor_returned_early">Aftur þegar? Styðjað þít heldur CAPod lifandi.</string>
</resources>
+4
View File
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Dona</string>
<string name="foss_upgrade_alreadydonated_label">Ho già donato</string>
<string name="foss_upgrade_no_money_label">Spendo tutti i miei soldi per gli AirPods</string>
<string name="upgrade_foss_preamble">CAPod FOSS è gratuito e open source. Se lo trovi utile, considera di sponsorizzare lo sviluppo per contribuire a mantenere il progetto in vita.</string>
<string name="upgrade_foss_sponsor_action">Finanzia lo sviluppo</string>
<string name="upgrade_foss_sponsor_subtitle">Nessuna pubblicità. Nessun tracciamento. Nessun vincolo con Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Già via? Il tuo supporto mantiene CAPod in vita.</string>
</resources>
+4
View File
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">תרומה</string>
<string name="foss_upgrade_alreadydonated_label">כבר תרמתי</string>
<string name="foss_upgrade_no_money_label">מיטב כספי מושקע ב-AirPods</string>
<string name="upgrade_foss_preamble">CAPod FOSS הוא חינם וקוד פתוח. אם הוא שימושי לך, שקול לשקול על חסוי הפיתוח כדי לעזור להמשיך את הפרויקט.</string>
<string name="upgrade_foss_sponsor_action">פיתוח חסות</string>
<string name="upgrade_foss_sponsor_subtitle">אין פרסומות. אין מעקב. אין תלות ב-Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">כבר חוזר? התמיכה שלך שומרת את CAPod בחיים.</string>
</resources>
+4
View File
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">寄付</string>
<string name="foss_upgrade_alreadydonated_label">すでに寄付しました</string>
<string name="foss_upgrade_no_money_label">AirPodsに全財産をつぎ込んでいます</string>
<string name="upgrade_foss_preamble">CAPod FOSSは無料かつオープンソースです。便利だと思ったら、プロジェクトの継続に向けて開発をスポンサーすることをご検討ください。</string>
<string name="upgrade_foss_sponsor_action">開発支援者</string>
<string name="upgrade_foss_sponsor_subtitle">広告なし。追跡なし。Google Play依存なし。</string>
<string name="upgrade_foss_sponsor_returned_early">もう戻りますか?あなたのサポートがCAPodを支えています。</string>
</resources>
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">შემოწირულობა</string>
<string name="foss_upgrade_alreadydonated_label">მე უკვე შემოწირული მაქვს</string>
<string name="foss_upgrade_no_money_label">მთელ ჩემს ფულს AirPods-ებზე ვხარჯავ</string>
<string name="upgrade_foss_preamble">CAPod FOSS უფასოა და ღია წყაროა. თუ გსარგებლია, გაანაბრეთ განვითარება პროექტის გადასაგრელად.</string>
<string name="upgrade_foss_sponsor_action">სპონსორის განვითარება</string>
<string name="upgrade_foss_sponsor_subtitle">რეკლამა არა. თვალთვალება არა. Google Play-ზე დამოკიდება არა.</string>
<string name="upgrade_foss_sponsor_returned_early">უკვე დაბრუნდით? თქვენი მხარდაჯერა CAPod-ს გადარჩენებას ეხმარება.</string>
</resources>
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">បរិច្ចាគ</string>
<string name="foss_upgrade_alreadydonated_label">ខ្ញុំបានបរិច្ចាគរួចហើយ</string>
<string name="foss_upgrade_no_money_label">ខ្ញុំចំណាយលុយទាំងអស់របស់ខ្ញុំលើ AirPods</string>
<string name="upgrade_foss_preamble">CAPod FOSS ជាកម្មវិធីឥតគិតថ្លៃ និងប្រភពបើកចំហ។ ប្រសិនបើអ្នករកហើញថ្លៃវាមានប្រយោជន៍ សូមពិចារណាឧបត្ថម្ភការអភិវឌ្ឍន៍ ដើម្បីជួយឥ្សរិយគម្រោងបន្តដំណើរការ។</string>
<string name="upgrade_foss_sponsor_action">ឧបត្ថម្ភការអភិវឌ្ឍន៍</string>
<string name="upgrade_foss_sponsor_subtitle">គ្មានការផ្សាយពាណិជ្ជកម្ម។ គ្មានការតាមដាន។ គ្មានការចាក់សោ Google Play។</string>
<string name="upgrade_foss_sponsor_returned_early">ត្រលប់មកវិញហើយ។? ការគាំទ្ររបស់អ្នកចិញ្ចឹម CAPod ឥ្សរិយនៅរស់។</string>
</resources>
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Beşdar bike</string>
<string name="foss_upgrade_alreadydonated_label">Min berê bexş kir</string>
<string name="foss_upgrade_no_money_label">Ez hemû pereyê xwe li AirPods xerc dikim</string>
<string name="upgrade_foss_preamble">CAPod FOSS belaş û çavkaniya vekirî ye. Ger hûn wê kêrhastî dibînin, fikir bikin ku perkirezan bike da ku alîkariya berdewamî projeya bide.</string>
<string name="upgrade_foss_sponsor_action">Pêşdebiriyê teref bike</string>
<string name="upgrade_foss_sponsor_subtitle">Reklam tune. Śopandin tune. Girêdana Google Play tune.</string>
<string name="upgrade_foss_sponsor_returned_early">Zirû vegeriya? Piştgiriya we CAPod sax dihêle.</string>
</resources>
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">ದಾನ ಮಾಡಿ</string>
<string name="foss_upgrade_alreadydonated_label">ನಾನು ಈಗಾಗಲೇ ದಾನ ಮಾಡಿದ್ದೇನೆ</string>
<string name="foss_upgrade_no_money_label">ನಾನು ನನ್ನ ಎಲ್ಲಾ ಹಣವನ್ನು AirPods ಗಾಗಿ ಖರ್ಚು ಮಾಡುತ್ತೇನೆ</string>
<string name="upgrade_foss_preamble">CAPod FOSS ಉಚಿತ ಮತ್ತು ತೆರೆದ ಮೂಲದ್ದಾಗಿದೆ. ನಿಮಗೆ ಉಪಯುಕ್ತವಾಗಿ ತೋರಿದರೆ, ಯೋಜನೆಯನ್ನು ಮುಂದುವರಿಸಲು ಅಭಿವೃದ್ಧಿಗೆ ಪ್ರಾಯೋಜಕತ್ವ ನೀಡಲು ಪರಿಗಣಿಸಿ.</string>
<string name="upgrade_foss_sponsor_action">ಅಭಿವೃದ್ಧಿಗೆ ಪ್ರಾಯೋಜಕತ್ವ ನೀಡಿ</string>
<string name="upgrade_foss_sponsor_subtitle">ಜಾಹೀರಾತುಗಳಿಲ್ಲ. ಭಕ್ಷಣೆ ಇಲ್ಲ. Google Play ಲಾಕ್-ಇನ್ ಇಲ್ಲ.</string>
<string name="upgrade_foss_sponsor_returned_early">ಇಷ್ಟೇ ಹಿಂದಿರುಗಿದಿರಿ? ನಿಮ್ಮ ಬೆಂಬಲ CAPod ಅನ್ನು ಜೀವಂತವಾಗಿಡುತ್ತದೆ.</string>
</resources>
+4
View File
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">후원하기</string>
<string name="foss_upgrade_alreadydonated_label">이미 후원했어요</string>
<string name="foss_upgrade_no_money_label">AirPods에 돈을 다 썼어요</string>
<string name="upgrade_foss_preamble">CAPod FOSS는 무료 오픈소스입니다. 유용하게 사용하고 계신다면 프로젝트 유지에 도움이 되도록 후원을 고려해 주세요.</string>
<string name="upgrade_foss_sponsor_action">개발 후원하기</string>
<string name="upgrade_foss_sponsor_subtitle">광고 없음. 추적 없음. Google Play 의존 없음.</string>
<string name="upgrade_foss_sponsor_returned_early">벌써 돌아가세요? 여러분의 지원이 CAPod를 유지합니다.</string>
</resources>
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Демөөрчүлүк кылуу</string>
<string name="foss_upgrade_alreadydonated_label">Мен буга чейин демөөрчүлүк кылгам</string>
<string name="foss_upgrade_no_money_label">Мен бүт акчамды AirPods үчүн короттум</string>
<string name="upgrade_foss_preamble">CAPod FOSS беплатан жана ачык каттулууда. Егер пайдалуу таапсаңыз, жобаны улантырууга колдоо көрсөтүңүз.</string>
<string name="upgrade_foss_sponsor_action">Иштелеп чыгууну колдоо</string>
<string name="upgrade_foss_sponsor_subtitle">Жарнама. Кадалоолоо жок. Google Playга байланма.</string>
<string name="upgrade_foss_sponsor_returned_early">Артка кайттыңызбы? Сиздин колдооңуз CAPodты тирүү сактайт.</string>
</resources>
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">ບໍລິຈາກ</string>
<string name="foss_upgrade_alreadydonated_label">ຂ້ອຍໄດ້ບໍລິຈາກແລ້ວ</string>
<string name="foss_upgrade_no_money_label">ຂ້ອຍໃຊ້ເງິນທັງໝົດຂອງຂ້ອຍໃສ່ AirPods</string>
<string name="upgrade_foss_preamble">CAPod FOSS ແມ່ນຟຣີ ແລະ ໂອເພນຊອດ. ຖ້າທ່ານເຫັນວ່າມີປະໂຫຍດ, ພິຈາລະນາສະໜັບສະໜູນການພັດທະນາເພື່ອຊ່ວຍໃຫ້ໂຄງການດຳເນີນຕໍ່ໄປ.</string>
<string name="upgrade_foss_sponsor_action">ສະໜັບສະໜູນການພັດທະນາ</string>
<string name="upgrade_foss_sponsor_subtitle">ບໍ່ມີໂຄສະນາ. ບໍ່ມີການຕິດຕາມ. ບໍ່ຕິດ Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">ກັບມາແລ້ວ? ການສະໜັບສະໜູນຂອງທ່ານຮັກສາ CAPod ໃຫ້ດຳເນີນຕໍ່ໄປ.</string>
</resources>
+4
View File
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Aukoti</string>
<string name="foss_upgrade_alreadydonated_label">Aš jau paaukojau</string>
<string name="foss_upgrade_no_money_label">Visus savo pinigus išleidžiu „AirPods“ ausinėms</string>
<string name="upgrade_foss_preamble">CAPod FOSS yra nemokama ir atviro kodo programa. Jei ji jums naudinga, pagalvokite apie rėmimą, kad projektą būtų galima tęsti.</string>
<string name="upgrade_foss_sponsor_action">Remti plėtrą</string>
<string name="upgrade_foss_sponsor_subtitle">Jokių reklamų. Jokio sekimo. Jokio „Google Play“ priklausomybės.</string>
<string name="upgrade_foss_sponsor_returned_early">Jau grįžte? Jūsų parama palaiko CAPod gyvybę.</string>
</resources>
+4
View File
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Ziedot</string>
<string name="foss_upgrade_alreadydonated_label">Es jau ziedoju</string>
<string name="foss_upgrade_no_money_label">Es iztērēju visu savu naudu AirPods austiņām</string>
<string name="upgrade_foss_preamble">CAPod FOSS ir bezmaksas un atkļējkoda. Ja tā ir noderba, apsveriet attīstības sponsēšanu, lai palīdzētu turpināt projektu.</string>
<string name="upgrade_foss_sponsor_action">Sponsorēt attīstību</string>
<string name="upgrade_foss_sponsor_subtitle">Bez reklamām. Bez seklōšanas. Bez Google Play saistībām.</string>
<string name="upgrade_foss_sponsor_returned_early">Jau atpakaļ? Jūsu atbalsts uztur CAPod pie dzīvības.</string>
</resources>
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Донирај</string>
<string name="foss_upgrade_alreadydonated_label">Веќе донирав</string>
<string name="foss_upgrade_no_money_label">Ги трошам сите пари на AirPods</string>
<string name="upgrade_foss_preamble">CAPod FOSS е бесплатен и отворен код. Ако ви е корисен, размислете да го спонзорирате развојот за да помогнете проектот да продолжи.</string>
<string name="upgrade_foss_sponsor_action">Спонзорирај развој</string>
<string name="upgrade_foss_sponsor_subtitle">Без реклами. Без пратење. Без зависност од Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Веќе се вративте? Вашата поддршка го одржува CAPod жив.</string>
</resources>
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">സംഭാവന ചെയ്യുക</string>
<string name="foss_upgrade_alreadydonated_label">ഞാൻ ഇതിനകം സംഭാവന ചെയ്തു</string>
<string name="foss_upgrade_no_money_label">ഞാൻ എൻ്റെ എല്ലാ പണവും AirPods-നായി ചെലവഴിക്കുന്നു</string>
<string name="upgrade_foss_preamble">CAPod FOSS സൗജന്യവും ഓപ്പൻ സോഴ്സുമാണ്. നിങ്ങൾക്ക് ഇത് ഉപകാരപ്രദമാണെങ്കിൽ, പ്രോജെക്ട് തുടരാൻ സഹായിക്കുന്നതിന് ഡെവലപ്പ്മെന്റ് സ്പോൺസർ ചെയ്യുന്നത് പരിഗണിക്കുക.</string>
<string name="upgrade_foss_sponsor_action">വികസനം സ്പോൺസർ ചെയ്യുക</string>
<string name="upgrade_foss_sponsor_subtitle">പരസ്യങ്ങൾ ഇല്ല. ട്രാക്കിംഗ് ഇല്ല. Google Play ലോക്ക്-ഇൻ ഇല്ല.</string>
<string name="upgrade_foss_sponsor_returned_early">ഇത്ര പെട്ടെന്ന് തിരിച്ചു പോകുന്നോ? നിങ്ങളുടെ പിന്തുണ CAPod നിലനിർത്തുന്നു.</string>
</resources>
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Хандивлах</string>
<string name="foss_upgrade_alreadydonated_label">Би аль хэдийн хандивласан</string>
<string name="foss_upgrade_no_money_label">Би бүх мөнгөө AirPods-д зарцуулдаг</string>
<string name="upgrade_foss_preamble">CAPod FOSS нь үнэгүй, нээлтэй эхийн програм. Хэрэв танд хэрэгтэй бол төслийг үргэлжүүлэхэд туслахын тулд хөгжүүлэлтийг дэмжих талаар бодоорой.</string>
<string name="upgrade_foss_sponsor_action">Хөгжлийг дэмжих</string>
<string name="upgrade_foss_sponsor_subtitle">Реклам байхгүй. Хяналт байхгүй. Google Play-д хамааралгүй.</string>
<string name="upgrade_foss_sponsor_returned_early">Аль хэдэйн буцаж ирлээ? Таны дэмжлэг CAPod-ийг амьд байлгадаг.</string>
</resources>
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">दान करा</string>
<string name="foss_upgrade_alreadydonated_label">मी आधीच दान केले आहे</string>
<string name="foss_upgrade_no_money_label">मी माझे सर्व पैसे AirPods वर खर्च करतो</string>
<string name="upgrade_foss_preamble">CAPod FOSS विनामूल्य आणि ओपन सोर्स आहे. जर तुम्हाला ते उपयुक्त वाटत असेल, तर प्रकल्प सुरू ठेवण्यासाठी विकासास प्रायोजित करण्याचा विचार करा.</string>
<string name="upgrade_foss_sponsor_action">विकासास प्रायोजित करा</string>
<string name="upgrade_foss_sponsor_subtitle">कोणत्याही जाहिराती नाहीत. कोणतेही ट्रॅकिंग नाही. Google Play चे बंधन नाही.</string>
<string name="upgrade_foss_sponsor_returned_early">आधीच परत? तुमचा पाठिंबा CAPod जिवंत ठेवतो.</string>
</resources>
+4
View File
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Sumbang</string>
<string name="foss_upgrade_alreadydonated_label">Saya telah menyumbang</string>
<string name="foss_upgrade_no_money_label">Saya belanjakan semua wang saya untuk AirPods</string>
<string name="upgrade_foss_preamble">CAPod FOSS adalah percuma dan sumber terbuka. Jika anda mendapatinya berguna, pertimbangkan untuk menaja pembangunannya bagi membantu projek ini terus berjalan.</string>
<string name="upgrade_foss_sponsor_action">Pembangunan penaja</string>
<string name="upgrade_foss_sponsor_subtitle">Tiada iklan. Tiada penjejakan. Tiada ikatan Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Sudah kembali? Sokongan anda mengekalkan CAPod.</string>
</resources>
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">လှူဒါန်းပါ</string>
<string name="foss_upgrade_alreadydonated_label">ကျွန်တော် လှူပြီးပါပြီ</string>
<string name="foss_upgrade_no_money_label">ကျွန်တော် ပိုက်ဆံအားလုံးကို AirPods မှာ သုံးတယ်</string>
<string name="upgrade_foss_preamble">CAPod FOSS သည် အခမဲ့နှင့် open source ဖြစ်သည်။ အသုံးပြုခုပ်ဆည်းနိုင် အသားဖြစ်ပါက ပရာဗက်ဆက်လက်ရန် ဖွဲ့ဖြီးရေးကို ပံ့ပိုးပေးသည်။</string>
<string name="upgrade_foss_sponsor_action">ဖွံ့ဖြိုးတိုးတက်မှုကို ကမကထပံ့ပိုးရန်</string>
<string name="upgrade_foss_sponsor_subtitle">ကြောင်ညာမရှိ။ ခြေရာမခံ။ Google Play ချုပ်ချယ်မြုမရှိ။</string>
<string name="upgrade_foss_sponsor_returned_early">ပြန်သွားပြီးလါ? သင်၏ ပံ့ပိုးမှု CAPod ကို ရှင်သန်ဆက်လက်ကေစေသည်။</string>
</resources>
+4
View File
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Doner</string>
<string name="foss_upgrade_alreadydonated_label">Jeg har allerede donert</string>
<string name="foss_upgrade_no_money_label">Jeg bruker alle pengene mine på AirPods</string>
<string name="upgrade_foss_preamble">CAPod FOSS er gratis og åpen kildekode. Hvis du synes den er nyttig, kan du vurdere å sponse utviklingen for å bidra til at prosjektet går videre.</string>
<string name="upgrade_foss_sponsor_action">Støtt utviklingen</string>
<string name="upgrade_foss_sponsor_subtitle">Ingen reklame. Ingen sporing. Ingen Google Play-avhengighet.</string>
<string name="upgrade_foss_sponsor_returned_early">Allerede tilbake? Din støtte holder CAPod i live.</string>
</resources>
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">दान गर्नुहोस्</string>
<string name="foss_upgrade_alreadydonated_label">मैले पहिले नै दान गरिसकेको छु</string>
<string name="foss_upgrade_no_money_label">म आफ्नो सबै पैसा AirPods मा खर्च गर्छु</string>
<string name="upgrade_foss_preamble">CAPod FOSS निःशुल्क र खुला स्रोत छ। यदि तपाईंले उपयोगी पाउनुभएको छ भने, प्राजेक्ट चलाउन राख्न विकासमा प्रायोजन गर्न विचार गर्नुहोस्।</string>
<string name="upgrade_foss_sponsor_action">विकासलाई प्रायोजन गर्नुहोस्</string>
<string name="upgrade_foss_sponsor_subtitle">कुनै विज्ञापन छैन। कुनै ट्रैकिङ छैन। Google Playमा बाँधिएको छैन।</string>
<string name="upgrade_foss_sponsor_returned_early">पहिले नै फिर्नुभयो? तपाईंको सहयोगले CAPod जीवित राख्छ।</string>
</resources>
+4
View File
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Doneren</string>
<string name="foss_upgrade_alreadydonated_label">Ik heb al gedoneerd</string>
<string name="foss_upgrade_no_money_label">Ik besteed al mijn geld aan AirPods</string>
<string name="upgrade_foss_preamble">CAPod FOSS is gratis en open source. Als je het nuttig vindt, overweeg dan om de ontwikkeling te sponsoren om het project gaande te houden.</string>
<string name="upgrade_foss_sponsor_action">Sponsor ontwikkeling</string>
<string name="upgrade_foss_sponsor_subtitle">Geen advertenties. Geen tracking. Geen Google Play-vergrendeling.</string>
<string name="upgrade_foss_sponsor_returned_early">Alweer terug? Jouw steun houdt CAPod in leven.</string>
</resources>
+6 -2
View File
@@ -1,6 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="foss_upgrade_donate_label">Wsparcie</string>
<string name="foss_upgrade_alreadydonated_label">Już się zrzuciłem</string>
<string name="foss_upgrade_donate_label">Wesprzyj</string>
<string name="foss_upgrade_alreadydonated_label">Już wsparłem</string>
<string name="foss_upgrade_no_money_label">Wydałem wszystkie pieniądze na AirPods</string>
<string name="upgrade_foss_preamble">CAPod FOSS jest bezpłatny i otwartoźródłowy. Jeśli uważasz go za użyteczny, rozważ sponsorowanie rozwoju, aby pomóc kontynuować projekt.</string>
<string name="upgrade_foss_sponsor_action">Wesprzyj rozwój</string>
<string name="upgrade_foss_sponsor_subtitle">Bez reklam. Bez śledzenia. Bez uzależnienia od Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Już wróciłeś? Twoje wsparcie utrzymuje CAPod przy życiu.</string>
</resources>
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Doar</string>
<string name="foss_upgrade_alreadydonated_label">Eu já doei</string>
<string name="foss_upgrade_no_money_label">Eu gasto todo o meu dinheiro em AirPods</string>
<string name="upgrade_foss_preamble">O CAPod FOSS é gratuito e de código aberto. Se você o achar útil, considere patrocinar o desenvolvimento para ajudar a manter o projeto ativo.</string>
<string name="upgrade_foss_sponsor_action">Patrocinador do desenvolvimento</string>
<string name="upgrade_foss_sponsor_subtitle">Sem anúncios. Sem rastreamento. Sem dependência do Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Já voltou? Seu apoio mantém o CAPod vivo.</string>
</resources>
+4
View File
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Doar</string>
<string name="foss_upgrade_alreadydonated_label">Já doei</string>
<string name="foss_upgrade_no_money_label">Gastei todo o meu dinheiro em AirPods</string>
<string name="upgrade_foss_preamble">O CAPod FOSS é gratuito e de código aberto. Se o achar útil, considere patrocinar o desenvolvimento para ajudar a manter o projeto ativo.</string>
<string name="upgrade_foss_sponsor_action">Patrocine o desenvolvimento</string>
<string name="upgrade_foss_sponsor_subtitle">Sem anúncios. Sem rastreamento. Sem dependência do Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Já está a sair? O seu apoio mantém o CAPod ativo.</string>
</resources>
+9 -1
View File
@@ -1,2 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="foss_upgrade_donate_label">Donar</string>
<string name="foss_upgrade_alreadydonated_label">Hai gia donat</string>
<string name="foss_upgrade_no_money_label">Mett tut mes daners en AirPods</string>
<string name="upgrade_foss_preamble">CAPod FOSS è gratuit ed open source. Sche ti chattast ch\'el è util, considerescha da sponsurisar il sviluppament per agidà a mantegnair il project.</string>
<string name="upgrade_foss_sponsor_action">Sponsurisar il sviluppament</string>
<string name="upgrade_foss_sponsor_subtitle">Nagins annunzis. Nagin tracking. Nagin blocadi da Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Gia enavos? Tes sustegn mantegn CAPod viv.</string>
</resources>
+4
View File
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Donează</string>
<string name="foss_upgrade_alreadydonated_label">Am donat deja</string>
<string name="foss_upgrade_no_money_label">Mi-am cheltuit toți banii pe AirPods</string>
<string name="upgrade_foss_preamble">CAPod FOSS este gratuit si open source. Daca il gasiti util, luati in considerare sponsorizarea dezvoltarii pentru a ajuta la continuarea proiectului.</string>
<string name="upgrade_foss_sponsor_action">Sponsorizează dezvoltarea</string>
<string name="upgrade_foss_sponsor_subtitle">Fara reclame. Fara urmarire. Fara dependenta de Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Ati revenit deja? Sprijinul dvs. mentine CAPod in viata.</string>
</resources>
+4
View File
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Пожертвовать</string>
<string name="foss_upgrade_alreadydonated_label">Я уже пожертвовал</string>
<string name="foss_upgrade_no_money_label">Я потратил все свои деньги на AirPods</string>
<string name="upgrade_foss_preamble">CAPod FOSS бесплатный и с открытым исходным кодом. Если вам полезно приложение, рассмотрите возможность спонсировать разработку, чтобы помочь проекту продолжать развиваться.</string>
<string name="upgrade_foss_sponsor_action">Поддержать разработку</string>
<string name="upgrade_foss_sponsor_subtitle">Без рекламы. Без слежки. Без привязки к Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Вернулись уже? Ваша поддержка помогает CAPod жить.</string>
</resources>
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Dona</string>
<string name="foss_upgrade_alreadydonated_label">Apo giai donadu</string>
<string name="foss_upgrade_no_money_label">Apo spèndidu totu is soddos meos pro is AirPods</string>
<string name="upgrade_foss_preamble">CAPod FOSS est gratis e fonte aberta. Si test de agiudu, considera de patrotzinare su isviluppu pro agiudare a mantennere su progetu.</string>
<string name="upgrade_foss_sponsor_action">Sponsoriza s\'isvilupu</string>
<string name="upgrade_foss_sponsor_subtitle">Peroe publicidade. Peroe trachiamentu. Peroe dipendentzia de Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Zas torradas giai? Su sostegnu tuyu mantenet CAPod in vida.</string>
</resources>
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">පරිත්‍යාග</string>
<string name="foss_upgrade_alreadydonated_label">මම දැනටමත් පරිත්‍යාග කර ඇත</string>
<string name="foss_upgrade_no_money_label">මම මගේ මුදල් සියල්ල AirPods සඳහා වියදම් කරමි</string>
<string name="upgrade_foss_preamble">CAPod FOSS නියමිත සහ විවෘත මූලාශ්‍රයි. ඔබට ඉත උපකාරී නම්, ආයෝජනය ගිණුම්‍ සහය ගෙවිපීම කිරීමට අර්වමයක් ගෙවිපීම් ප්‍රස්තා කරන්නට සංවර්ධන කරන්න.</string>
<string name="upgrade_foss_sponsor_action">සංවර්ධනයට අනුග්‍රහය දෙන්න</string>
<string name="upgrade_foss_sponsor_subtitle">අළු නැත. ට්‍රැකිං නැත. Google Play සලක඾ත්වය නැත.</string>
<string name="upgrade_foss_sponsor_returned_early">එක්බේන්මට පළ්ට? ඔබ෪් සහය් CAPod ක්‍රියාත්මකව තබා ගෙනයි.</string>
</resources>
+4
View File
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Prispieť</string>
<string name="foss_upgrade_alreadydonated_label">Už som prispel/a</string>
<string name="foss_upgrade_no_money_label">Všetky peniaze som minul na AirPods</string>
<string name="upgrade_foss_preamble">CAPod FOSS je zadarmo a open source. Ak ho nájdete užitočným, zvažte sponzorovanie vývoja, aby ste pomohli udržiavať projekt v chode.</string>
<string name="upgrade_foss_sponsor_action">Sponzorovaný vývoj</string>
<string name="upgrade_foss_sponsor_subtitle">Žiadne reklamy. Žiadne sledovanie. Žiadna závislosť od Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Už späť? Vaša podpora udržiava CAPod pri živote.</string>
</resources>
+4
View File
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Doniraj</string>
<string name="foss_upgrade_alreadydonated_label">Sem že doniral</string>
<string name="foss_upgrade_no_money_label">Ves svoj denar porabim za AirPods</string>
<string name="upgrade_foss_preamble">CAPod FOSS je brezplačen in odprtokoden. Če se vam zdi koristen, razmislite o sponzoriranju razvoja, da pomagate ohranjati projekt.</string>
<string name="upgrade_foss_sponsor_action">Podpri razvoj</string>
<string name="upgrade_foss_sponsor_subtitle">Brez oglasov. Brez sledenja. Brez vezave na Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Že nazaj? Vaša podpora ohranja CAPod pri življenju.</string>
</resources>
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Dhuro</string>
<string name="foss_upgrade_alreadydonated_label">Unë tashmë kam dhuruar</string>
<string name="foss_upgrade_no_money_label">Unë i shpenzoj të gjitha paratë e mia për AirPods</string>
<string name="upgrade_foss_preamble">CAPod FOSS është falas dhe me burim të hapur. Nëse e gjeni të dobishmëm, konsideroni sponsorizimin e zhvillimit për ta mbajtur projektin gjallë.</string>
<string name="upgrade_foss_sponsor_action">Zhvillimi i sponsorëve</string>
<string name="upgrade_foss_sponsor_subtitle">Pa reklama. Pa gjurmim. Pa varësi nga Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Ktheheni tashmjë? Mbështjetja juaj e mban CAPod gjallë.</string>
</resources>
+4
View File
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Донације</string>
<string name="foss_upgrade_alreadydonated_label">Већ сам донирао/ла</string>
<string name="foss_upgrade_no_money_label">Сав новац трошим на АирПодс</string>
<string name="upgrade_foss_preamble">CAPod FOSS је бесплатан и отвореног кода. Ако вам је користан, размотрите спонзорисање развоја да би пројекат остао жив.</string>
<string name="upgrade_foss_sponsor_action">Спонзориши развој</string>
<string name="upgrade_foss_sponsor_subtitle">Без реклама. Без праћења. Без зависности од Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Већ одлазите? Ваша подршка чува CAPod живим.</string>
</resources>
+4
View File
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Donera</string>
<string name="foss_upgrade_alreadydonated_label">Jag har redan donerat</string>
<string name="foss_upgrade_no_money_label">Jag spenderar alla mina pengar på AirPods</string>
<string name="upgrade_foss_preamble">CAPod FOSS är gratis och öppen källkod. Om du tycker att det är användbart kan du överväga att sponsra utvecklingen för att hjälpa till att hålla projektet vid liv.</string>
<string name="upgrade_foss_sponsor_action">Sponsra utvecklingen</string>
<string name="upgrade_foss_sponsor_subtitle">Inga annonser. Ingen spårning. Ingen bindning till Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Redan tillbaka? Ditt stöd håller CAPod vid liv.</string>
</resources>
+4
View File
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Changia</string>
<string name="foss_upgrade_alreadydonated_label">Nimeshachangia tayari</string>
<string name="foss_upgrade_no_money_label">Natumia pesa zangu zote kwenye AirPods</string>
<string name="upgrade_foss_preamble">CAPod FOSS ni bure na chanzo huria. Ikiwa unaikuta ni muhimu, fikiria kuchangia maendeleo ili kusaidia mradi kuendelea.</string>
<string name="upgrade_foss_sponsor_action">Faidhi maendeleo</string>
<string name="upgrade_foss_sponsor_subtitle">Hakuna matangazo. Hakuna ufuatiliaji. Hakuna kufungwa kwa Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Unarudi tayari? Msaada wako unaweka CAPod iwe hai.</string>
</resources>
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">நன்கொடை</string>
<string name="foss_upgrade_alreadydonated_label">நான் ஏற்கனவே நன்கொடை அளித்துவிட்டேன்</string>
<string name="foss_upgrade_no_money_label">எனது பணத்தை எல்லாம் ஏர்பாட்களில் செலவிடுகிறேன்</string>
<string name="upgrade_foss_preamble">CAPod FOSS இலவசமாகவும் திறழ் மூலக்கோடும் உள்ளது. இது பயனுள்ளதாக கணித்தால், தொடர்பு பொறுத்துக்க அம்சத்தை நிச்சயிக்கவும்.</string>
<string name="upgrade_foss_sponsor_action">மேம்பாட்டை ஸ்பான்சர் செய்யுங்கள்</string>
<string name="upgrade_foss_sponsor_subtitle">ஐவிளம்பரம் இல்லை. கண்காணிப்பு இல்லை. Google Play கட்டுப்பாட்டு இல்லை.</string>
<string name="upgrade_foss_sponsor_returned_early">இத்தனை இருக்கிறீரகளா? உங்கள் ஆதரவு CAPodஐ உயிருடன் வைத்திருக்கிறது.</string>
</resources>
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">విరాళం ఇవ్వండి</string>
<string name="foss_upgrade_alreadydonated_label">నేను ఇప్పటికే విరాళం ఇచ్చాను</string>
<string name="foss_upgrade_no_money_label">నా డబ్బు అంతా ఎయిర్‌పాడ్స్‌కే ఖర్చు చేస్తాను</string>
<string name="upgrade_foss_preamble">CAPod FOSS ఉచిత మరియు ముక్త మూలం. ఇది ఉపయోగకరంగా అనిపిస్తే, ప్రాజెక్ట్ కొనసాగించడానికి అభివృద్ధిని స్పాన్సర్ చేయబక్తా పరిగణించండి.</string>
<string name="upgrade_foss_sponsor_action">అభివృద్ధిని స్పాన్సర్ చేయండి</string>
<string name="upgrade_foss_sponsor_subtitle">జాహీరాతులు లేవు. ట్రాకింగ్ లేదు. Google Play ఒక్కటికి పరిమితం కాదు.</string>
<string name="upgrade_foss_sponsor_returned_early">ఇర్వై వెనక్కి వెళ్ళారా? మీ మద్దతు CAPodను సజీవంగా ఉంచుతుంది.</string>
</resources>
+4
View File
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">บริจาค</string>
<string name="foss_upgrade_alreadydonated_label">ฉันบริจาคแล้ว</string>
<string name="foss_upgrade_no_money_label">ฉันใช้เงินทั้งหมดไปกับ AirPods</string>
<string name="upgrade_foss_preamble">CAPod FOSS เป็นฟรีและโอเพนซอร์ส หากคุณเห็นว่ามีประโยชน์ ลองสนับสนุนการพัฒนาเพื่อช่วยให้โครงการดำเนินต่อไป</string>
<string name="upgrade_foss_sponsor_action">สปอนเซอร์การพัฒนา</string>
<string name="upgrade_foss_sponsor_subtitle">ไม่มีโฆษณา ไม่ติดตาม ไม่ผูกขาด Google Play</string>
<string name="upgrade_foss_sponsor_returned_early">กลับมาแล้ว? การสนับสนุนของคุณช่วยให้ CAPod ดำเนินต่อไป</string>
</resources>
+4
View File
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Bağış yap</string>
<string name="foss_upgrade_alreadydonated_label">Zaten bağış yaptım</string>
<string name="foss_upgrade_no_money_label">Tüm paramı AirPods\'a harcıyorum</string>
<string name="upgrade_foss_preamble">CAPod FOSS ücretsiz ve açık kaynaklıdır. Eğer faydalı bulduysan, projenin devam edebilmesi için lütfen geliştirmeye destek olmayı düşün.</string>
<string name="upgrade_foss_sponsor_action">Geliştirmeyi destekleyin</string>
<string name="upgrade_foss_sponsor_subtitle">Reklam yok. Takip yok. Google Play sınırlaması yok.</string>
<string name="upgrade_foss_sponsor_returned_early">Şimdiden döndün mü? Desteğin CAPod\'u ayakta tutuyor.</string>
</resources>
+4
View File
@@ -3,4 +3,8 @@
<string name="foss_upgrade_donate_label">Пожертва</string>
<string name="foss_upgrade_alreadydonated_label">Вже пожертвував</string>
<string name="foss_upgrade_no_money_label">Витратив усі гроші на AirPods</string>
<string name="upgrade_foss_preamble">CAPod FOSS є безкоштовним та відкритим програмним забезпеченням. Якщо він вам корисний, розгляньте можливість спонсорування розробки, щоб допомогти проєкту продовжувати роботу.</string>
<string name="upgrade_foss_sponsor_action">Розвиток спонсорів</string>
<string name="upgrade_foss_sponsor_subtitle">Без реклами. Без стеження. Без прив\'язки до Google Play.</string>
<string name="upgrade_foss_sponsor_returned_early">Вже повертаєтеся? Ваша підтримка зберігає CAPod.</string>
</resources>

Some files were not shown because too many files have changed in this diff Show More