Compare commits

...
Author SHA1 Message Date
darken bd4d87c06a feat(aap): Add Custom EQ protocol support
Adds decode and encode for AAP opcode 0x63, Apple's iOS 27 "Custom EQ"
(three bands plus a Recommended/Custom mode selector, H2 models).

The wire format comes from librepods commit 7341e41 and has never been
confirmed on hardware, so the decoder validates every field and returns
null on any mismatch -- including a strict payload-length equality, so a
frame carrying trailing unknown bytes is surfaced verbatim in the
unknown-message hex log rather than mis-parsed into plausible values.
Nothing optimistically assumes the write lands: SetCustomEq deliberately
has no optimistic state update and no verification predicate, because an
unacknowledged write must not render as success, and the device is not
known to report this setting at all.

Also recovers two bytes the 0x53 decoder was discarding. Opcode 0x53 is
the Headphone Accommodations configuration -- an iOS Accessibility
feature that the iOS 18.1+ hearing-aid feature reuses -- and payload
bytes 4 and 5 are its "Apply To: Phone / Media" flags. They are plain
0x01 flags, not the Apple-bool 0x01/0x02 encoding used by the 0x09
control settings. They describe scope only and say nothing about whether
a profile exists, so isAllZero stays a pure band-data predicate.

A debug-only card (BuildConfig.DEBUG, mirroring the existing PME chart)
drives the opcode for hardware evaluation. It edits local draft state and
sends exactly one packet per Apply tap; slider-driven sends would flood
the link and make a logcat observation window unreadable. Apply is
disabled while no pod is worn, because AapOutboundController queues
non-exempt commands in that state and the queue collapses repeats,
surfacing them later at an unrelated moment. That guard reads the AAP
EarDetection setting rather than PodDevice's BLE-fallback property, so it
matches the controller's gate in every state -- notably, neither gates
when the AAP setting is absent.

On-device evaluation, both legs negative: AirPods Pro 2 USB-C (A3048,
fw 81.2675000075000000.6814) and AirPods Pro 3 (A3064, fw ...6503)
never advertise 0x63 across settings bursts of 33 and 26 frames, never
echo a write, never persist one across a case-cycle reconnect, and
produce no audible change. Both accept the write at the socket and
discard it. This does not establish that the byte format is wrong -- a
device ignoring an unimplemented opcode is indistinguishable from one
rejecting a malformed frame -- and it is not evidence that privileged
access is required, since neither device advertises the feature at all.
2026-08-21 15:23:32 +02:00
Matthias Urhahn b07428f894 Merge pull request #686 from d4rken-org/worktree-anc-adaptive-misreport
Device: Fix duplicate listening mode commands and silent failures
2026-08-20 18:16:20 +02:00
darken dbd1d422c6 refactor(aap): Split the echo classifier out of this change
Everything that reinterprets what the device reported moves to its own branch
(anc-echo-classifier), leaving only changes that stand on their own.

The classifier addresses a fault that has never been observed being handled: it
reproduced on two of four sessions and none since it was written. It also adds a
failure mode that did not exist before, where a report misattributed to our own
write makes CAPod show a mode the device is not in. That is a poor trade to carry
into main on the strength of tests alone, so it waits until it can be seen working
against a live fault.

What remains does not depend on the misreport:

- the verification deadline was 1000ms while the device answers in 833-1008ms, so
  a healthy reply could land just after the timer and trigger a bogus divergence
  plus a redundant re-send; this was captured live
- a listening mode request the device did not confirm produced no feedback at all
  for any mode except Off, which was a gap in the event plumbing rather than a
  timing artifact
- a mode outside the device's listening mode cycle was rendered as an ordinary
  selectable button whenever it happened to be the current mode
- an Off report arriving while a different mode was requested could teach the Allow
  Off inference, persisting "Off is permitted" into the device profile

Also drops effectiveAncMode, which only had an effect while the classifier was
present.
2026-08-20 12:38:30 +02:00
darken 0b83e86c5e fix(aap): Correct an overstated safety claim and a test that proved nothing
Review follow-ups on the previous commit.

Its claim that "nothing is learned or persisted" was wrong. BatteryEstimator
buckets drain samples by the current listening mode and force-persists the
accumulated window whenever that mode changes, so a misattributed mode can write
a drain rate to disk under the wrong bucket, and the corrective report does not
remove it. This is not new: the misreport being fixed here already mis-buckets in
the same way, and more often, since the device claims OFF while the pods play
Adaptive. Classifying corrects the common case and only gets it wrong on the rarer
misattribution. The design stands, the claim does not.

Two other claims were also too strong. The recorded echo is the first report after
the verification was installed, which is not exactly the wire write, so write
contention above the latency boundary can still inflate a fast refusal. And a
superseded write is not left alone entirely: classification is skipped, but it
still falls through to the ordinary retry path.

The supersession regression test asserted nothing: it fed a fresh ADAPTIVE report
in before its only assertion, overwriting either outcome, so it passed whether or
not the guard existed. It now asserts on the state left by the delayed echo, and
fails with the guard removed.

Also drops an unused import and restores an indent lost when the remap argument
was removed.
2026-08-20 00:06:03 +02:00
darken b396ce457d fix(aap): Attribute a listening mode echo before drawing conclusions from it
Extends the previous commit's classifier so it only judges evidence it can
actually attribute to our own write.

The classifier judged whatever mode happened to be current when the deadline
fired, and never looked at timing, despite the refusal-versus-misreport
distinction resting on it. A delayed answer to an earlier write, or a mode change
made on the pods themselves mid-request, could be taken for the answer to the
outstanding write.

Now the first report after a write is recorded with its latency, and that
recorded frame is what gets classified. A write is left alone entirely when it
was superseded by another listening mode write or by a stem press, since its
echoes can no longer be attributed. An answer arriving faster than 500ms is a
refusal, never a change: captures put refusals at 25-267ms and real changes at
815-1010ms. A re-send restamps its own send time and drops the previous attempt's
echo, so a retry is never judged on stale evidence. Latency is measured with the
monotonic clock, so a wall clock correction cannot turn a fast refusal into an
apparent change.

A misattribution is still possible, because AAP reports carry no correlation id
and a change made from iOS or another paired phone is invisible here. That is why
nothing is learned or persisted from this: the worst case is one wrong reading
that the device's next report corrects.

An earlier version of this work also carried a session-scoped remap, so that a
stem-initiated switch could be read correctly after our own write had proven the
device mislabels a value (issue #594). It is not included. The same
unattributability that bounds the classifier to a single wrong reading would have
let one misattribution rewrite every later report in the session, and a wrongly
resolved Off could persist AllowOffOption into the device profile, outliving the
session that produced it. Stem-initiated switches on an affected session are
therefore still not shown correctly.

Refs #594
2026-08-19 23:52:52 +02:00
darken 734e15c94d fix(aap): Tell a listening mode refusal apart from an unusable echo
Follow-up to the previous commit, which left the wrong half of this in place.
Distrusting the report only while the request was outstanding meant that four
seconds later the rejection cleared the pending mode, the bogus value came back,
and the user was shown an error for a mode change that had actually worked.

The two cases have different signatures, both readable from state the engine
already holds:

- A refusal echoes the mode the device is staying in, quickly. Captured Off
  refusals answer in 25-267ms with the previous mode.
- The Pro 3 misreport answers with a third mode, neither the one requested nor
  the one it was in, at normal change latency (815-1010ms).

So an echo that is neither the requested nor the previous mode is treated as an
unusable report rather than a refusal: no re-send of a write that already took
effect, no rejection, no error, and the requested mode is recorded as current.
Refusals still work, which is what the Off rejection message and the Allow Off
learning depend on.

This is deliberately engine-local. Seeding the cycle mask and Allow Off belief
from the device profile into the session would have encoded the rule directly,
but it inverts the current engine-to-profile data flow and creates a belief that
no device report can ever correct, since AirPods never report 0x1A or 0x34.
librepods keeps the same knowledge in its service layer and preferences, not in
its protocol manager.

The fault is per-session rather than per-request: across four sessions today the
pods either misreported every Adaptive write or none of them. The heuristic is
covered by unit tests but has not yet been observed handling a live bad session.
2026-08-19 20:33:32 +02:00
darken edf84248ab fix(anc): Don't show a phantom Off mode when AirPods misreport the listening mode
AirPods Pro 3 can answer a listening mode write with 0x0D 0x01 (Off) while
audibly switching to the requested mode. Seen on firmware
81.2675000075000000.6503, intermittently, and not reproducible on demand.

CAPod took that report at face value: it surfaced an Off button that isn't even
in the device's listening mode cycle, selected it, and said nothing about the
request not having been confirmed.

- visibleAncModes no longer re-admits a mode purely because it is the current
  one. That escape clause was what conjured the extra button.
- effectiveAncMode keeps showing the requested mode while our own request is
  outstanding and the device reports a mode it should not be able to reach.
- A rejected listening mode request now surfaces a message for every mode, not
  only Off. Other modes were dropped silently.
- The Allow Off inference ignores an Off report that arrived while a different
  mode was pending, so a single glitch cannot permanently persist "Off is
  allowed" into the device profile. An unsolicited Off still trains it, which
  is what keeps the option discoverable after it is enabled elsewhere.
- Verification deadline moved from a hardcoded 1000ms to 2000ms, and a matching
  device report now settles the verification when it arrives. Measured reply
  latency is 833-1008ms, so the old deadline sat inside the device's normal
  spread and could fire a bogus divergence plus a redundant re-send.

Settling verification on arrival is limited to SetAncMode deliberately. Every
other verified command is optimistically written into state when it is queued,
so its predicate is satisfied immediately and only the device's contradicting
echo makes it fail. Settling those early would swallow the rejection.

Note that AirPods never report AllowOffOption (0x34) or ListeningModeCycle
(0x1A), so which modes are permitted is always inferred, never device truth.
2026-08-19 19:09:20 +02:00
Matthias Urhahn 83dfdb9219 Merge pull request #682 from d4rken-org/feat/focus-duck-fallback
Reaction: Lower conversation volume even on phones that block it
2026-08-19 09:05:16 +02:00
darken 70b9c2981e fix(upgrade): Align the androidx.hilt declarations with the resolved 1.2.0
addWorkerManager() needed androidx.hilt 1.2.0 for its KSP compiler, which left
addDagger() asking for 1.0.0 while conflict resolution silently handed it 1.2.0,
and hilt-compiler declared twice at two different versions.

The version moves to Versions.AndroidX.Hilt.core, addDagger() stays the single
place that registers hilt-common and the hilt-compiler, and addWorkerManager()
declares only hilt-work. Declared and resolved versions now match, so the
dependency report no longer shows a 1.0.0 -> 1.2.0 substitution.

No dependency graph change: 1.2.0 is what already resolved.
2026-08-18 17:55:19 +02:00
darken 971fcbd34c fix(upgrade): Add a persistent acknowledgement safety net for Play purchases
Play auto-refunds (and revokes) purchases not acknowledged within 3 days.
The in-process ack machinery covers every case where the process lives
long enough; what it cannot cover is a process death around the Play
sheet (aggressive OEM task killers) followed by the user not reopening
the app before the deadline.

Add a gplay-only WorkManager safety net:
- PurchaseAckWorker: self-completing sweep via a new bounded
  BillingManager.ensureAllAcknowledged() that refreshes and acknowledges
  in the same coroutine (the reactive ack collector is async, so a worker
  cannot prove its acks happened through it). Retries with exponential
  backoff until the purchase's refund deadline, then gives up visibly.
- PurchaseAckScheduler: two unique work identities. A launch watch
  (REPLACE, armed and awaited before launchBillingFlow with a 30min delay
  so it cannot complete while the user is still in the sheet) and a
  discovered-purchase rescue (KEEP, 1min delay, armed directly from an
  ack pass that finds unacknowledged purchases, pre-attempt). Separate
  identities so a new purchase flow can never displace a pending rescue.
  Both triggers are fail-open: a broken WorkManager never blocks a
  purchase or an ack. WorkManager resolves via Provider at first arm so
  eager Application-time construction of the billing stack cannot
  trigger WorkManager's on-demand initialization prematurely.
- Nothing cancels the work from the foreground path: an ack pass can see
  zero unacked purchases while the sheet is still open, so the worker
  completes itself after its own reconciliation instead.

The ack pass now runs under a mutex (the worker sweep and the reactive
collector would otherwise race the token bookkeeping) and reports
per-outcome counts for the sweep result mapping.

This is a port of d4rken-org/sdmaid-se#2685; the ported sources are
byte-identical to the donor apart from the package rename.

CAPod had no explicit WorkManager wiring at all (work-runtime only
arrived transitively through Glance), so this also adds it:
- addWorkerManager() pinning androidx.work 2.7.1, the version already
  resolved via Glance, plus androidx.hilt:hilt-work and its KSP
  compiler. work-runtime-ktx is required at 2.7.1: CoroutineWorker,
  Operation.await, OneTimeWorkRequestBuilder and workDataOf all still
  live in the ktx artifact at that version. androidx.hilt moves 1.0.0 ->
  1.2.0 (by conflict resolution) because 1.0.0's hilt-compiler ships no
  KSP SymbolProcessorProvider, so @HiltWorker would generate nothing.
- WorkManagerModule providing the singleton WorkManager.
- App implements Configuration.Provider with the injected
  HiltWorkerFactory. WorkManager 2.7.1 still declares that interface as
  getWorkManagerConfiguration(), not the later property form.
- The manifest removes androidx.work's startup initializer so the
  on-demand configuration is the one that takes effect.

FOSS stays untouched behaviour-wise: all new billing types live in
src/gplay, workers need no manifest entry, and the worker factory
resolves the worker only in gplay variants.
2026-08-18 17:55:19 +02:00
Matthias Urhahn cebed0a60d Merge pull request #683 from d4rken-org/worktree-anc-selector-redesign
General: Redesign the noise control mode selector
2026-08-18 15:18:38 +02:00
darken 8817da309f ui(anc): Redesign the listening mode selector
Replaces the outlined segmented row with a filled track carrying a single
sliding thumb. The old control stacked three signals for one state (container
fill, bold, underline) in an outline treatment nothing else in the card uses,
which is what made it read as bolted on.

The track now sits at the same tonal step as the battery panel above it and
reuses the card's 16dp/12dp radii, so selection is carried by thumb position
and fill alone. Only the active mode is named and its slot expands to fit,
which keeps long translations off the width budget entirely.

Slot widths, label reveal and content tint are all derived from one clamped
fractional position, so geometry and content stay in lockstep. The thumb's
leading edge is that position in collapsed-slot units rather than a sum of
the animating widths, which would overshoot the target and crawl back.

Also fixes an invisible pending state: the in-flight branch set
activeContainerColor to secondaryContainer, which is exactly what
SegmentedButtonDefaults already used for the selected segment, so a requested
mode was indistinguishable from a confirmed one. An unconfirmed request now
draws a hollow pulsing thumb and tints the mode the pods are still in.

Transparency moves off Icons.TwoTone.Hearing, which is already the "In ear"
status chip and rendered identically two rows below it in the same card.
2026-08-18 15:04:50 +02:00
darken 27cc0e8d00 test(reaction): Cover the audio-focus duck fallback
Pins the outcome mapping of duckMusicVolume (a higher read-back stays a skip,
an unmoved one is a refusal), the focus request lifecycle (idempotent grant,
retry after denial, abandon only what is held, permanent loss clears the state
but a transient one does not), and the reaction side: focus is requested only
for an ignored write, released at teardown, re-requested by a keep-alive after
a permanent loss, and a late-landing volume write is restored.

The negative assertions are load-bearing: the MediaControl mock is relaxed, so
an accidental focus request on a healthy duck would pass silently.
2026-08-18 12:37:33 +02:00
darken cecd3088be feat(reaction): Duck via audio focus when the volume write is ignored
ColorOS 16 accepts setStreamVolume from a backgrounded app and leaves the
level where it was, so the conversation reaction ducked nothing. duckMusicVolume
now classifies the outcome (Ducked / Unchanged / Skipped) instead of collapsing
everything into a nullable duck, and a level that came back *higher* stays a
skip: that is the user raising the volume between the two reads, not a refusal.

On Unchanged the reaction requests AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK and lets
the framework attenuate the other player. Teardown abandons the focus and, if
the volume ended up below the pre-duck level anyway, restores it (guard for a
device that applies the write asynchronously). The focus request is built by an
injected factory because AudioFocusRequest.Builder is an unmocked stub in plain
JVM unit tests.
2026-08-18 12:36:30 +02:00
Matthias Urhahn 412ea369f1 Merge pull request #681 from d4rken-org/worktree-ca-duck-noop-diag
Reaction: Retry conversation volume lowering when the system ignores it
2026-08-17 18:40:57 +02:00
darken 22ca3a2a46 fix(bluetooth): Fix bogus scan-gap delays in debug logs
lastScanAt was read and written inside the log lambdas, which only run while
a logger is attached (log() checks Logging.hasReceivers first). In a release
build with recording off, the bookkeeping therefore never happened, so the
first delay of every debug recording reported the time since the *previous*
recording ended. Two logs from a support case opened with delay=878453ms and
delay=359983ms, which read as 14 and 6 minutes of suppressed scanning but
were just the gap between recordings.

The bookkeeping moves out of the lambdas, and the clock changes from
currentTimeMillis to elapsedRealtime so a wall-clock correction cannot
fabricate a gap either. That also puts the delay in the same boot-clock
domain as ScanResult.timestampNanos.
2026-08-17 13:00:02 +02:00
darken 92feea8a10 fix(reaction): Don't report a volume duck the system ignored
On ColorOS 16, setStreamVolume from a backgrounded app is accepted without
an exception and leaves the volume untouched: a debug log from an OPPO
PME110 shows "duckMusicVolume(100%): 40 -> 40 (requested 0)" while the same
call succeeded in the foreground.

duckMusicVolume returned a VolumeDuck for that, so ConversationReaction
recorded an Active session, armed the stale backstop, and later "restored" a
level that was never left. It now returns null when the read-back shows no
decrease, which routes the caller into its existing duck-no-op path: nothing
armed, nothing to restore, and a repeat START retries the duck instead of
treating the dead session as a keep-alive.

The predicate is applied >= prior rather than == prior. A route that
quantizes the target back up to the starting index attenuated nothing
either, and a volume that came back higher (user raised it between the two
reads) must not produce a duck whose restore would undo their change. The
WARN says "volume did not decrease" rather than blaming the ROM, since the
read-back alone cannot distinguish those three causes.
2026-08-17 13:00:02 +02:00
d4rken-org-releaser[bot] 3af1af872e Release: 5.2.3-rc0 2026-08-16 19:51:18 +00:00
Matthias Urhahn 1f00925cd8 Merge pull request #680 from d4rken-org/crowdin-update-20260816
General: Update translations from Crowdin
2026-08-16 20:47:28 +02:00
darken 50079e2b7a fix(upgrade): Remove orphaned translation strings failing lint vitals
upgrade_screen_sub_check_failed_title/_message no longer exist in the
English source (removed in 3e6541de). Seven locales still carried
Crowdin TM-poisoned translations for them, tripping ExtraTranslation
in lintVitalGplayBeta/Release.
2026-08-16 20:17:47 +02:00
darken 02ec76980c General: Update app translations from Crowdin 2026-08-16 19:52:33 +02:00
darken 52ad90f77e General: Update fastlane translations from Crowdin 2026-08-16 19:39:06 +02:00
darken 563c6e71f7 fix(upgrade): Invalidate a dead billing connection from refreshStrict too
refreshStrict() throws its incomplete-result error AFTER useConnection
already returned: refreshPurchases hands back a partial result instead of
throwing, so useConnection's dead-binder detection never sees it. A gate
that ran against a connection whose binder died mid-query (partialError
cause chain carrying SERVICE_DISCONNECTED / SERVICE_TIMEOUT) left that
connection installed, and every later purchase check kept talking to the
corpse until something else tore it down.

processReconciliation() already compensated for the refresh()/connect-loop
paths; the strict path had no such call. Extract that dead-connection block
into invalidateOnDeadConnection() and call it from both. The strict path
deliberately does NOT feed the episode clock — a gate the user aborted
mid-purchase is not a reconciliation outcome.
2026-08-16 17:44:02 +02:00
darken a357d77b00 test(upgrade): Cover pending purchases across the billing stack and screen
Ports the pending-purchase coverage alongside the production change.

- BillingConnectionTest / BillingManagerTest: PENDING ingestion, the
  PURCHASED-only entitlement exits, provesAbsence ignoring a surviving
  pending overlay, the reconciliation pass and the ack pass skipping
  pending purchases.
- UpgradeRepoGplayTest: pendingSkus never feeding isPro, the strict
  verify path and PendingPurchaseBillingException on an already-owned
  recovery.
- GplayUpgradeViewModelTest: the shared pre-purchase gate on both paths
  (pending, timeout, error, owned upgrade, renewing subscription with an
  unknown product), the pending-payment launch failure, and the pending
  card rendering while prices are still loading or have failed.
- GplayUpgradeScreenTest / GplayUpgradeOwnershipTest /
  GplayUpgradeScreenHostTest: the card for all three audiences, the
  locked offers and switch button, restore staying enabled, and the
  informational dialog reaching the composition.
2026-08-16 17:44:02 +02:00
darken abc82b0f09 fix(upgrade): Show supporter status on every upgrade screen route
The FOSS upgrade screen's view mapping only surfaced STATUS_UPGRADED on
the manage route. Forced routes (the Pro-locked settings entry)
deliberately don't auto-close, so a supporter completing the sponsor
flow from there stayed on the sales pitch with a live sponsor button --
reading as if sponsoring didn't work, with only the transient thanks
toast saying otherwise. The gplay flavour already renders ownership
state route-independently on this path.

The isPro branch now wins on every route, matching the adjacent
comment's stated intent and gplay's behaviour. Forced routes keep their
don't-auto-close semantics; the durable status view is what
acknowledges the upgrade.
2026-08-16 17:44:02 +02:00
darken 3e6541deec feat(upgrade): Explain a pending Google Play payment on the upgrade screen
A user whose payment Play is still processing now gets an explanation
instead of a screen that keeps selling them the upgrade they already
bought.

- New PendingPurchaseCard, rendered above the ownership/acquisition split
  so it reaches every audience: the acquisition buyer, the owner switching
  products, and the grace user (whose offers box is hidden entirely during
  a young episode) — all three have their purchase actions locked and need
  the same explanation.
- Loaded.hasPendingPurchase is SKU-agnostic: the subscription and the
  one-time purchase are alternatives, so a pending payment for either one
  disables both offers and the ownership switch button. Restore stays
  enabled — re-checking with Play is the useful action here.
- Both purchase paths now run the same pre-purchase gate
  (verifyPurchaseStateNow, bounded): a pending payment answers with the
  informational PurchasePending dialog instead of launching a flow Play
  would reject. The subscription path previously launched unverified, and
  its gate also blocks on an owned upgrade (RestoreSucceeded) and on a
  still-renewing subscription (SubscriptionStillRenewing), so a stale
  screen can't sell Pro to someone who already owns it.
- restorePurchase() reports a found-but-unpaid purchase as PurchasePending
  rather than RestoreFailed, whose copy asserts a completed check and
  steers toward multi-account troubleshooting and support.
- A PendingPurchaseBillingException from a launch maps to the same dialog.
- SubscriptionCheckFailed becomes PurchaseCheckFailed (both paths use it
  now); its string is replaced by
  upgrade_screen_purchase_check_failed_message and the translated entries
  of the old key are removed, along with the already-orphaned
  upgrade_screen_sub_check_failed_title, so ExtraTranslation lint stays
  quiet.
2026-08-16 17:44:02 +02:00
darken 33a972001e feat(upgrade): Carry pending Google Play purchases through the billing stack
Play reports a purchase as PENDING while a slow payment method (cash,
carrier billing, bank transfer) is still being processed. Until now
BillingConnection dropped those at ingestion, so the app had no idea the
user had bought anything: the upgrade screen kept selling, and a second
purchase attempt was rejected by Play with ITEM_ALREADY_OWNED.

Pending purchases now enter the reducer state and travel to the UI, while
every entitlement exit stays PURCHASED-only:

- BillingConnection ingests PURCHASED + PENDING (UNSPECIFIED_STATE is
  still dropped everywhere). The freshUpdates stream keeps receiving only
  PURCHASED, and provesAbsence now ignores a surviving PENDING overlay
  entry, so a payment in progress can't freeze the unconfirmed-episode
  clock.
- combinePurchaseResults gets the sku-type resolver: a PENDING result only
  suppresses the couldn't-verify error when it maps to a known Pro SKU. An
  unknown pending product proves nothing about the type whose query failed.
- PurchaseRefresh now carries provenance (confirmed set,
  hasConfirmedProPurchase, commit-time occurredAt, partialError) instead of
  just the merged view plus isComplete.
- BillingData splits into purchases (entitlement carrier) and
  pendingPurchases via a single from() classifier used at every exit.
- BillingManager gains processReconciliation(), run after the connect
  loop's initial refresh and by refresh(): it re-signals dead-binder
  invalidation and feeds the grace episode clock with the refresh's COMMIT
  time. The ack pass skips pending purchases, which Play rejects
  permanently and would report as a bug every pass.
- BillingConnection.querySubscriptions / BillingManager.querySubscriptions
  are replaced by refreshStrict(): the pre-purchase gate needs both product
  types and the pending state, and still fails closed on anything short of
  a complete round-trip.
- UpgradeRepoGplay exposes Info.pendingSkus (never part of isPro),
  Info.hasAutoRenewingSubscription, verifyPurchaseStateNow() for the gates,
  and reports PendingPurchaseBillingException when an already-owned
  recovery finds a pending payment. The grace branch now carries
  billingData through so pending stays visible while Pro runs on grace.
2026-08-16 17:44:02 +02:00
Matthias Urhahn fe67ce0365 Shrink issue screenshots uploaded as HTML img tags 2026-08-11 14:43:10 +02:00
darken 511944c21c fix(upgrade): Share the settings row title with the upgrade screen
settings_upgrade_status_label lived twice per flavor: once for the
Settings row, once as a raw locale-translated string standing in for
what the upgrade screen otherwise composes from the app name and the
tier qualifier. On gplay the two could drift apart -- 15 of 75
locales showed the wrong word order or an untranslated English
fallback where the composed title used the correct language. The row
now composes through the same brandTitle template gplay's own
upgrade screen title already used, so the two can no longer disagree.

FOSS's value was a support ask ("Sponsor CAPod"), not a composed
brand title, so its wording stays untouched -- the key is renamed to
upgrade_foss_sponsor_label (byte-identical text, all 75 locales) and
both FOSS call sites read that one resource instead.
2026-08-07 22:16:00 +02:00
d4rken-org-releaser[bot] 007282d3f5 Release: 5.2.2-rc1 2026-08-07 18:28:44 +00:00
darken 5ffdf66b9d Fix release builds failing on a missing Play Core annotation
R8 reported com.google.android.gms.common.annotation.NoNullnessRewrite as missing; it is a compile-time-only annotation referenced by review-ktx and absent from the runtime classpath. Adds the same targeted -dontwarn SD Maid SE already uses.
2026-08-07 19:30:15 +02:00
d4rken-org-releaser[bot] 7e90d115e0 Release: 5.2.2-rc0 2026-08-07 16:58:23 +00:00
darken ffc3ffb718 test(upgrade): Sweep locales in both flavours now the template is shared
The template moved to main, so FOSS resolves per-language arrangements
too and composes reordered titles for the first time. Running the sweep
only for gplay would leave that path unguarded.
2026-08-07 18:27:33 +02:00
darken 6b358cb101 refactor(upgrade): Move the title template out of the flavour source sets
Arrangement is a property of the language, not the flavour, so the key
now lives once in main alongside app_name. The qualifier stays
flavour-specific: only the word order and punctuation move.

FOSS consequently inherits each language's arrangement instead of being
frozen at the default order.
2026-08-07 18:27:33 +02:00
darken 7cbcde50d3 General: Update app translations from Crowdin 2026-08-07 18:27:33 +02:00
darken 5365a5da29 chore(upgrade): Add the title template to main ahead of the flavour move
Phase 1 of the Crowdin migration: the flavour copies still override this
for their own builds, so behaviour is unchanged while translations are
gathered against the new source string.
2026-08-07 18:27:33 +02:00
darken 908acc4ea0 test(upgrade): Assert the splice honoured the template, not just its output
The locale sweep checked format specifiers with a regex that does not know
%<s, which reuses the previous argument and so emits the qualifier twice.
That damages the template, triggers the fallback, and still satisfies every
output assertion because a fallback title also carries one correctly styled
qualifier. Both checks now run against the formatter's own output.

Also pins the highlight colour: it is a parameter because the toolbar tints
FOSS and Pro differently, so hardcoding it back would have stayed green.
2026-08-07 16:51:44 +02:00
darken 5a635b424b fix(upgrade): Retire the composed app_name_pro and app_name_foss keys
Both are now derived: the title is composed from app_name and the
flavour's upgrade_badge_label through app_name_upgraded_template, so the
pre-composed strings have no remaining reader.
2026-08-07 16:51:44 +02:00
darken f1e4ffb099 General: Update app translations from Crowdin 2026-08-07 16:51:44 +02:00
darken d012799d49 fix(upgrade): Compose the Pro title from a translatable template
The composed app_name_pro was split on spaces and only styled when it
produced exactly two tokens. Arabic has four (kabud en-dash two-word
qualifier) and lost its branding entirely; Estonian puts the qualifier
first and so passed the guard while highlighting the brand instead.

Titles are now built from a per-flavour app_name_upgraded_template with
the app name and the tier qualifier as placeholders, so translators own
word order and punctuation.
2026-08-07 16:51:44 +02:00
darken 06fd256889 fix(upgrade): Show a failed error-dialog fix action inline instead of a toast
The Google Play launch-failure message was shown via Toast, which Android caps
at 2 lines: English lost the trailing "device.", French was cut mid-word and
lost an entire condition. The strings are fine, the container was wrong.

The fix action now rethrows after logging, the failure reaches the dialog, and
the dialog renders the message inline while staying open. The dismiss button
stays available, so the dialog is never latched.

The message is passed per dispatch rather than read from the LocalizedError, so
no future action button can surface the fix action's failure copy. The inline
state is keyed on the throwable, not the LocalizedError, which is rebuilt with
fresh action lambdas on every recomposition.
2026-08-07 09:49:57 +02:00
darken 2f715ccac9 fix(upgrade): Report empty Play product results as a merchandising error
When both the IAP and SUB queries came back with nothing, the screen always
reported a connectivity failure, telling users to clear Play's cache and reboot.
Play can answer OK and simply have no sellable offer (region, account
eligibility, pulled product), where that advice is futile.

Both causes are now inspected: only when BOTH are OfferUnavailableBillingException
does the merchandising copy surface. A single non-merchandising failure can't
rule out a real Play problem, so the conservative copy stays.
2026-08-07 09:49:57 +02:00
darken 4dba593c13 General: Update app translations from Crowdin 2026-08-06 13:07:28 +02:00
darken ed5ec8152b General: Update app translations from Crowdin 2026-08-06 13:07:28 +02:00
darken 11232e9169 fix(overview): Reset the review card latch when it leaves the list
The card is a keyed item in the overview's lazy list, which restores the
saveable state of removed items when they come back. A review tap left
the dismiss action permanently disabled once a higher priority card took
the slot and gave it back, and a dismissed card returned fully dead.

Fixes review finding F1.
2026-08-06 09:55:09 +02:00
darken 19976c89f6 test(review): Cover the probe quirks and the card latch
Adds timing, caching and boundary coverage for the review tool plus the
card's latch matrix. The existing both-actions card test is superseded:
its dismiss-then-review sequence is exactly what the latch blocks.
2026-08-06 09:55:09 +02:00
darken 323b10224c fix(overview): Latch the review card's actions after a tap
The card stays up until the next state emission, so a dismiss after a
review would overwrite the review bookkeeping with a snooze and a review
after a dismiss would re-open what was just closed. Repeated review taps
stay allowed so a failed Play request can still be retried.
2026-08-06 09:55:09 +02:00
darken 39b8fcb8f8 fix(review): Harden the Play review probe and tap path
Timeouts on all three Play calls, a dismiss generation backstop for the
tap race, and a 3-state probe verdict that caches Play's definitive
answers for the process, retries transient failures on a bounded budget,
and re-evaluates eligibility at the snooze and pro-grace boundaries.
2026-08-06 09:55:09 +02:00
darken 5ef3965a0d General: Update app translations from Crowdin 2026-08-05 16:42:36 +02:00
darken 1e1f0b13c6 fix(overview): Keep the review prompt from crashing or stacking
Corrupt review settings terminated the shared state flow on AppScope,
so the exception crashed the process instead of reaching the ViewModel's
catch. Absorb it upstream of both replayingShare calls.

The review card also no longer stacks on top of the enable-Bluetooth
prompt.

Fixes review findings F1, F2
2026-08-05 16:27:13 +02:00
darken 7716221053 feat(overview): Cover the review prompt with unit tests
Pins the Play review tool's eligibility gate, probe retries, single-flight
guard and cancellation handling, the DataStore round trip of the review
timestamps, the overview's card priority gate and the card itself.
2026-08-05 16:27:13 +02:00
darken 26707dc0e5 feat(overview): Ask happy users for a Play review
Adds a review prompt card to the overview. On Google Play it uses the
in-app review flow, gated on the user having been Pro for a while, not
having dismissed it recently and not having reviewed yet. FOSS gets a
no-op implementation.

The card is the lowest priority item on the overview and stays hidden
while a permission, troubleshooter, background-monitoring-off or
no-profiles card is on screen.
2026-08-05 16:27:13 +02:00
Matthias Urhahn e8b7f73c6b Add shared screenshot thumbnail workflow (#666)
Shrinks oversized screenshots posted in issues and comments into clickable
thumbnails. Implementation shared from d4rken-org/.github; this stub only
supplies the triggers, because workflow_call cannot be driven by issue_comment
directly. Pinned by commit SHA rather than a moving tag.
2026-08-05 12:59:40 +02:00
darken 442f6bcc88 fix: Correct the Google Play launch-failure toast
The toast claimed Google Play was not installed for every failed launch,
but the SecurityException path means Play is installed and merely disabled
or profile-restricted - telling those users to install an app they already
have. Neutral wording covers both cases; the resource key is unchanged (no
translations exist yet).
2026-08-05 11:26:02 +02:00
darken e7cb53f0b8 fix: Harden the error dialog's Google Play fix action
The dialog's fix dispatch ran unguarded: a throwing action crashed the UI
thread from inside a click handler and skipped onDismiss(), leaving the
dialog latched on the current error. The dispatch is now wrapped in
try/catch with onDismiss() in a finally block.

Google Play fix action:
- Drop FLAG_ACTIVITY_NEW_TASK. The action runs on an activity context, so
  the flag only detached Play's app info from the caller's task and back
  stack.
- Catch SecurityException next to ActivityNotFoundException: Play can be
  installed but blocked (disabled app, restricted profile, guarding ROM),
  which denies the launch instead of failing to resolve it.
- The fallback toast is now a translatable string resource instead of a
  hardcoded literal.

New coverage: ComposeErrorDialogGuardTest pins that a throwing fix action
still dismisses the dialog (shared source set, so both flavors run it),
GplayFixActionTest pins the denied and unresolvable launches showing a
toast instead of crashing, and ComposeErrorDialogTest now asserts the
launch intent carries no NEW_TASK flag.
2026-08-04 22:01:26 +02:00
darken ed940b3b3d fix(debug): Stop a failed start from erasing a resumed recording
Two rollback defects found reviewing the failed-start handling, both cases
where cleaning up after a start that could not finish damaged something it
did not own:

- FileLogger.start() deletes the log file only when that same call created
  it, and reports the failure instead of swallowing it. A resumed session
  appends to the previous recording's core.log, and a failed append used to
  erase it while telling the recorder the start had succeeded. The writer is
  published only once it is usable, so a failed attempt leaves nothing behind
  that would make a later start() a no-op (F3).
- The module's rollback skips self-suppression: a recorder broken in one way
  throws the same instance on the start line and again when the rollback
  stops it, and addSuppressed(self) raises IllegalArgumentException — which
  aborted the rollback before the failure state was committed and took the
  shared state collector with it (F4).

Recorder.start() is the only production caller of FileLogger.start(), and it
runs inside the module's whole-branch guard, so the new throw lands in the
rollback rather than escaping.

Fixes review findings F3, F4
2026-08-04 18:26:47 +02:00
darken c696e523a9 fix(debug): Hold off orphan zipping while a start is in flight
A start commits its session dir into the recorder state only once the
recorder is live. For the whole window before that, shouldRecord is set
but isRecording is not, so a scan sees a directory with a non-empty
core.log and no sibling zip - an orphan - and the manager's auto-zip
compresses the directory the recorder is writing into. When the start
then fails, the rollback deletion races that zipper, and the archive
left behind defeats the collision check in createSessionDir: the retry
reuses the session ID of the attempt that just died.

The scan and the recorder state it was taken against now travel as one
value, so the reconciliation can tell a pending start apart from a
settled one and defer new zips until the state is terminal. Running
zips are untouched. A sibling '.zip' or '.zip.tmp' now counts as a name
collision as well, since the session ID is derived from that name.

Fixes review finding F1.
2026-08-04 18:26:47 +02:00
darken f9327930b5 fix(debug): Keep the recorder usable when a recording fails to start
Starting a recording spans several steps — create the session directory,
start the recorder, persist the trigger file, write the header — and only
the last of them commits the recorder into the module's state. Anything
throwing inside that window escaped the reactive collector, which then
died for the rest of the process: the started recorder kept writing where
nothing could stop it, the trigger file survived to re-attempt the dead
session on every launch, and startRecorder() waited forever for a state
nobody would publish. The debug log toggle stayed dead until reinstall.

The whole start branch is now guarded. A failure rolls back first — stop
the recorder, clear the log dir mirror, remove the trigger file, delete a
session dir this attempt created — and only then decides what the failure
means: our own scope dying still takes the collector with it, anything
else (a cancellation from inside the start work included) is committed as
a start failure and surfaced to the caller. shouldRecord is reset with it,
so the every-state collector lands in the idle branch instead of retrying.

The stop branch gets the same treatment: a recorder that cannot stop is
logged and the cleared state committed anyway, so an awaiting stop
completes. Recorder.stop() itself now guarantees logger removal, writer
closure and reference clearing. Session directory names get a collision
suffix, since a same-second retry would otherwise share a directory with
the attempt it replaces, and the public start/stop entry points are
serialized so two callers cannot race the same transition.
2026-08-04 18:26:47 +02:00
darken e7ee126f7e refactor(upgrade): Drop the superseded upgrade preamble card
The hero card took over both call sites (FOSS pitch, GPLAY acquisition), so
the standalone preamble card had no callers left.

Fixes review finding F1.
2026-08-03 20:28:01 +02:00
darken ed5e5ba7e1 fix(upgrade): Record the last known entitlement upstream of the flatMapLatest buffer
The tracking onEach sat downstream of flatMapLatest, so its channel buffer
could hold the Pro emission while the inner flow already threw: the catch
then read a null last-known state and emitted a non-Pro error Info, kicking
a supporter back to the pitch. Tracking now runs in the same coroutine as
the throw.
2026-08-03 20:28:01 +02:00
darken 2dbe86ad5e test(error): Emit through the error flow in the dialog test
The fake source was asked to emit directly; SingleEventFlow's emitters live
on the errorEvents flow itself, the way the ViewModels use them.
2026-08-03 20:28:01 +02:00
darken 6fcc2c4348 fix(upgrade): Theme the upgrade retry button for its error card
The retry sits inside the errorContainer card but drew itself with the
default primary-on-surface outlined colors, which clashes with the card and
loses contrast once the tap latch disables it. Content and border now follow
onErrorContainer, with a dimmed disabled pair, and both states get a
preview.
2026-08-03 20:28:01 +02:00
darken 375af943ae fix(error): Let the error dialog dismiss instead of only acknowledging
LocalizedError can now carry a fix action, and the shared error dialog shows
it next to a Dismiss button when one is present; errors without a fix keep
today's OK-only shape. The Google Play billing-unavailable error uses it to
open Play's app info as a generic troubleshooting affordance.
2026-08-03 20:28:01 +02:00
darken 65f7cbe8b7 fix(upgrade): Color the brand inside the gplay upgrade pitch title
The acquisition top bar reused the widgets' flat upgrade label, so the Pro
postfix stayed uncolored while the owned/grace title highlighted it. The
title is now a gplay-only template that takes the composed brand as a
placeholder, spliced back in as the same styled AnnotatedString the status
title uses — word order stays the translation's business. The widget label
keeps its own key and its own wording.
2026-08-03 20:28:01 +02:00
darken 2833639892 feat(upgrade): Merge upgrade screen mascot and preamble into one hero card
Adds UpgradeHeroCard, which pairs the mascot with the preamble copy inside a
single ElevatedCard and stacks them once the copy runs out of room. Used by
the FOSS pitch view and the GPLAY acquisition view; grace episodes and the
FOSS status views keep their standalone header, which has no preamble to
pair with. Screen tests pin the hero's presence and absence per state.
2026-08-03 20:28:01 +02:00
darken 495f58c23b fix(upgrade): Align the round-4 restore, recorder sentinel and contention test
Restore the consumed sponsor marker only when no newer launch was armed
meanwhile. Make the recorder's monotonic base a nullable sentinel so a
boot-adjacent elapsedRealtime of 0 is not mistaken for a resumed session,
and clear it on stop. Pin the contention test's collector subscription and
assert it actually echoed.
2026-08-03 20:28:01 +02:00
darken 9b60ed945f fix(upgrade): Settle FOSS entitlement errors instead of hanging
A thrown cache read used to die inside shareIn's sharing coroutine, leaving
every collector waiting forever. Catch inside flatMapLatest, keep the last
known entitlement on late failures, and let a successful persist revive an
error-stuck inner flow.
2026-08-03 20:28:01 +02:00
darken b77f4a9581 fix(flow): Subscribe before emitting in DynamicStateFlow.updateBlocking
updateBlocking emitted its update into updateActions FIRST and only then
subscribed internalFlow.first { it.updatedBy == update } against a
shareIn(replay = 1). That is a lost wakeup: if the producer processes our
update plus a successor before the first{} collector is registered, our
identity-matched State is displaced from the replay-1 cache and the await
never completes. A reactive collector makes the successor guaranteed rather
than unlikely - RecorderModule reacts to every state with an update of its
own, so each caller's update immediately breeds another.

The trigger is scheduling pressure: on a 2-core machine the producer and the
awaiting caller no longer run in parallel, so the emit-to-subscribe window is
wide. The CI test jobs wedged at GitHub's 6h job timeout; reproduced locally
under taskset -c 0,1 and confirmed by jstack - the caller parked forever in
updateBlocking's first{} while the producer sat idle with the update long
since processed.

The awaiter is now started UNDISPATCHED before the emit, so it runs
synchronously up to its first suspension inside first's collect and the
collector exists on the shared flow before the update can be processed.

Covered by a contention test that mirrors the module interaction (two
concurrent updateBlocking workers plus a value-neutral reactive echo
collector), and the two recorder test harnesses now wrap their block and
their cleanup stop in timeouts - a regressed await has to fail in seconds
instead of wedging a runner for six hours.
2026-08-03 08:30:33 +02:00
darken 91dafe8a04 test(debug): Stop leaked recorders in the recorder-module test harness
The realtime harness cancelled its module scope but never stopped the
recorder, and cancelling a scope does not uninstall a running recorder's
globally installed FileLogger. A test that started a recording therefore left
one writing into every test that followed, and an assertion failing before the
explicit stop did the same.

The harness now stops the module in a nested finally and fails its own test if
a file logger survived, removing the straggler afterwards so a single leak
cannot cascade. The tracked-recording test gets the same finally treatment.
2026-08-03 08:30:33 +02:00
darken 1dfed1c21c fix(debug): Make the short-recording warning clock-change safe and raise it to 10s
The "that recording looks very short" prompt measured duration against the
wall clock, so any adjustment mid-recording decided it: an NTP sync or a
manual clock change moving forward made a three-second recording look like an
hour and skipped the prompt, moving backward trapped a long recording in it
with no way past but "stop anyway".

A live session now measures from a monotonic base taken at the start. Only a
session resumed from the trigger file still uses the persisted wall time -
that file has to survive reboots, which monotonic time does not - and a
negative duration there fails open instead of warning.

The threshold moves from 5s to 10s: a recording stopped that quickly holds
nothing but the recorder starting and stopping, which costs a support
round-trip to re-request. It stays a prompt, not a block, because a crash is
logged and flushed immediately.
2026-08-03 08:30:33 +02:00
darken 26633b940d fix(upgrade): Make the FOSS supporter persist create-only-if-absent
persistUpgrade() wrote an unconditional record, so any sponsor return that
got past the ViewModel's isPro guard replaced an existing supporter's
upgradedAt - the "supporter since" date the status screen shows - and, for
the legacy records every existing supporter has, their stored reason too.
That guard reads a shareIn replay and can be stale, so it is not a race-free
answer on its own.

The write now happens inside the store transaction: an existing record is
kept and reported back, only an absent one is created. The ViewModel thanks
the user only for an actual unlock and stays quiet otherwise. It also no
longer eats the sponsor visit when something fails - a failed entitlement
read or a failed write restores the pending-launch marker so the next return
can retry, while the error still travels the normal path.

FossCache gains the same constructor test seam BillingCache has, so the
transaction can be exercised against a real DataStore on a temp file.
2026-08-03 08:30:33 +02:00
darken d98c118f37 test(overview): Cover the background-monitoring-off signal
Adds monitoringStatus decision tests to OverviewViewModelTest plus Compose
tests for the new dashboard card and the missing-paired-device banner.
2026-08-02 14:51:43 +02:00
darken 8f0ec5a374 fix(overview): Signal when background monitoring is off
An auto-created profile without a paired Bluetooth device resolves to MANUAL
mode, so nothing runs in the background while the dashboard claimed to be
monitoring. The dashboard now states that background monitoring is off and
offers to pick a paired device, and the per-card banner names what a missing
paired device costs.

Closes #658
2026-08-02 14:51:43 +02:00
darken 53b4150ff6 fix(ui): Stop translating the flavor brand labels
"FOSS" is the flavor's name, not prose. Locale copies of the FOSS badge
and of the composed "CAPod FOSS" title had drifted - some translated the
word, one Thai entry into an unrelated word entirely - all of it shown as
the app's own brand.

Both base entries are now translatable="false" and every locale copy is
gone; with the flag set, any remaining locale entry would trip a fatal
ExtraTranslation in the release lint. The GPlay "Pro" badge is prose and
stays translated.
2026-08-02 12:40:32 +02:00
darken 6b2536c74f fix(debug): Keep debug recording available when diagnostics hang
The debug log header read the flavor's upgrade diagnostics unbounded. A
wedged source (a stuck DataStore file lock, a billing store that never
answers) left the recorder started but never committed, so the user asking
for a log got nothing at exactly the moment the app was misbehaving.

The read now runs under a deadline: a source that hangs or fails degrades
to "unavailable" and the recording starts. Completion is tracked
separately from the value, so a flavor that legitimately has nothing to
report (FOSS) still logs no line at all instead of claiming a failure.
Cancellation is unchanged: an outer scope death still rolls the
uncommitted recorder back.

The GPlay diagnostics' pro-history read gets the same bound its billing
cache read already had.
2026-08-02 12:40:32 +02:00
darken 72fb1d3b17 fix(upgrade): Never let a failed cache stamp abort entitlement bookkeeping
stampLastProState() was bounded against a wedged file lock, but a write
that failed outright (corrupt preferences file, no disk space) still threw
straight through into the entitlement path it only decorates.

Non-cancellation exceptions from the edit now log a warning and skip the
stamp, same as the timeout does. Cancellation keeps propagating - caught
first on purpose, swallowing it would break the caller's structured
concurrency. Reads stay loud: a snapshot that couldn't be read must not
be mistaken for a never-bought install.
2026-08-02 12:40:32 +02:00
darken 31c3b47bcd fix(upgrade): Only count a sponsor visit when the page actually opened
The sponsor unlock heuristic armed itself on every tap, regardless of
whether a browser ever opened the page. An unrelated later background
round-trip could then hand out supporter status with no page ever shown.

WebpageTool.open() now reports whether an activity was actually started
and the FOSS repo passes that through synchronously, so the ViewModel can
only arm after a successful launch. A second tap while a launch is still
pending is ignored, and the upgraded status view's donate button gets its
own unarmed entry point - an existing supporter has nothing left to
unlock, and re-persisting would rewrite their "supporter since" date.
2026-08-02 12:40:32 +02:00
Matthias Urhahn 7cb31ce783 Merge pull request #656 from d4rken-org/worktree-fix-unknown-notification
Fix: Ongoing notification stuck showing "Unknown"
2026-07-30 14:30:33 +02:00
darken 2f69090bc2 fix(monitor): Invalidate the notification cache at session launch 2026-07-30 13:23:49 +02:00
darken 0c39ca89c8 fix(upgrade): Show the supporter-since date on the FOSS status screen
The upgraded status now renders the date the supporter unlocked, derived in the
same emission as the view so the screen never shows the status without the date
it is supposed to carry.

Guards that date: a return from the recurring-donation button no longer runs
persistUpgrade() when the install is already Pro, which would have rewritten
upgradedAt and visibly reset the displayed date. The sponsor-return tracker is
seeded from the handle-backed pending launch so a process death while the
sponsor page is in front does not swallow the first return.

The status views are titled "CAPod FOSS" instead of "CAPod Pro" -- on FOSS the
flavor name is the brand.
2026-07-30 12:55:31 +02:00
darken 10317b373f fix(upgrade): Polish the GPlay offers-unavailable card
The card reports that PRICES could not be loaded, so it now says so instead
of borrowing the generic "Google Play services are unavailable" title, which
contradicted its own body.

The retry latches after the first tap: the guard sits inside onClick because
`enabled` only takes effect after recomposition, so two taps in the same frame
would both fire. It resets naturally when the card leaves composition.

Returning to the screen re-runs the SKU query when it is in the unavailable
state. MainActivity's per-resume refresh only covers the entitlement, so a
transient Play outage left the retry card up until it was tapped by hand.
2026-07-30 12:55:31 +02:00
darken 7fb1f7aabd fix(debug): Bound the billing cache and fold pro history into diagnostics
BillingCache reads and writes are now bounded by a timeout seam: a wedged
DataStore file lock made the debug-log header hang, and a silent fallback to
the default snapshot would have reported "never bought" for an install whose
evidence merely could not be read. Reads now fail loudly, writes fail soft.

UpgradeDiagnosticsGplay absorbs the pro-state history that the recorder header
used to read directly, with a separate failure boundary per source so one
broken DataStore cannot suppress the other's evidence.

RecorderModule's start-failure guard now covers ordinary exceptions, not just
cancellation, stops the uncommitted recorder under NonCancellable and appears
once instead of per resume branch.
2026-07-30 12:55:31 +02:00
darken 54d3c9d824 fix(monitor): Stop re-promoting a stale notification into a new session 2026-07-30 12:47:23 +02:00
darken b616696a41 fix(monitor): Retract the stale ongoing notification on teardown 2026-07-29 20:09:12 +02:00
darken e364a5b02c fix(upgrade): Cover widget entry refresh and recorder edge cases
- WidgetConfigurationActivity refreshes the entitlement on resume: it is a
  second launcher entry point and can't rely on MainActivity reconciling.
- The upgrade-return callback re-asks decideConfirm() instead of trusting the
  upgrade activity's result code, so RESULT_OK stays entitlement-gated.
- RecorderModule stops the freshly started recorder when the header's
  diagnostics reads are cancelled, instead of leaking an untracked recording.
- FOSS beta channel points at the GitHub releases page; the Play testing URL
  is signature-incompatible for FOSS builds.
- Billing bug reports carry the contextual wrapper again, so the report is
  grouped by call site instead of the raw billing exception.

Fixes review findings F1, F2, F3, F4, F5.
2026-07-29 14:05:26 +02:00
darken 3651bb3d55 refactor(upgrade): Converge GPlay billing on the canonical stack
Replaces capod's older billing core, upgrade UI and their tests with the
canonical sdmaid-se stack at the pinned revision.

Core (gplay): BillingManager/BillingConnection/BillingConnectionProvider on
billing 8.3 with the centralized connect loop, merging purchases-listener
overlay and the canonical ack pipeline; the dying ack collector, the
ackedTokens gate and the in-billing foreground loop are gone. Full canonical
exception set (internal/network/offer-unavailable added), OurSku with capod's
product ids, BillingCache with snapshot()/episode-guarded stampLastProState.

FOSS: UpgradeControlFoss becomes UpgradeRepoFoss and exposes the canonical API
surface over capod's RETAINED FossUpgrade/FossCache schema — existing supporter
records must keep decoding.

Diagnostics: UpgradeDiagnostics + gplay/foss implementations, read by
RecorderModule next to CurriculumVitae's Pro history as two independent,
isolated header reads.

UI: canonical upgrade screens for both flavors under common/upgrade/ui with
capod chrome (M3 AlertDialog keeping rotation-safety, capod Scaffold, capod
previews). Nav.Main.Upgrade gains `forced`. Entitlement refresh moves to a
per-resume, unthrottled MainActivity call.

Strings reuse capod's existing translated ids wherever equivalent; only
referenced-but-missing ones are authored.

mockk 1.12.4 -> 1.14.9: 1.12.4 cannot synthesize a sealed-class return value
while recording, which the ported restore tests need.
2026-07-29 14:05:26 +02:00
darken 0192ae7081 feat(core): Add Pro-state history and safe state collection helpers
Additive infrastructure for the canonical billing port, no coupling to the
billing core yet.

- CurriculumVitae: Pro-state slice only (ProState, ProHistory,
  updateProState, proHistory, transition classification, tolerant enum
  decode). Raw preference keys so a transition updates state, counter and
  timestamp in one DataStore transaction.
- ViewModel4.safeStateIn: render-state flows forward recoverable failures
  to errorEvents and emit an explicit fallback state instead of throwing
  into collectAsStateWithLifecycle().
- testhelpers: TestApplication, BaseComposeRobolectricTest and the
  mockDataStoreValue helper.
2026-07-29 14:05:26 +02:00
darken 8c1b57a47c refactor(upgrade): Adopt canonical entitlement interface and gates
UpgradeRepo gains the canonical shape: settledness rides each Info
emission, plus storeSite/upgradeSite/betaSite and a suspend refresh().
getSponsorUrl() is replaced by upgradeSite (FOSS only, GPlay keeps the
heart icon hidden). UpgradeRepoExtensions is the canonical file with
isPro/isProSettled/isProForUi.

UpgradeRepoGplay folds its parallel isSettled flow into Info.isSettled
(behaviour preserving) and implements refresh() as a bounded, unthrottled
call to the existing billing refresh. UpgradeControlFoss is settled from
its first emission and no-ops refresh().

Interactive gates move to isProForUi so a paying user isn't bounced to
the upgrade screen during the GPlay cold-start race: the device-settings
and press-controls pro gates, the theme setters, and the widget confirm
action, which now goes through a sealed ConfirmOutcome so the activity
can only return RESULT_OK for an entitled, valid configuration.
Presentation paths that can't reach a suspending gate (general settings
theme items, overview device limit) render the upgrade branch only when
the entitlement is hard-locked: settled, error-free and not pro.
2026-07-29 14:05:26 +02:00
darken fb48ff9d43 chore(claude): Enable google-play and devtools plugins 2026-07-29 13:11:55 +02:00
darken 2d604c1e6a fix(reaction): Serialize pause arming behind queued snapshots 2026-07-28 23:52:04 +02:00
darken 5e28eeed38 fix(reaction): Serialize media key dispatch and read playback snapshots
Two pre-existing concurrency defects in MediaControl (#647).

Lost update: sendPlay() wrote capPaused after the suspending sendKey(),
whose delay(100) is a window in which a concurrent
sendPause(rememberForResume = true) could arm the flag only to have it
overwritten. Stem presses run on the app scope while ear, sleep and
conversation reactions run on the monitor scope, so the senders really
do race. The compound check/dispatch/flag sequence now runs under a
Mutex, the flag is cleared before the first suspension, and the key
pair completes under NonCancellable so cancellation cannot strand an
unpaired DOWN event.

Coalescing blind spot: the playback callback ignored its configs
argument and read live isMusicActive, so queued deliveries all observed
the newest state and an inactive to active edge in between was never
seen, leaving capPaused stale. The edge is now derived from the
delivered snapshot. A pause that passes the live active check records
that observation so an already-queued music-start snapshot cannot drain
later and read as a fresh edge.
2026-07-28 23:52:04 +02:00
darken edd3cf0c2b ui(monitor): Give the early service notification a content text 2026-07-28 23:51:46 +02:00
darken 9f322f0a3f test(monitor): Cover foreground re-promotion and start-rejection logging 2026-07-28 23:51:46 +02:00
darken de287fcb66 fix(monitor): Log FGS start rejections distinctly with full stack 2026-07-28 23:51:46 +02:00
darken 28ad09e96d fix: Stop suppressing foreground service timing exceptions
Suppressing ForegroundServiceDidNotStartInTimeException and re-entering
Looper.loop() left zombie processes behind that kept collecting ANRs.
Always delegate to the previous handler instead.
2026-07-28 23:51:46 +02:00
darken 8f71af8c5d fix(monitor): Re-satisfy foreground obligation on every start command
Every startForegroundService() re-arms the 10s startForeground() deadline,
even when the service is already foreground. The service only promoted in
onCreate(), so repeated start requests could time out and ANR.
2026-07-28 23:51:46 +02:00
darken 305a6d6c2a docs(aap): Warn that 0x0001 closes the stream mid-session 2026-07-28 20:18:54 +02:00
darken 26b04854c4 chore(claude): Document AAP protocol landmines and add scratch dir 2026-07-28 20:18:54 +02:00
darken 7fc13ac2c5 chore(claude): Correct modifier-position claim in code style rule 2026-07-28 20:18:54 +02:00
darken 7c75ec02bd chore(claude): Port code-style, test gotchas, and PR labels from sdmaid-se 2026-07-28 20:18:54 +02:00
darken f39ea20690 chore(claude): Restore always-on release guardrails 2026-07-28 20:18:54 +02:00
darken b573a79582 chore(claude): Path-scope rules and align with Opus 5 guidance 2026-07-28 20:18:54 +02:00
darken 82cc9764e5 fix(logging): Install logger before Hilt injection
super.onCreate() triggers Hilt's singleton construction, so any log emitted
during that graph build was discarded before the logger existed.

Fixes review finding F2
2026-07-28 19:07:54 +02:00
darken c9a5db1ac8 test(mediacontrol): Name handler registration test after what it asserts
The test claimed to guard against a main-looper binding, but the
assertion only checks that the injected handler instance is forwarded.
Rename it and document that the Looper identity is covered by the
AndroidModule provider and the runtime thread-name QA check instead.

Fixes review finding F1
2026-07-28 19:07:54 +02:00
darken a95377cf7b fix(media): Move audio playback callback off the main thread
MediaControl registered its AudioPlaybackCallback with a null Handler,
binding delivery to the main looper. Both the callback body and the
constructor's seed read call AudioManager.isMusicActive, a binder
transaction into AudioService, producing two ANR clusters: one in
onPlaybackConfigChanged and one in <init>, the latter on the cold-start
critical path since MediaControl is constructed during App.onCreate.

Registration and seeding now run on a dedicated, injected Handler backed
by a "CAPod-MediaControl" HandlerThread, and the callback is delivered on
that same looper. Registration happens before seeding so a transition
during registration is queued behind the seed instead of being lost.

The handler is constructor-injected via a new @AudioCallbackHandler
qualifier so unit tests can drive it without Robolectric.
2026-07-28 19:07:54 +02:00
darken 80ea6fbd68 General: Update app translations from Crowdin 2026-07-28 15:53:54 +02:00
darken c08c6f1129 refactor(strings): Move flavor-specific upgrade-status strings into flavor source sets 2026-07-24 17:53:19 +02:00
darken 73dc10aee9 fix(ui): Draw all screens edge-to-edge under system bars
Scrolling content now slides under the transparent status and
navigation bars instead of clipping at the inset boundary. Adds
PaddingValues.plus and systemBarsAndCutoutInsets helpers, moves inset
consumption from scroll viewports into content padding on every screen,
fixes reorder auto-scroll thresholds for content padding, adds IME
handling to form screens, and removes the unused EdgeToEdgeHelper.
2026-07-24 17:53:13 +02:00
Matthias Urhahn 37278af34e Merge pull request #644 from d4rken-org/fix/upgrade-offercard-flash
Fix: Prevent red flash when opening the Pro upgrade screen
2026-07-24 15:15:02 +02:00
darken c822569a63 fix(upgrade): Avoid red unavailable flash during billing warm-up
On entry upgradeInfo looks like a non-owner until Play reconciles, so if the SKU query resolves first the offers box briefly rendered the red "unavailable" card before the owner/grace status or prices appeared. Gate that card behind settled and !skuQueryInProgress and show a neutral spinner during warm-up.

Also label the restore-failed dialog's dismiss button "Close" instead of "Cancel" — it reports a result, it doesn't ask to abort an action.
2026-07-24 14:42:58 +02:00
Matthias Urhahn eb41d84a0f Merge pull request #643 from d4rken-org/worktree-upgrade-offercard-ui
General: Redesign the Pro upgrade screen and restore flow
2026-07-24 14:32:20 +02:00
darken c072b90876 feat(upgrade): Adopt SD Maid offercard layout and restore UX
Reshape the Google Play upgrade screen into SD Maid SE's offercard layout:
purchase options as titled offer rows (name · price, terms, action) with an
"or" divider inside one action card, extracted into gplay-local UpgradeContent
/ UpgradeOffers / UpgradeOwnership / UpgradeRestore primitives. Keeps capod's
icon benefits card, splash graphic, and floating back arrow.

Restore now mirrors SD Maid: a reusable restore section (emphasized for
returning buyers), verification-gated across all surfaces, and a restore-failed
dialog that leads with the live Play check and offers Contact support.

Billing logic is unchanged apart from onContactSupport() navigating to the
contact form. Offer rows render conditionally on offer availability; the offers
box AnimatedContent keys on an availability phase so same-state updates
recompose in place.
2026-07-24 14:15:12 +02:00
Matthias Urhahn 6175bd3bb1 Merge pull request #642 from d4rken-org/worktree-billing-sdmaid-backport
General: Make Pro purchases more reliable when Google Play is flaky
2026-07-23 13:25:59 +02:00
darken 36c54d5d19 fix(upgrade): Harden billing storage, restore races, and offer retry 2026-07-23 13:01:14 +02:00
Matthias Urhahn f629f05f23 Merge pull request #641 from d4rken-org/crowdin-update-20260722
General: Update translations from Crowdin
2026-07-22 22:21:35 +02:00
Matthias Urhahn 2e969d3439 Merge pull request #640 from d4rken-org/worktree-flavor-string-placement
General: Tidy up FOSS and Play Store specific upgrade texts
2026-07-22 22:21:23 +02:00
darken 809305c4bd General: Update app translations from Crowdin 2026-07-22 21:14:42 +02:00
darken 3398da68ba General: Update fastlane translations from Crowdin 2026-07-22 20:56:07 +02:00
darken 061aa21c85 refactor(l10n): Move flavor-specific upgrade strings to source sets 2026-07-22 17:09:46 +02:00
darken 28b5b85be2 ui(overview): Redesign unmatched devices card with icon and description 2026-07-22 16:32:27 +02:00
Matthias Urhahn 825892df74 General: See your Pro status and switch from subscription to one-time purchase (#638)
* feat(upgrade): Add Pro status view, grace UI and sub-to-IAP switch

* fix(upgrade): Pad restore purchase to a minimum visible duration

* ui(upgrade): Mention Play-website install fix in restore troubleshooting

* fix(upgrade): Stop re-acknowledging already-acked purchases

* ui(settings): Move upgrade status row into the Other category
2026-07-22 15:40:57 +02:00
Matthias Urhahn bfc0e1cfaa test(upgrade): Cover PENDING purchases granting no Pro and no ack 2026-07-14 17:27:36 +02:00
Matthias Urhahn f4facd3e86 fix(upgrade): Reconnect billing instantly on user actions 2026-07-14 16:43:58 +02:00
Matthias Urhahn c8179c5c19 fix(upgrade): Cut report noise, pace retries and fix trial wording 2026-07-11 21:32:06 +02:00
Matthias Urhahn bd36c5e5d6 fix(upgrade): Recover billing from stale purchase data and mid-flow errors 2026-07-11 12:15:17 +02:00
Matthias Urhahn 778d45170f feat(upgrade): Re-check purchases when the app comes to the foreground 2026-07-11 12:10:08 +02:00
Matthias Urhahn b52c2cbe6f feat(upgrade): Show restore banner and progress for returning Pro buyers 2026-07-11 12:05:06 +02:00
Matthias Urhahn 69febaac28 feat(upgrade): Keep one-time Pro buyers Pro through longer Play outages 2026-07-11 12:00:00 +02:00
Matthias Urhahn 2f1cbe34e5 fix(upgrade): Surface Google Play errors when a purchase can't start 2026-07-11 11:51:54 +02:00
Matthias Urhahn 8dcc4f8489 fix(upgrade): Harden purchase restore and billing error handling 2026-07-10 22:28:30 +02:00
d4rken-org-releaser[bot] 3ddd5f3cfd Release: 5.2.1-rc0 2026-07-08 11:21:18 +00:00
Matthias Urhahn 31f4d4aafa fix(battery): Stop remaining-time jumping up when toggling ANC
A mode's own drain-rate bucket starts empty until it accumulates history,
so toggling ANC into an unlearned mode fell straight through to Apple's
optimistic spec rating while the mode just left showed its worse measured
rate. Result: enabling ANC could make the displayed time jump up ~1h.

Fill an empty ANC bucket at display time with the less-optimistic of the
mode-agnostic UNKNOWN reading and a sibling mode's learned rate, scaled by
the ratio of the two modes' rated drain. Scoped to spec'd models and
device-supported modes; picks the best-evidenced sibling, tie-broken by
closest rated drain then recency. No persistence or UI change.

The existing spec ceiling and display clamp still backstop the borrowed rate.
2026-07-07 22:23:01 +02:00
Matthias Urhahn e06dc933b8 chore(logging): Log BLE scan reception nanos in scan summaries 2026-07-06 12:42:10 +02:00
Matthias Urhahn 1418be999d feat(reaction): Time-cap auto-pause debounce on slow BLE scanners 2026-07-06 12:42:10 +02:00
467 changed files with 32192 additions and 4230 deletions
+1
View File
@@ -1,2 +1,3 @@
worktrees/
tmp/
scheduled_tasks.lock
+36 -10
View File
@@ -29,18 +29,44 @@ Quick build check: `./gradlew assembleFossDebug`
- Use `assembleFossDebug` as the fastest build variant for iteration
- Follow existing patterns — the codebase uses MVVM + Hilt + Coroutines
- Always use string resources for user-facing text (see localization rules)
- Always use string resources for user-facing text
- Check `git log --oneline -20` for commit message style before committing
- Ordinary unit tests use JUnit 5 + kotest assertions + mockk and extend `testhelpers.BaseTest` — not
the Android defaults. `testFossDebugUnitTest` does not run `testGplay` tests
- Changing a production screen that backs a `@PreviewTest` entry in `PlayStoreScreenshots.kt` means
regenerating the smoke screenshot set
## Rules Reference
Detailed guidelines are in `.claude/rules/`:
Always loaded:
- `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
- `release.md` Release workflow (`Release prepare` dispatch), inputs, channel mapping, rollback
| Rule | Covers |
|------|--------|
| `.claude/rules/architecture.md` | BLE vs AAP paths, `DeviceMonitor` merge boundary, FOSS pro gating |
| `.claude/rules/build-commands.md` | Gradle commands and what CI actually gates |
| `.claude/rules/commit-guidelines.md` | Commit message format and prefixes |
| `.claude/rules/pull-requests.md` | PR title and description conventions |
| `.claude/rules/agent-instructions.md` | Delegation limits and implementation scope |
| `.claude/rules/release.md` | Release guardrails — never hand-edit versions or tags |
Loaded on demand, when a matching file is read (`paths:` frontmatter):
| Rule | Loads for |
|------|-----------|
| `.claude/rules/architecture-aap-protocol.md` | `**/aap/**`, conversation reaction |
| `.claude/rules/code-style.md` | Kotlin/Compose sources in `main/`, `foss/`, `gplay/`, `debug/` |
| `.claude/rules/testing.md` | `app/src/test/`, `testFoss/`, `testGplay/` |
| `.claude/rules/localization.md` | `**/res/values/strings.xml` (base locale) |
| `.claude/rules/screenshots.md` | Screenshot composables, `screenshotTest/`, fastlane scripts |
Skills, invoked by name:
| Skill | Purpose |
|-------|---------|
| `/release` | Release workflow dispatch, inputs, channel mapping, rollback |
## Scratch space
`.claude/tmp/` is gitignored. Put plans, repro screenshots, throwaway scripts, and captured logs
there rather than in `/tmp` — they stay greppable and survive across sessions without being
committed.
+22 -29
View File
@@ -1,39 +1,32 @@
---
description: Instructions for Claude Code sub-agents and task delegation
globs:
- "**"
description: Sub-agent delegation limits and implementation scope for this project
---
# Agent Instructions
## Critical Thinking
## Delegation
- 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
Delegation adds coordination overhead and multiplies token cost, so it has to earn its place through
genuine independence and parallel speedup.
## Explore vs. Implement
- Delegate only for large, genuinely independent work that parallelizes — a wide multi-file
investigation across unrelated areas, for example
- Don't delegate what you'd finish yourself in a handful of tool calls
- Don't spawn a sub-agent to verify or double-check your own work
- If one sub-agent can do it, use one rather than several
- Sub-agents don't inherit your conversation — state the full task, the relevant paths, and
whether you want research only or research plus implementation
- `Explore` is the right type for read-only codebase investigation
- **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
Running Gradle through the build-runner agent is a separate standing rule in the user's global
CLAUDE.md; it is context isolation, not delegation, and this file does not restate it.
## Sub-Agent Delegation
## Implementation scope
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
- Follow existing patterns — match the code style and architecture already in use
- Change only what the task needs
- When behavior is unexpected, fix the root cause rather than working around it
- Don't create new files when editing an existing one would do
- Don't refactor surrounding code while fixing a bug
- Don't add comments or docs to code you didn't change
- Don't guess at file paths — use Glob/Grep
@@ -0,0 +1,67 @@
---
description: AAP protocol landmines and settled dead ends — what not to send, and what has already been proven impossible
paths:
- "app/src/main/java/**/aap/**"
- "app/src/main/java/**/monitor/core/aap/**"
- "app/src/main/java/**/reaction/core/conversation/**"
---
# AAP Protocol — Landmines and Dead Ends
This file holds only what the code doesn't already say. The Conversational Awareness status
taxonomy is KDoc'd on `ConversationAwarenessEvent`, the `0x4B` frame shapes are documented at the
decode site in `DefaultAapDeviceProfile`, and known control IDs are catalogued in `AapControlId`
read those, don't duplicate them here.
## Never send `0x0001` mid-session
`AapMessageType.CAPABILITIES_REQUEST` (`0x0001`) is a **handshake-phase opcode only**. Sending
`04 00 04 00 01 00` on an established session makes the device close the L2CAP stream
(`Stream closed by remote`), forcing a full reconnect. Verified experimentally on AirPods Pro 3
(fw `81.2675000075000000.6503`). The Stream State Info payload also differs between the original
connection (45B) and the forced reconnect (28B), suggesting device-side state loss.
The enum lists it with no warning, so it looks callable. It isn't.
This came up as a "cheap refresh settings" probe after writing `DYNAMIC_END_OF_CHARGE` (`0x3B`),
because the device doesn't echo that write in-session. **There is no read-setting primitive in AAP.**
Settings are push-only — on connect, on external change, or not at all. The supported pattern is
optimistic UI state + profile-learned persistence + let reconnects refresh.
## `0x37` does not use the Apple-bool encoding
Hearing Protection PPE (`0x37`, the EN 352 82 dBA media cap) is **Pro 3 only** and encodes as a
plain `01 = on` / `00 = off`. Every other AAP boolean uses the Apple-bool convention where false is
`0x02``encodeAppleBool` is wrong for this one. Companion `0x38` carries the cap level
(observed `0x52` = 82 dBA).
Hardware-confirmed reads on Pro 3 (A3064), 2026-06-10. A full settings flood on Pro 2 USB-C (A3048)
never contains `0x37` or `0x38`. Write-effect is **not** yet hardware-verified; it goes over the same
PSM `0x1001` channel CAPod already writes ANC and CA to, so the risk is low, but it is untested.
## Settled dead ends — do not re-investigate
**Real-time ambient dB level (#521) is not implementable.** AirPods send no dB or attenuation
telemetry over any AAP opcode or ATT characteristic. Apple's feature measures SPL with the Watch or
iPhone microphone and subtracts a *static per-model lookup table* held in the private
`HearingUtilities.framework`; AirPods contribute only their current listening mode. Established via
the iOS 26.1 decompile plus a sweep of librepods, apple-wireshark, and the tyalie AAP definitions.
`0x50` is PerfStats, `0x53` a PME config blob, `0x58` an Opus mic audio stream — none is a metric.
**Loud Sound Reduction (#520) has no non-root toggle.** LSR lives on a separate raw ATT channel, not
AAP: a second L2CAP socket to **PSM 31 (`0x001F`)**, handle `0x1B`, plain `0x01`/`0x00`. Connecting
and *reading* works without Apple vendor-ID spoofing. **Writes are silently ignored** — the pods
return a Write Response (`13`) and the immediate read-back is unchanged. Reproduced back-to-back on
Pixel 8 + Pro 2 USB-C, 2026-06-10. This matches librepods only exposing the toggle behind their
root/Xposed VID-spoofing hook. A functional toggle is root-only; a read-only status indicator is
feasible today.
Not to be confused with `0x37` above — different feature, different channel, and that one is a
normal writable AAP setting.
## Session exclusivity
The pods accept exactly **one AAP session**. Any debug activity that boots the app starts
`MonitorService`, which auto-connects and wins the socket — a proof-of-concept activity will connect
at the L2CAP layer and then receive nothing. Protocol experiments have to go through the monitor's
own session, i.e. the real feature write path.
+36 -98
View File
@@ -1,123 +1,61 @@
---
description: Architecture overview, module structure, key components, data flow, and dependencies
globs:
- "app/**/*.kt"
- "**/*.gradle.kts"
description: Load-bearing architectural invariants that are not obvious from reading the code
---
# Architecture
## Single-Module Structure
Invariants worth knowing before you touch device state, the AAP stack, or the upgrade flow. Class
inventories and source-set layout are omitted deliberately — read the tree for those.
One Gradle module: `app/`. Source sets:
## BLE vs AAP
- `main` — shared code (Compose UI, services, monitor, bluetooth, AAP protocol, widgets)
- `foss` / `gplay` — flavor-specific code (e.g. upgrade/billing implementations)
- `debug` — debug-only code including screenshot content composables
- `test` / `testFoss` / `testGplay` — unit tests
- `screenshotTest` — Compose Preview Screenshot tests for Play Store assets
A previous `app-common/` module was merged into `app/` (commit `be8f4919`).
## 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
### Device Monitoring
`monitor/core/` is split into two data-source siblings that `DeviceMonitor` merges:
- `monitor/core/ble/BlePodMonitor` — passive BLE scanning; reads Apple advertisement beacons (battery, case state, in-ear, etc.). Works for any pod in range; no pairing required
- `monitor/core/aap/` — AAP connection lifecycle layer on top of `AapConnectionManager`:
- `AapLifecycleManager` — starts/stops the AAP subsystem
- `AapAutoConnect` — auto-opens AAP sessions for bonded/known devices
- `AapKeyPersister`, `AapLearnedSettingsPersister` — persist session keys and learned pod settings across app restarts
- `StemConfigSender`, `StemPressReaction`, `AncGestureResolver` — push config and react to stem/HID events
- `monitor/core/cache/DeviceStateCache` — persisted last-known state so profiles still show data when a device is out of range
- `DeviceMonitor` — singleton that `combine`s `BlePodMonitor.devices + AapConnectionManager.allStates + DeviceStateCache + profiles` into unified `PodDevice` objects. ViewModels observe `DeviceMonitor.devices`; they do **not** reach into `BlePodMonitor` or the AAP layer directly
- `MonitorControl` / `MonitorService` — foreground service lifecycle holding the scan awake
- `BluetoothEventReceiver`, `BootCompletedReceiver` — system triggers that wake the service
**BLE vs AAP — what each path gives you:**
Two independent data paths. Which one a feature can use decides whether it is even possible.
| | BLE (advertisements) | AAP (L2CAP session) |
|---|---|---|
| Direction | Read-only, passive | Bidirectional commands + events |
| Prerequisite | Bluetooth on | Bonded + `BLUETOOTH_CONNECT` + active L2CAP socket |
| Data | Battery, case open, in-ear, pod model | Settings, ANC mode control, press controls, stem events, device info |
| Prerequisite | `BLUETOOTH_SCAN` on Android 12+, Bluetooth/location permissions below | Bonded + `BLUETOOTH_CONNECT` + active L2CAP socket |
| Data | Battery, case open, in-ear, pod model | Settings, ANC control, press controls, stem events, device info |
| Availability | Any pod in range | Only your own paired pods |
### Reaction System
A figure BLE never advertises cannot be obtained without a bonded AAP session, and anything
requiring a write is AAP-only.
- `ReactionsCard`: Compose UI for reaction settings, embedded in the device settings screen
- `PopUpWindow`: Displays AirPods status when case is opened
- `PopUpContent`: Compose pod rendering — model-specific UI branches inline, no factory class
## `DeviceMonitor` is the state merge boundary
### Widget System (Glance)
`DeviceMonitor` (singleton) `combine`s four live sources — `BlePodMonitor.devices`,
`AapConnectionManager.allStates`, `BluetoothManager2.connectedDevices` (supplies `isSystemConnected`),
and `DeviceProfilesRepo.profiles` — then merges `DeviceStateCache` on top, deliberately after the
combine so cache writes don't feed back into it.
- `BatteryGlanceWidget`, `AncGlanceWidget`: Jetpack Glance-based home-screen widgets
- `WidgetConfigurationActivity`: Configuration UI launched on widget placement
- Lives under `app/src/main/java/eu/darken/capod/main/ui/widget/`
The invariant is about **state**, not about the whole AAP layer:
### Upgrade / Pro Features
- Unified device state comes from `DeviceMonitor.devices` — don't assemble your own from `BlePodMonitor`
- Commands go **through** `AapConnectionManager.sendCommand(...)`. ViewModels legitimately inject it
(`OverviewViewModel`, `DeviceSettingsViewModel`, `PressControlsViewModel` all do)
- Nothing outside the AAP engine touches `AapConnection` (the L2CAP socket wrapper) directly
- `TroubleShooterViewModel` reaching into `BlePodMonitor` for raw diagnostic scans is an intentional
exception, not a pattern to copy
- `UpgradeRepo` interface with two flavor implementations:
- `UpgradeRepoGplay` — billing-client backed, includes grace-period handling for interrupted purchases
- `UpgradeControlFoss` — cache/sponsor-backed; users are `isPro = false` until they call `upgrade()`, after which the pro flag is persisted via DataStore
- FOSS is **not** "always pro" — it's opt-in via a local sponsor flow
Because the cache is merged in, a `PodDevice` may carry data while the device is out of range —
presence in the flow does not imply a live connection.
### AAP (Apple Accessory Protocol) Stack
## `AapConnectionManager` owns sessions
Three-layer structure under `pods/core/apple/aap/`:
It holds every open AAP session keyed by `BluetoothAddress`. Consumers call `sendCommand(...)` and
observe `allStates`.
- **`protocol/`** — pure data: `AapMessage`, `AapCommand`, `AapSetting`, `AapDeviceProfile`, `AapDeviceInfo`, `StemPressEvent`, `KeyExchangeResult`. Plus `DefaultAapDeviceProfile` and `Model.Features` capturing per-model capability
- **`engine/`** — session state machine for one connection:
- `AapConnection` — the L2CAP socket wrapper
- `AapSessionEngine` — drives the session lifecycle; tested in `AapSessionEngineTest`
- `AapInboundInterpreter` / `AapOutboundController` — decode incoming messages, encode outgoing
- `AapSettingsCoordinator`, `AapAncController`, `HidTracker`, `AapDeviceInfoDiagnostics` — feature-specific coordinators that sit on top of the session
- **`AapConnectionManager`** (singleton) — owns all open AAP sessions keyed by `BluetoothAddress`, uses `L2capSocketFactory` to create sockets. Consumers don't touch `AapConnection` directly — they call `sendCommand(...)` and observe `allStates`
The stack under `pods/core/apple/aap/` splits into `protocol/` (pure data) and `engine/` (per-connection
state machine). The glue in `monitor/core/aap/` wires it into the foreground service and persists
learned settings and session keys across restarts.
The monitor-layer glue (`monitor/core/aap/`) described above wires this stack into the foreground service and persists its learned state.
## FOSS is not "always pro"
### Common Utilities
`UpgradeRepo` has two flavor implementations. `UpgradeControlFoss` starts users at `isPro = false`
and only persists the pro flag after `upgrade()` is called via the local sponsor flow. Do not assume
the FOSS flavor bypasses pro gating.
- `EdgeToEdgeHelper`: Handles Android edge-to-edge display insets
## Navigation is mid-migration
## 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
1. `BluetoothEventReceiver` / `BootCompletedReceiver` wake `MonitorService` (foreground)
2. `MonitorService` keeps `BlePodMonitor` scanning (passive advertisements) and `AapLifecycleManager` running (active L2CAP sessions via `AapConnectionManager`)
3. `DeviceMonitor` merges BLE + AAP + cached state + profiles into `PodDevice` objects
4. ViewModels (`OverviewViewModel`, `DeviceSettingsViewModel`, `PressControlsViewModel`, widget view models) observe `DeviceMonitor.devices`; settings/command changes are sent back through `AapConnectionManager.sendCommand(...)`
5. Reaction triggers (case-open popup, auto-play, notifications) and widget state updates react to the merged flow
## Testing Strategy
- **Unit Tests**: `app/src/test/` (shared), `app/src/testFoss/`, `app/src/testGplay/` (flavor-specific — e.g. `UpgradeRepoGplayTest`, `FossUpgradeSerializationTest`)
- **Screenshot Tests**: `app/src/screenshotTest/` — Compose Preview Screenshot Testing, powers the Play Store screenshot pipeline
## Key Dependencies
- **Hilt**: Dependency injection framework
- **Navigation**: Navigation3 (`addNavigation3()`) drives current Compose screen routing. Some legacy `androidx.navigation` helpers still exist (`NavDirectionsExtensions`, `ViewModel3`) — don't assume SafeArgs is fully gone
- **kotlinx.serialization**: JSON serialization for configuration and caching
- **Material Design 3**: Compose Material3 UI components
Navigation3 (`addNavigation3()`) drives current Compose routing, but legacy `androidx.navigation`
helpers still exist (`NavDirectionsExtensions`, `ViewModel3`). Don't assume SafeArgs is fully gone.
+33 -49
View File
@@ -1,71 +1,55 @@
---
description: Build, test, lint, and release commands for Gradle
globs:
- "**/*.gradle.kts"
- "**/*.gradle"
- "gradle/**"
description: Gradle build, test, and lint commands, and what CI actually gates
---
# Build Commands
## Build
## Quick local check
```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
./gradlew assembleFossDebug testFossDebugUnitTest
```
## Testing
`assembleFossDebug` is the fastest variant — use it for iteration.
## What CI gates
`.github/workflows/code-checks.yml`, on every PR. Core Gradle gates:
```bash
# Run all unit tests
./gradlew test
# Lint vitals — flavor x variant matrix. Note: Beta/Release only, never Debug.
./gradlew lintVitalFossBeta lintVitalFossRelease lintVitalGplayBeta lintVitalGplayRelease
# Run unit tests for specific variant
./gradlew testFossDebugUnitTest
# Builds — Debug only
./gradlew app:assembleFossDebug app:assembleGplayDebug
# Run instrumentation tests (requires connected device/emulator)
./gradlew connectedAndroidTest
./gradlew connectedFossDebugAndroidTest
# Run all checks (lint + tests)
./gradlew check
# Unit tests — both flavors
./gradlew testFossDebugUnitTest testGplayDebugUnitTest
```
## Code Quality
Four non-Gradle checks also run, **unconditionally** — there is no path filter, so they gate your PR
even if you didn't touch those areas:
```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
bash fastlane/check_metadata_length.sh # Play Store metadata length limits
shellcheck tools/release/bump.sh
bats tools/release/bump.bats
./tools/release/bump.sh --mode=check # version.properties + VERSION consistency
```
## Release
Reproducing those locally is usually only worth it when you changed fastlane metadata or release
tooling, but a failure there blocks the PR regardless.
**Do not run `./gradlew check` as a pre-submit gate.** It runs the full non-vital `lint` task, which
is already failing on `main` for reasons unrelated to your change — you'll burn time chasing
pre-existing findings that CI never looks at. CI gates `lintVital*`, not `lint`.
## Other commands
```bash
./gradlew assembleFossRelease assembleGplayRelease
./gradlew assembleGplayRelease # release build
./gradlew bundleGplayRelease # Play Store bundle
./gradlew connectedFossDebugAndroidTest # instrumentation, needs a device/emulator
./gradlew lintFix # auto-fix where possible
./gradlew updateLintBaseline # refresh the baseline
```
## 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
+113
View File
@@ -0,0 +1,113 @@
---
description: Kotlin and Compose conventions — logging, ViewModel base classes, the ScreenHost/Screen split, DataStore settings
paths:
- "app/src/main/**/*.kt"
- "app/src/foss/**/*.kt"
- "app/src/gplay/**/*.kt"
- "app/src/debug/**/*.kt"
---
# Code Style
## Logging
`logTag()` builds the tag; `log()` takes a lambda so the message is only built if it's emitted.
```kotlin
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.debug.logging.Logging.Priority.*
companion object {
private val TAG = logTag("Profiles", "Repo") // multi-part tags are the norm
}
log(TAG) { "Processing $item" } // DEBUG is the default
log(TAG, VERBOSE) { "Devices changed" }
log(TAG, ERROR) { "Failed: ${e.asLog()}" } // asLog() for stacktraces
```
Never suppress protocol logging — downgrading a level is fine, removing the call is not.
## ViewModel base classes
Four exist. Use **`ViewModel4`** for new work — it's the current one (12 subclasses) and wires
`NavigationEventSource` + `ErrorEventSource2`.
- `ViewModel4` — current, use this
- `ViewModel2` — plain base, no nav/error event sources (4 subclasses)
- `ViewModel1` — legacy (1 subclass)
- `ViewModel3`**dead, zero subclasses.** It's the `ViewModel4` shape against the older
`NavEventSource`/`ErrorEventSource` interfaces. Don't extend it.
## Compose: the Host/Screen split
Every screen is two composables.
**`<Feature>ScreenHost`** — the only place that touches `hiltViewModel()`, installs the event
handlers, and collects state.
**`<Feature>Screen`** — presentation only. Takes a plain state object plus `on*` callbacks, so it
previews without Hilt.
```kotlin
@Composable
fun SettingsScreenHost(vm: SettingsViewModel = hiltViewModel()) {
ErrorEventHandler(vm)
NavigationEventHandler(vm)
val state by vm.state.collectAsStateWithLifecycle(initialValue = null)
state?.let {
SettingsScreen(
state = it,
onNavigateUp = { vm.navUp() },
onWiki = { vm.openUrl("https://github.com/d4rken-org/capod/wiki") },
)
}
}
@Composable
fun SettingsScreen(
state: SettingsViewModel.State,
onNavigateUp: () -> Unit,
onWiki: () -> Unit,
modifier: Modifier = Modifier, // last, after the required params
) { ... }
```
- `modifier: Modifier = Modifier` goes after the required parameters — i.e. it is the first
*optional* one, per the Compose API guidelines. capod is not fully consistent here (roughly 10
composables put it after required params, 3 put it genuinely first); match the file you're in
rather than reformatting neighbours
- The Host null-guards state; `collectAsStateWithLifecycle(initialValue = null)` is the usual shape
- Wrap previews in `PreviewWrapper` (`common/compose/PreviewWrapper.kt`), which applies `CapodTheme`
plus a background `Surface`
- Trailing commas on multi-line parameter lists and argument lists
## DataStore settings
`createValue()` is overloaded. Primitives need no serializer:
```kotlin
val monitorMode = dataStore.createValue("core.monitor.mode", MonitorMode.AUTOMATIC)
```
`@Serializable` types take a `Json`, and optionally fall back instead of throwing on corrupt or
legacy stored JSON:
```kotlin
val config = dataStore.createValue("some.config", SomeConfig(), json, onErrorFallbackToDefault = true)
```
Read and write via `.value()` / `.value(x)` (suspend) or `.flow` (reactive). Both `value` functions
are **extension functions**, not members — see `.claude/rules/testing.md` for what that means when
mocking.
## General
- Package by feature, not by layer
- Prefer adding to an existing file over creating a new one
- Prefer flow-based, cancellable solutions
- No comments for self-evident code
- Place `@Suppress` as close to the affected code as possible — on the function or constructor,
not the whole class
-2
View File
@@ -1,7 +1,5 @@
---
description: Git commit message format and conventions
globs:
- "**"
---
# Commit Guidelines
+2 -2
View File
@@ -1,7 +1,7 @@
---
description: Guidelines for adding and naming Android string resources
globs:
- "**/res/values*/strings.xml"
paths:
- "**/res/values/strings.xml"
---
# Localization Guidelines
+33 -66
View File
@@ -1,22 +1,21 @@
---
description: Pull request naming and description conventions
globs:
- "**"
description: Pull request title and description conventions
---
# Pull Request Guidelines
## PR Title Format
## Title
```
<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.
Titles appear in auto-generated changelogs and are read by users. Use ELI5, user-facing language
no class names, library names, or implementation details. `refactor(settings): Migrate preferences
to DataStore` is the shape to avoid; `General: Remember settings between app restarts` is the shape
to use.
## Category Prefixes
| Prefix | Covers |
| Category | Covers |
|--------|--------|
| **Widget** | Home screen widget |
| **Reaction** | Case-open popup, auto-play/pause, notification triggers |
@@ -24,76 +23,44 @@ PR titles appear in auto-generated changelogs and are read by users. Use **ELI5,
| **General** | Dashboard, settings, notifications, themes, onboarding, support, app-wide UI |
| **Fix** | Bug fixes spanning multiple areas |
### Title Examples
## Description
- `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
PRs are reviewed in **GitHub's web UI**, which already shows the file tree, the diff, and the tests. Don't duplicate any
of it. The description should answer questions the diff can't — not restate it.
Only these sections, in this order:
PRs are reviewed in GitHub's web UI, which already shows the file tree, the diff, and the tests.
The description answers what the diff can't. Use exactly these sections, in this order:
1. `## What changed`
2. `## Technical Context`
3. `## Review checklist` *(optional)*
No `Scope`, `Files changed`, `Tests`, or `Review guidance` sub-sections — GitHub shows the files and tests, and review
notes belong in the checklist. Fold anything critical into a Technical Context bullet.
No `Scope`, `Files changed`, `Tests`, or `Review guidance` sections.
### What changed
**What changed** — user-facing explanation: the problem fixed or the feature added, from the user's
perspective. For refactors, tests, CI, and dependency bumps, write "No user-facing behavior change"
followed by a brief internal description.
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.
**Technical Context** — one bullet per point, no prose paragraphs, no nested `**Bug 1**` headers.
Cover only what the diff can't show:
- **Why** this approach, and what was rejected
- **Root cause** for bug fixes — the diff shows the fix, not what caused it
- **Non-obvious side effects** or behavioral changes
For non-user-facing PRs (refactors, tests, CI, dependency bumps): write "No user-facing behavior change" followed by a brief internal description.
**Review checklist**`- [ ]` items, only when there are several non-trivial things to verify.
A single tricky point stays a Technical Context bullet.
### Technical Context
## Labels
Explain what's hard to extract from the diff alone. Focus on:
Apply labels that match the change. Run `gh label list` to confirm what exists — do not invent new
ones. Skip labels that don't fit; no labels beats wrong labels.
- **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
Format rules:
- **One bullet per point.** No prose paragraphs, no nested sub-headers like `**Bug 1** / **Bug 2**` — if a PR fixes
multiple bugs, one bullet per bug is enough.
- **Don't restate the diff.** File paths, class renames, test names, and line-level changes are all visible in the web
UI.
### Review checklist (optional)
For PRs with multiple non-trivial review points, add a `## Review checklist` section with `- [ ]` tasks the reviewer can
tick off as they verify. Skip it for small PRs — a single tricky thing can stay as a Technical Context bullet.
### 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
```
- **Type**: `bug` for fixes, `enhancement` for new features or improvements
- **Transport**: `coms/AAP` when the change touches the L2CAP session path, `coms/BLE` when it
touches advertisement parsing. Both if it spans the merge in `DeviceMonitor`
- **Scope**: `device support` for new or fixed pod models, `Translations` for string/locale work,
`Build/Deploy` for CI, Gradle, and release tooling
- `Needs Info/Repro` is a triage label for issues — not for your own PRs
## 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)
- Link issues with "Closes #123" / "Fixes #123" / "Resolves #123"
- Prefix breaking changes with "BREAKING:"
- No `Co-authored-by` trailers
+14 -75
View File
@@ -1,78 +1,17 @@
# Release Process
---
description: Release guardrails — what never to do by hand. Full procedure is the /release skill.
---
Releases are cut via the **Release prepare** workflow (`.github/workflows/release-prepare.yml`). It bumps `version.properties` and `VERSION`, commits to `main`, tags `v<version>`, pushes atomically, and dispatches `release-tag.yml` which builds, signs, and uploads.
# Release
## Dispatch
The procedure lives in the `/release` skill (invoke it deliberately; it does not auto-load).
These constraints apply regardless of whether that skill was invoked.
```bash
# Plan only — no commit, no tag, no push.
gh workflow run release-prepare.yml -f bump_kind=build -f dry_run=true
# Real cut.
gh workflow run release-prepare.yml -f bump_kind=build -f dry_run=false
```
After `dry_run=false`: Job 1 computes + writes the summary, then Job 2 immediately commits/tags/pushes (no env gate — cancel the run between Job 1 and Job 2 if the summary looks wrong; you have ~seconds). The tag push naturally triggers `release-tag.yml` (the App-token push fires `on: push:` workflows; only `GITHUB_TOKEN`-pushes are suppressed). `release-tag.yml` then runs `validate-tag` and the existing `release-github` (`foss-production` approval) + `release-gplay` (`gplay-production` approval) jobs — those are the two human checkpoints, matching the pre-migration UX.
## Inputs
| Input | Default | Notes |
|---|---|---|
| `bump_kind` | `build` | `build` \| `patch` \| `minor` \| `major` |
| `version_type` | `keep-current` | Preserves current `rc`/`beta`. Set explicitly to switch. |
| `version_override` | empty | e.g. `5.1.2-rc0`. Bypasses bump_kind/version_type. |
| `expected_current` | empty | Optional: fail if `version.properties` ≠ this. Useful for tight coordination. |
| `dry_run` | `true` | Default is plan-only. |
Bump rules: `build` increments build; `patch`/`minor`/`major` zero everything to the right of the bumped field. All numeric fields bounded `0..99` (the `versionCode` formula collapses at ≥100).
## Local
```bash
./tools/release/bump.sh --mode=plan --bump-kind=build --version-type=keep-current
./tools/release/bump.sh --mode=check
bats tools/release/bump.bats
```
## Channel mapping
| Tag suffix | FOSS APK | GitHub release | Fastlane lane | Play track | Rollout |
|---|---|---|---|---|---|
| `-beta*` | `assembleFossBeta` | pre-release | `beta` | `beta` | 10% |
| `-rc*` (or anything else) | `assembleFossRelease` | full release | `production` | **`beta`** | 10% |
`lane :production` in `Fastfile` uploads to Play's **beta** track at 10% — manually promoted to production via Play Console.
## Rollback
| Stage reached | Steps |
|---|---|
| Bump on `main`, downstream not started | `git push origin :refs/tags/v<bad>`, `git revert <bump-sha>`, push |
| GitHub release created | Above + `gh release delete v<bad> --yes --cleanup-tag` |
| Play upload completed | Above + halt rollout in Play Console (or `bundle exec fastlane supply --track beta --rollout 0 --version-code <bad-code>`) |
| Job 2 ran but downstream rejected at env approval | Treat as first row — bump+tag are public on `main` regardless of downstream outcome |
`bump.sh` enforces strict `versionCode` monotonicity, so re-using a code is impossible without manually editing `version.properties`.
## Auth setup
`release-prepare.yml` Job 2 uses a GitHub App token (not `GITHUB_TOKEN`) to push the bump commit and tag. The App identity is in the rulesets' bypass list, which is what allows the push to bypass branch protection + tag-creation restrictions.
Required org secrets (set on the d4rken-org organization, accessible to `capod`):
- `RELEASE_APP_CLIENT_ID` — Client ID of the `d4rken-org-releaser` GitHub App (visible on the App's settings page, format `Iv1.<hex>` or similar)
- `RELEASE_APP_PRIVATE_KEY` — full `.pem` contents (including BEGIN/END lines)
The App is installed on this repo and added as a bypass actor to:
- The main-branch ruleset (PR + status check requirements)
- The tag ruleset (creation restriction on `v*`)
Other apps in the org can reuse the same App + secrets — just install the App on each repo and add it to that repo's rulesets' bypass lists.
## Defense in depth
`release-tag.yml` includes `validate-tag` which: (1) regex-checks `github.ref_name`, (2) runs `bump.sh --mode=check`, (3) asserts the parsed name matches the tag. Manual `gh workflow run release-tag.yml --ref vfoo` or hand-pushed tags fail before any build.
## Stuck-dispatch recovery
If Job 2's atomic push lands but the natural `on: push:` trigger doesn't fire `release-tag.yml` (rare — would mean GitHub dropped the event), the tag is public but no pipeline runs. Re-dispatch manually: `gh workflow run release-tag.yml --ref v<new> -f dry_run=false`.
- **`release-prepare.yml` is the only sanctioned path** for bumping a version or creating a release
tag. Do not edit `version.properties` or `VERSION` by hand, and do not create `v*` tags manually —
`validate-tag` in `release-tag.yml` rejects anything not matching `v<M.m.p>-(rc|beta)N`.
- **`tools/release/bump.sh` is the single source of truth for version logic.** Its versionCode
formula mirrors `buildSrc/src/main/java/ProjectConfig.kt` — the two must stay in sync.
- CI runs `./tools/release/bump.sh --mode=check` on every PR (`check-release-tooling` in
`code-checks.yml`). If you did touch `version.properties` or `VERSION`, run that locally first.
- All numeric version fields are bounded `0..99`; the versionCode formula collapses at ≥100.
+13 -2
View File
@@ -1,3 +1,14 @@
---
description: Play Store screenshot pipeline — generation, copying, and adding or removing screens
paths:
- "app/src/debug/**/screenshots/**"
- "app/src/screenshotTest/**"
- "fastlane/generate_screenshots.sh"
- "fastlane/copy_screenshots.sh"
- "fastlane/Fastfile"
- "fastlane/metadata/android/*/images/phoneScreenshots/**"
---
# Play Store Screenshot Pipeline
## Overview
@@ -19,7 +30,7 @@ ScreenshotContent.kt (mock data + composables)
| File | Purpose |
|------|---------|
| `app/src/debug/java/.../screenshots/ScreenshotContent.kt` | Mock data composables for each screen (7 screens) |
| `app/src/debug/java/.../screenshots/ScreenshotContent.kt` | Mock data composables (7 exist; `HomescreenWidgetContent` has an IDE preview only and is **not** in the Play Store pipeline) |
| `app/src/screenshotTest/kotlin/.../screenshots/PlayStoreScreenshots.kt` | `@PreviewTest` functions (currently: `DashboardLight`, `DashboardDark`, `CasePopUp`, `DeviceProfiles`, `AddProfile`, `DeviceSettingsReactions`, `WidgetConfiguration`) |
| `app/src/screenshotTest/kotlin/.../screenshots/PlayStoreLocales.kt` | Multi-preview annotations (auto-generated by batch script) |
| `fastlane/generate_screenshots.sh` | Batched generation; locale list (`ALL_LOCALES`) and `BATCH_SIZE` are defined inside the script |
@@ -77,7 +88,7 @@ When modifying a screen that appears in screenshots (check `ScreenshotContent.kt
Periodic, manual operation — not per-PR:
```bash
./fastlane/generate_screenshots.sh # full, ~30 min, 477 PNGs
./fastlane/generate_screenshots.sh # full, ~30 min, 476 PNGs (68 locales x 7)
./fastlane/copy_screenshots.sh --clean
bundle exec fastlane screenshots_only # uploads all 68 locales to Play Store
git checkout -- fastlane/metadata/android/ # discard non-smoke changes (gitignored anyway)
+79
View File
@@ -0,0 +1,79 @@
---
description: Unit test conventions — JUnit 5, kotest assertions, mockk, BaseTest, and which Gradle task runs which source set
paths:
- "app/src/test/**"
- "app/src/testFoss/**"
- "app/src/testGplay/**"
- "app/build.gradle.kts"
- "buildSrc/src/main/java/Dependencies.kt"
---
# Testing
The stack here is not the Android default — check this before reaching for a familiar library.
## Libraries
- **JUnit 5** (`org.junit.jupiter.api.Test`). Gradle sets `useJUnitPlatform()`.
- **kotest** for assertions: `io.kotest.matchers.shouldBe`, `shouldBeNull`, `shouldBeInstanceOf`,
`shouldContainExactly`, `io.kotest.assertions.throwables.shouldThrow`. Use kotest for new
assertions — `MediaControlTest` still uses JUnit `Assertions.*` and is a legacy exception.
- **mockk** for mocking. Not Mockito.
- **Turbine is not a dependency.** `testhelpers.flow.FlowTest` provides a `Flow<T>.test()` helper —
use it rather than adding one.
## Base classes
Extend `testhelpers.BaseTest`, or the applicable specialized base that already extends it:
- `BaseBlePodsTest` — BLE advertisement parsing per pod model
- `BaseAapSessionTest` — AAP protocol/session tests
`BaseTest` installs a `JUnitLogger` and calls `unmockkAll()` in `@AfterAll`. Skipping it can leave
global mockk and logging state behind for later test classes.
The only exceptions are the Robolectric-backed tests (Compose UI via
`testhelpers.compose.BaseComposeRobolectricTest`, and the few DataStore-backed ones such as
`CurriculumVitaeProHistoryTest`), which use JUnit 4 `@RunWith`/`@Rule` via `junit-vintage-engine`.
Don't copy that pattern for a plain unit test.
## Source sets and Gradle tasks
Each task compiles and runs only its own flavor — running the wrong one silently skips your test.
| Test location | Task |
|---|---|
| `app/src/test/` (shared) | either; run both before pushing |
| `app/src/testFoss/` | `./gradlew testFossDebugUnitTest` |
| `app/src/testGplay/` | `./gradlew testGplayDebugUnitTest` |
CI runs both. Flavor-specific tests are for code that only exists in that flavor — billing in
`gplay`, the sponsor-based upgrade flow in `foss`.
## Helpers that already exist
- `runTest2(autoCancel, context, expectedError, testBody)` in `testhelpers/coroutine/TestExtensions.kt`
use `expectedError = SomeException::class` instead of hand-rolling a throws-assertion around `runTest`
- `FakeDataStoreValue<T>(initial)` in `testhelpers/datastore/` — a working fake with a real backing
`MutableStateFlow`; read/write it through `.value` and pass `.mock` to the code under test
## Mocking `DataStoreValue`
`DataStoreValue.value()` and `.value(T)` are **extension functions** (`DataStoreValue.kt:54,56`), not
members, so MockK cannot stub them. They delegate to `flow.first()` and `update { }` — stub those:
```kotlin
every { someSetting.flow } returns flowOf(value) // covers .value() reads
coVerify { someSetting.update(any()) } // verifies .value(x) writes
```
`UpgradeRepoGplayTest` uses this shape. Prefer `FakeDataStoreValue` when you need reads and writes to
actually round-trip.
## Reading ViewModel state
`ViewModel2.asLiveState()` is `stateIn(..., initialValue = null).filterNotNull()` with
`SharingStarted.WhileSubscribed(5_000)` — so `vm.state` is a `Flow`, not a `StateFlow`, and has no
`.value` to read. Collect it: `vm.state.first()` is the established pattern across the existing
ViewModel tests. Because the upstream only runs while subscribed, a test that never collects sees
nothing happen at all.
+3 -1
View File
@@ -12,6 +12,8 @@
"jvm-tools@claude-code-cafe": true,
"frontend-design@claude-plugins-official": true,
"kotlin-lsp@claude-plugins-official": true,
"support-investigator@claude-code-cafe": true
"support-investigator@claude-code-cafe": true,
"google-play@claude-code-cafe": true,
"devtools@claude-code-cafe": true
}
}
+115
View File
@@ -0,0 +1,115 @@
---
description: Cut a capod release via the "Release prepare" workflow — dispatch inputs, channel mapping, rollback, and auth setup.
disable-model-invocation: true
argument-hint: "[bump_kind] [version_type|version_override]"
---
# Release Process
Releases are cut via the **Release prepare** workflow (`.github/workflows/release-prepare.yml`). It bumps `version.properties` and `VERSION`, commits to `main`, tags `v<version>`, pushes atomically, and dispatches `release-tag.yml` which builds, signs, and uploads.
## Required order
A real cut pushes a commit and a tag to `main` and is public the moment it lands. Do not skip ahead.
1. Run the dry run first and read its summary — never dispatch `dry_run=false` blind.
2. Report the planned version and `versionCode` back to the user.
3. Get explicit confirmation for that specific version before dispatching `dry_run=false`.
4. If the user named `bump_kind`/`version_type`/`version_override`, use exactly those. If the request
is ambiguous about which field moves, ask rather than assuming `build`.
## Dispatch
`gh workflow run` only fires the dispatch — it returns nothing about the result. The summary is
written asynchronously, so you have to go fetch it.
```bash
# Step 1 — plan only. No commit, no tag, no push. Always run this first.
gh workflow run release-prepare.yml -f bump_kind=build -f dry_run=true
# Step 2 — find the run just dispatched and wait for it.
gh run list --workflow=release-prepare.yml --limit 1 # note the run id
gh run watch <run-id> --exit-status
# Step 3 — read the computed plan (version + versionCode) before going further.
gh run view <run-id> --log | tail -40
```
Report the planned version and `versionCode`, get explicit confirmation, then:
```bash
# Step 4 — real cut. Repeat the dry run's inputs EXACTLY; change only dry_run.
gh workflow run release-prepare.yml -f bump_kind=build -f dry_run=false
```
The `bump_kind=build` above is only an example. If the confirmed plan came from a `patch`/`minor`/
`major` bump, a `version_type` switch, or a `version_override`, Step 4 must carry those same flags —
otherwise you cut a different version than the one that was approved.
After `dry_run=false`: Job 1 computes + writes the summary, then Job 2 immediately commits/tags/pushes (no env gate — cancel the run between Job 1 and Job 2 if the summary looks wrong; you have ~seconds). The tag push naturally triggers `release-tag.yml` (the App-token push fires `on: push:` workflows; only `GITHUB_TOKEN`-pushes are suppressed). `release-tag.yml` then runs `validate-tag` and the existing `release-github` (`foss-production` approval) + `release-gplay` (`gplay-production` approval) jobs — those are the two human checkpoints, matching the pre-migration UX.
## Inputs
| Input | Default | Notes |
|---|---|---|
| `bump_kind` | `build` | `build` \| `patch` \| `minor` \| `major` |
| `version_type` | `keep-current` | Preserves current `rc`/`beta`. Set explicitly to switch. |
| `version_override` | empty | e.g. `5.1.2-rc0`. Bypasses bump_kind/version_type. |
| `expected_current` | empty | Optional: fail if `version.properties` ≠ this. Useful for tight coordination. |
| `dry_run` | `true` | Default is plan-only. |
Bump rules: `build` increments build; `patch`/`minor`/`major` zero everything to the right of the bumped field. All numeric fields bounded `0..99` (the `versionCode` formula collapses at ≥100).
## Local
```bash
./tools/release/bump.sh --mode=plan --bump-kind=build --version-type=keep-current
./tools/release/bump.sh --mode=check
bats tools/release/bump.bats
```
## Channel mapping
| Tag suffix | FOSS APK | GitHub release | Fastlane lane | Play track | Rollout |
|---|---|---|---|---|---|
| `-beta*` | `assembleFossBeta` | pre-release | `beta` | `beta` | 10% |
| `-rc*` | `assembleFossRelease` | full release | `production` | **`beta`** | 10% |
`release-tag.yml` accepts only `v<M.m.p>-rcN` or `v<M.m.p>-betaN` — any other suffix fails
`validate-tag` before a build starts. There is no third channel.
`lane :production` in `Fastfile` uploads to Play's **beta** track at 10% — manually promoted to production via Play Console.
## Rollback
| Stage reached | Steps |
|---|---|
| Bump on `main`, downstream not started | `git push origin :refs/tags/v<bad>`, `git revert <bump-sha>`, push |
| GitHub release created | Above + `gh release delete v<bad> --yes --cleanup-tag` |
| Play upload completed | Above + halt rollout in Play Console (or `bundle exec fastlane supply --track beta --rollout 0 --version-code <bad-code>`) |
| Job 2 ran but downstream rejected at env approval | Treat as first row — bump+tag are public on `main` regardless of downstream outcome |
`bump.sh` enforces strict `versionCode` monotonicity, so re-using a code is impossible without manually editing `version.properties`.
## Auth setup
`release-prepare.yml` Job 2 uses a GitHub App token (not `GITHUB_TOKEN`) to push the bump commit and tag. The App identity is in the rulesets' bypass list, which is what allows the push to bypass branch protection + tag-creation restrictions.
Required org secrets (set on the d4rken-org organization, accessible to `capod`):
- `RELEASE_APP_CLIENT_ID` — Client ID of the `d4rken-org-releaser` GitHub App (visible on the App's settings page, format `Iv1.<hex>` or similar)
- `RELEASE_APP_PRIVATE_KEY` — full `.pem` contents (including BEGIN/END lines)
The App is installed on this repo and added as a bypass actor to:
- The main-branch ruleset (PR + status check requirements)
- The tag ruleset (creation restriction on `v*`)
Other apps in the org can reuse the same App + secrets — just install the App on each repo and add it to that repo's rulesets' bypass lists.
## Defense in depth
`release-tag.yml` includes `validate-tag` which: (1) regex-checks `github.ref_name`, (2) runs `bump.sh --mode=check`, (3) asserts the parsed name matches the tag. Manual `gh workflow run release-tag.yml --ref vfoo` or hand-pushed tags fail before any build.
## Stuck-dispatch recovery
If Job 2's atomic push lands but the natural `on: push:` trigger doesn't fire `release-tag.yml` (rare — would mean GitHub dropped the event), the tag is public but no pipeline runs. Re-dispatch manually: `gh workflow run release-tag.yml --ref v<new> -f dry_run=false`.
+26
View File
@@ -0,0 +1,26 @@
name: Thumbnail images
# Shrinks oversized screenshots in issues and comments into clickable
# thumbnails. The logic lives in d4rken-org/.github so every app repo shares
# one copy; this stub only supplies the triggers, because workflow_call cannot
# be triggered by issue_comment directly.
#
# The shared workflow splits into an unprivileged parsing job and a privileged
# job that only makes the API call, so issues:write below is the ceiling, not
# what the parsing half receives.
#
# Add <!-- no-thumbnail --> to a body to opt it out.
on:
issue_comment:
types: [created, edited]
issues:
types: [opened, edited]
permissions: {}
jobs:
thumbnail:
permissions:
issues: write
uses: d4rken-org/.github/.github/workflows/thumbnail-images.yml@94e36d9a67a887e24338f95fd0429925cea21c98
+1 -1
View File
@@ -1 +1 @@
5.2.0-rc0 50200000
5.2.3-rc0 50203000
+12 -2
View File
@@ -189,14 +189,24 @@ dependencies {
addCompose()
addGlance()
addWorkerManager()
addDataStore()
addNavigation3()
addSerialization()
addTesting()
"gplayImplementation"("com.android.billingclient:billing:8.0.0")
"gplayImplementation"("com.android.billingclient:billing-ktx:8.0.0")
"gplayImplementation"("com.android.billingclient:billing:8.3.0")
"gplayImplementation"("com.android.billingclient:billing-ktx:8.3.0")
"gplayImplementation"("com.google.android.play:review:2.0.2")
"gplayImplementation"("com.google.android.play:review-ktx:2.0.2")
// Robolectric-backed Compose UI tests (run as regular unit tests via the vintage engine).
testImplementation(platform("androidx.compose:compose-bom:${Versions.Compose.bom}"))
testImplementation("androidx.compose.ui:ui-test-junit4")
testImplementation("androidx.compose.ui:ui-test-manifest")
testImplementation("org.robolectric:robolectric:4.15.1")
"screenshotTestImplementation"(platform("androidx.compose:compose-bom:${Versions.Compose.bom}"))
"screenshotTestImplementation"("com.android.tools.screenshot:screenshot-validation-api:0.0.1-alpha13")
+3 -1
View File
@@ -5,4 +5,6 @@
# 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>(...); }
-keep class androidx.work.** { <init>(...); }
# Play Core KTX references this compile-time-only GMS annotation not on the runtime classpath
-dontwarn com.google.android.gms.common.annotation.NoNullnessRewrite
@@ -19,6 +19,7 @@ import eu.darken.capod.common.compose.PreviewWrapper
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.core.MonitorMode
import eu.darken.capod.main.ui.overview.OverviewScreen
import eu.darken.capod.main.ui.overview.OverviewViewModel
import eu.darken.capod.monitor.core.battery.BatteryEstimate
@@ -85,6 +86,7 @@ internal fun DashboardContent(showAap: Boolean = false) = PreviewWrapper {
devices = devices,
isDebug = false,
isBluetoothEnabled = true,
effectiveMode = MonitorMode.AUTOMATIC,
profiles = listOf(
MockPodDataProvider.profile("My AirPods Pro", PodModel.AIRPODS_PRO2),
MockPodDataProvider.profile("AirPods Max", PodModel.AIRPODS_MAX),
@@ -0,0 +1,31 @@
package eu.darken.capod.common.review
import android.app.Activity
import eu.darken.capod.common.debug.logging.Logging.Priority.INFO
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class FossReviewTool @Inject constructor() : ReviewTool {
override val state: Flow<ReviewTool.State> = flowOf(ReviewTool.State())
override suspend fun dismiss() {
log(TAG, INFO) { "dismiss()" }
// NOOP
}
override suspend fun reviewNow(activity: Activity) {
log(TAG, INFO) { "reviewNow($activity)" }
// NOOP
}
companion object {
private val TAG = logTag("Review", "Tool", "FOSS")
}
}
@@ -0,0 +1,16 @@
package eu.darken.capod.common.review
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
abstract class ReviewModule {
@Binds
@Singleton
abstract fun reviewTool(tool: FossReviewTool): ReviewTool
}
@@ -4,7 +4,8 @@ import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import eu.darken.capod.common.upgrade.core.UpgradeControlFoss
import eu.darken.capod.common.upgrade.core.UpgradeDiagnosticsFoss
import eu.darken.capod.common.upgrade.core.UpgradeRepoFoss
import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
@@ -12,6 +13,10 @@ import javax.inject.Singleton
abstract class UpgradeModule {
@Binds
@Singleton
abstract fun control(foss: UpgradeControlFoss): UpgradeRepo
abstract fun control(foss: UpgradeRepoFoss): UpgradeRepo
@Binds
@Singleton
abstract fun diagnostics(foss: UpgradeDiagnosticsFoss): UpgradeDiagnostics
}
@@ -12,18 +12,25 @@ import kotlinx.serialization.json.Json
import javax.inject.Inject
import javax.inject.Singleton
// Retained legacy migration: installs that predate the DataStore move still carry their upgrade
// state in the "settings_foss" SharedPreferences file.
private val Context.fossCacheDataStore by preferencesDataStore(
name = "settings_foss",
produceMigrations = { ctx -> listOf(SharedPreferencesMigration(ctx, "settings_foss")) },
)
@Singleton
class FossCache @Inject constructor(
@ApplicationContext context: Context,
@SerializationCapod json: Json,
class FossCache internal constructor(
// Test seam: the store is handed in so a test can supply its own DataStore instead of the
// Context-bound production delegate. Same pattern as BillingCache.
private val dataStore: DataStore<Preferences>,
json: Json,
) {
private val Context.dataStore by preferencesDataStore(
name = "settings_foss",
produceMigrations = { ctx -> listOf(SharedPreferencesMigration(ctx, "settings_foss")) }
)
private val dataStore: DataStore<Preferences> = context.dataStore
@Inject constructor(
@ApplicationContext context: Context,
@SerializationCapod json: Json,
) : this(context.fossCacheDataStore, json)
val upgrade = dataStore.createValue<FossUpgrade?>(
key = "foss.upgrade",
@@ -1,46 +0,0 @@
package eu.darken.capod.common.upgrade.core
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,
) : UpgradeRepo {
override val upgradeInfo: Flow<UpgradeRepo.Info> = fossCache.upgrade.flow.map { data ->
if (data == null) {
Info()
} else {
Info(
isPro = true,
upgradedAt = data.upgradedAt,
upgradeReason = data.reason
)
}
}
fun upgrade(reason: FossUpgrade.Reason) {
fossCache.upgrade.valueBlocking = FossUpgrade(
upgradedAt = Instant.now(),
reason = reason
)
}
data class Info(
override val isPro: Boolean = false,
override val upgradedAt: Instant? = null,
val upgradeReason: FossUpgrade.Reason? = null,
override val error: Throwable? = null,
) : UpgradeRepo.Info {
override val type: UpgradeRepo.Type = UpgradeRepo.Type.FOSS
}
override fun getSponsorUrl(): String = "https://github.com/sponsors/d4rken"
}
@@ -0,0 +1,15 @@
package eu.darken.capod.common.upgrade.core
import eu.darken.capod.common.upgrade.UpgradeDiagnostics
import javax.inject.Inject
import javax.inject.Singleton
/**
* FOSS has no store entitlement to reconcile: the upgrade state is a local sponsor record, already
* covered by the existing header fields. Nothing to add.
*/
@Singleton
class UpgradeDiagnosticsFoss @Inject constructor() : UpgradeDiagnostics {
override suspend fun debugInfo(): String? = null
}
@@ -0,0 +1,151 @@
package eu.darken.capod.common.upgrade.core
import eu.darken.capod.common.WebpageTool
import eu.darken.capod.common.coroutine.AppScope
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
import eu.darken.capod.common.debug.logging.asLog
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.flow.setupCommonEventHandlers
import eu.darken.capod.common.upgrade.UpgradeRepo
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.shareIn
import java.time.Instant
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class UpgradeRepoFoss @Inject constructor(
@AppScope private val appScope: CoroutineScope,
private val fossCache: FossCache,
private val webpageTool: WebpageTool,
) : UpgradeRepo {
override val storeSite: String = STORE_SITE
override val upgradeSite: String = UPGRADE_SITE
override val betaSite: String = BETA_SITE
private val refreshTrigger = MutableStateFlow(UUID.randomUUID())
// Written only from the sharing coroutine (single collector) — no synchronization needed.
// Recorded INSIDE the flatMapLatest block, upstream of its channel buffer: a downstream onEach
// can still be waiting on a buffered emission when the inner flow throws, and the catch below
// would then read a stale (null) value and revoke an entitlement we already saw.
private var lastKnownInfo: Info? = null
override val upgradeInfo: Flow<UpgradeRepo.Info> = refreshTrigger
.flatMapLatest {
fossCache.upgrade.flow
.map { data ->
if (data == null) {
Info()
} else {
Info(
isPro = true,
upgradedAt = data.upgradedAt,
upgradeReason = data.reason,
)
}
}
// Same coroutine as the throw below, so the ordering is guaranteed. Only
// successfully mapped elements pass here — catch emissions go straight downstream
// and never record themselves as a last known state.
.onEach { lastKnownInfo = it }
.catch { e ->
// A SharedFlow cannot fail: without this, a thrown cache read dies inside
// shareIn's sharing coroutine and every collector hangs forever (VM state stuck
// on Loading, checkSponsorReturn suspended mid-unlock). The catch sits INSIDE
// flatMapLatest so the error completes only this inner subscription — refresh()
// resubscribes the cache and recovery stays possible. Last-known preservation:
// a late read failure must not revoke an entitlement we already saw; the error
// rides on the previous Info instead. Contrast: gplay keeps a retryWhen loop
// because billing re-settles in-place — the FOSS read is a local one-shot, and
// refresh-driven resubscription IS the retry.
if (e is CancellationException) throw e
log(TAG, WARN) { "upgradeInfo read failed: ${e.asLog()}" }
emit((lastKnownInfo ?: Info()).copy(error = e))
}
}
.setupCommonEventHandlers(TAG) { "upgradeInfo" }
.shareIn(appScope, SharingStarted.WhileSubscribed(3000L, 0L), replay = 1)
// Synchronous so the caller learns whether the page actually opened: the FOSS unlock heuristic
// only arms on a successful launch, and a fire-and-forget coroutine can't report that back.
fun openGithubSponsorsPage(): Boolean {
log(TAG) { "openGithubSponsorsPage()" }
return webpageTool.open(upgradeSite)
}
// Writes capod's RETAINED persistence schema: existing supporter records are serialized with
// `reason` (foss.upgrade.reason.*). Adopting canonical's `upgradeType` schema would decode
// every stored record as null and strip those supporters' entitlement.
/**
* Create-only-if-absent inside the store transaction: an existing record (and its upgradedAt —
* the user-visible "supporter since" date) is never replaced. The VM-level isPro guard alone is
* not race-free: it reads a shareIn replay that can be stale. Note the kept record is still
* re-encoded through the current schema — decoded fields are preserved exactly.
*
* Caveat from [FossCache]'s `onErrorFallbackToDefault = true`: a stored record that fails to
* decode reads as null and therefore counts as ABSENT to this transaction, i.e. it gets
* replaced. That matches the pre-existing read behaviour — such a record already presents the
* user as free — and re-creating it on the next successful sponsor visit is the recovery path.
* Decode failures therefore fall back to absent by design (the flag), so the error path around
* [upgradeInfo] covers IO/corruption throws, not schema mismatches.
*
* @return true if a new record was created, false if an existing record was kept.
*/
internal suspend fun persistUpgrade(): Boolean {
log(TAG) { "persistUpgrade()" }
val updated = fossCache.upgrade.update { existing ->
existing ?: FossUpgrade(
upgradedAt = Instant.now(),
reason = FossUpgrade.Reason.DONATED,
)
}
// A returned transaction proves the store is readable again: revive a possibly error-stuck
// inner flow so the record propagates to collectors still holding the error replay.
refresh()
return if (updated.old == null) {
true
} else {
log(TAG, WARN) {
"persistUpgrade(): Record already exists (upgradedAt=${updated.old.upgradedAt}), keeping it"
}
false
}
}
override suspend fun refresh() {
log(TAG) { "refresh()" }
refreshTrigger.value = UUID.randomUUID()
}
data class Info(
override val isPro: Boolean = false,
override val upgradedAt: Instant? = null,
val upgradeReason: FossUpgrade.Reason? = null,
override val error: Throwable? = null,
) : UpgradeRepo.Info {
override val type: UpgradeRepo.Type = UpgradeRepo.Type.FOSS
// The FOSS entitlement is a local cache read — authoritative from the first emission,
// there is no billing handshake to wait out.
override val isSettled: Boolean = true
}
companion object {
private const val STORE_SITE = "https://github.com/d4rken-org/capod/releases"
private const val UPGRADE_SITE = "https://github.com/sponsors/d4rken"
private const val BETA_SITE = "https://github.com/d4rken-org/capod/releases"
private val TAG = logTag("Upgrade", "Foss", "Repo")
}
}
@@ -1,4 +1,4 @@
package eu.darken.capod.upgrade.ui
package eu.darken.capod.common.upgrade.ui
import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavKey
@@ -13,7 +13,7 @@ import javax.inject.Inject
class UpgradeNavigation @Inject constructor() : NavigationEntry {
override fun EntryProviderScope<NavKey>.setup() {
entry<Nav.Main.Upgrade> { UpgradeScreenHost() }
entry<Nav.Main.Upgrade> { key -> UpgradeScreenHost(route = key) }
}
@Module
@@ -0,0 +1,333 @@
package eu.darken.capod.common.upgrade.ui
import android.widget.Toast
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Button
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.twotone.AutoAwesome
import androidx.compose.material.icons.twotone.Favorite
import androidx.compose.material.icons.twotone.Info
import androidx.compose.material.icons.twotone.Verified
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LifecycleEventEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
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
import eu.darken.capod.common.navigation.Nav
import androidx.compose.ui.unit.dp
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
// Which presentation the FOSS upgrade screen shows: the classic support pitch, or one of the
// status views behind the settings "upgrade status" entry.
internal enum class FossUpgradeView {
PITCH,
STATUS_FREE,
STATUS_UPGRADED,
}
// The Settings row and the PITCH title both read this one resource, so they cannot name the tier
// differently — unlike the composed brand title, this stays a support ask ("Sponsor CAPod") rather
// than naming the flavor. gplay's counterpart in UpgradeScreen.kt composes from brandTitleText
// instead; each flavor implementation lives only in its own source set.
@Composable
internal fun settingsUpgradeStatusTitle(): String = stringResource(R.string.upgrade_foss_sponsor_label)
@Composable
fun UpgradeScreenHost(
route: Nav.Main.Upgrade = Nav.Main.Upgrade(),
vm: UpgradeViewModel = hiltViewModel(),
) {
LaunchedEffect(route) { vm.bindRoute(route) }
ErrorEventHandler(vm)
NavigationEventHandler(vm)
val context = LocalContext.current
val snackbarHostState = remember { SnackbarHostState() }
// Seeded from the ViewModel's handle-backed pending launch: after a process death while the
// sponsor page was open, a blank tracker would swallow the very first return. The handle is the
// authority on whether a return is still expected, so it reconstructs the tracker's state.
val sponsorReturnTracker = remember(vm) {
SponsorReturnTracker(wentToBackground = vm.hasPendingSponsorLaunch())
}
LaunchedEffect(Unit) {
vm.snackbarEvents.collect { stringRes ->
snackbarHostState.showSnackbar(context.getString(stringRes))
}
}
LaunchedEffect(Unit) {
vm.toastEvents.collect { stringRes ->
Toast.makeText(context, context.getString(stringRes), Toast.LENGTH_LONG).show()
}
}
LifecycleEventEffect(Lifecycle.Event.ON_STOP) {
sponsorReturnTracker.onStop()
}
LifecycleEventEffect(Lifecycle.Event.ON_RESUME) {
if (sponsorReturnTracker.consumeResumeReturn()) {
vm.checkSponsorReturn()
}
}
val state by vm.state.collectAsStateWithLifecycle()
UpgradeScreen(
// Until the route binding lands (one frame): the default route keeps rendering the pitch
// exactly as before, only the manage route waits for the status decision.
view = state?.view ?: FossUpgradeView.PITCH.takeIf { !route.manage },
supporterSince = state?.supporterSince,
snackbarHostState = snackbarHostState,
onGithubSponsors = vm::goGithubSponsors,
onOpenSponsors = vm::openSponsors,
onShowUpgradeOptions = vm::onShowUpgradeOptions,
onNavigateUp = vm::navUp,
)
}
@Composable
internal fun UpgradeScreen(
view: FossUpgradeView? = FossUpgradeView.PITCH,
supporterSince: Instant? = null,
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
onGithubSponsors: () -> Unit = {},
onOpenSponsors: () -> Unit = {},
onShowUpgradeOptions: () -> Unit = {},
onNavigateUp: () -> Unit = {},
) {
UpgradeScreenScaffold(
// Status views describe the existing install, not a support ask — they get the composed
// flavor title, with the postfix highlighted for supporters like the dashboard does it.
title = if (view == FossUpgradeView.PITCH) {
AnnotatedString(settingsUpgradeStatusTitle())
} else {
// "CAPod FOSS", not "CAPod Pro": the FOSS flavor's own qualifier resource supplies the
// tier word, so the title names this build. The upgraded gate keeps the highlight for
// supporters only.
upgradeScreenTitle(upgraded = view == FossUpgradeView.STATUS_UPGRADED)
},
onNavigateUp = onNavigateUp,
snackbarHostState = snackbarHostState,
) { paddingValues ->
when (view) {
null -> Unit // Route not bound yet (single frame); content lands with the next state.
FossUpgradeView.PITCH -> UpgradePitchContent(
paddingValues = paddingValues,
onGithubSponsors = onGithubSponsors,
)
FossUpgradeView.STATUS_FREE -> UpgradeStatusFreeContent(
paddingValues = paddingValues,
onShowUpgradeOptions = onShowUpgradeOptions,
)
FossUpgradeView.STATUS_UPGRADED -> UpgradeStatusUpgradedContent(
paddingValues = paddingValues,
supporterSince = supporterSince,
onOpenSponsors = onOpenSponsors,
)
}
}
}
@Composable
private fun UpgradePitchContent(
paddingValues: PaddingValues,
onGithubSponsors: () -> Unit,
) {
UpgradeScreenContent(
paddingValues = paddingValues,
) {
UpgradeHeroCard(
text = stringResource(R.string.upgrade_foss_preamble),
colors = CardDefaults.elevatedCardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
),
)
UpgradeSectionCard(
title = stringResource(R.string.upgrade_screen_why_title),
icon = Icons.TwoTone.AutoAwesome,
) {
UpgradeFeatureList(text = upgradeBenefitsText())
}
UpgradeSectionCard(
title = stringResource(R.string.upgrade_screen_how_title),
icon = Icons.TwoTone.Favorite,
) {
UpgradeSectionBody(text = stringResource(R.string.upgrade_screen_how_body))
}
UpgradeActionCard(
colors = CardDefaults.elevatedCardColors(
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
contentColor = MaterialTheme.colorScheme.onTertiaryContainer,
),
) {
Button(
onClick = onGithubSponsors,
modifier = Modifier
.fillMaxWidth()
.testTag(UpgradeScreenTags.FOSS_SPONSOR),
) {
Text(stringResource(R.string.upgrade_foss_sponsor_action))
}
UpgradeHintText(text = stringResource(R.string.upgrade_foss_sponsor_subtitle))
}
}
}
@Composable
private fun UpgradeStatusFreeContent(
paddingValues: PaddingValues,
onShowUpgradeOptions: () -> Unit,
) {
UpgradeScreenContent(
paddingValues = paddingValues,
) {
UpgradeHeader(
mascotSize = 104.dp,
)
UpgradeSectionCard(
title = stringResource(R.string.upgrade_screen_status_free_title),
icon = Icons.TwoTone.Info,
modifier = Modifier.testTag(UpgradeScreenTags.FOSS_STATUS_FREE),
) {
UpgradeSectionBody(text = stringResource(R.string.upgrade_screen_status_free_body))
Button(
onClick = onShowUpgradeOptions,
modifier = Modifier
.fillMaxWidth()
.testTag(UpgradeScreenTags.FOSS_SHOW_OPTIONS),
) {
Text(stringResource(R.string.upgrade_screen_status_free_action))
}
}
}
}
@Composable
private fun UpgradeStatusUpgradedContent(
paddingValues: PaddingValues,
supporterSince: Instant? = null,
onOpenSponsors: () -> Unit,
) {
UpgradeScreenContent(
paddingValues = paddingValues,
) {
UpgradeHeader(
mascotSize = 104.dp,
)
UpgradeSectionCard(
title = stringResource(R.string.upgrade_screen_status_upgraded_title),
icon = Icons.TwoTone.Verified,
modifier = Modifier.testTag(UpgradeScreenTags.FOSS_STATUS_UPGRADED),
colors = CardDefaults.elevatedCardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
),
) {
Text(
text = stringResource(R.string.upgrade_foss_supporter_thanks),
style = MaterialTheme.typography.bodyMedium,
)
supporterSince?.let { since ->
val formatter = remember {
DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withZone(ZoneId.systemDefault())
}
Text(
text = stringResource(R.string.upgrade_foss_supporter_since, formatter.format(since)),
style = MaterialTheme.typography.bodySmall,
)
}
}
UpgradeSectionCard(
title = stringResource(R.string.upgrade_screen_recurring_title),
icon = Icons.TwoTone.Favorite,
) {
UpgradeSectionBody(text = stringResource(R.string.upgrade_screen_recurring_body))
OutlinedButton(
onClick = onOpenSponsors,
modifier = Modifier
.fillMaxWidth()
.testTag(UpgradeScreenTags.FOSS_DONATE),
) {
Text(stringResource(R.string.upgrade_foss_sponsor_again_action))
}
}
}
}
internal class SponsorReturnTracker(
private var wentToBackground: Boolean = false,
) {
fun onStop() {
wentToBackground = true
}
fun consumeResumeReturn(): Boolean {
return if (wentToBackground) {
wentToBackground = false
true
} else {
false
}
}
}
@Preview2
@Composable
private fun UpgradeScreenPreview() {
PreviewWrapper {
UpgradeScreen()
}
}
@Preview2
@Composable
private fun UpgradeScreenStatusFreePreview() {
PreviewWrapper {
UpgradeScreen(view = FossUpgradeView.STATUS_FREE)
}
}
@Preview2
@Composable
private fun UpgradeScreenStatusUpgradedPreview() {
PreviewWrapper {
UpgradeScreen(
view = FossUpgradeView.STATUS_UPGRADED,
supporterSince = Instant.ofEpochMilli(1_700_000_000_000L),
)
}
}
@@ -0,0 +1,198 @@
package eu.darken.capod.common.upgrade.ui
import android.os.SystemClock
import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.lifecycle.HiltViewModel
import eu.darken.capod.R
import eu.darken.capod.common.coroutine.DispatcherProvider
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.flow.SingleEventFlow
import eu.darken.capod.common.navigation.Nav
import eu.darken.capod.common.uix.ViewModel4
import eu.darken.capod.common.upgrade.core.UpgradeRepoFoss
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.take
import java.time.Instant
import javax.inject.Inject
@HiltViewModel
class UpgradeViewModel @Inject constructor(
private val handle: SavedStateHandle,
dispatcherProvider: DispatcherProvider,
private val upgradeRepo: UpgradeRepoFoss,
) : ViewModel4(dispatcherProvider = dispatcherProvider) {
// Route is bound from the Host via bindRoute(); SavedStateHandle.toRoute<>() crashes under Nav3.
private val routeFlow = MutableStateFlow<Nav.Main.Upgrade?>(null)
fun bindRoute(route: Nav.Main.Upgrade) {
if (routeFlow.value != null) return
routeFlow.value = route
}
val snackbarEvents = SingleEventFlow<Int>()
val toastEvents = SingleEventFlow<Int>()
// Which presentation the screen shows. The manage route (settings "upgrade status" entry)
// gets a status view first; the pitch only appears once a free user asks for the upgrade
// options. Upgrading wins on EVERY route, not just manage: forced routes (Pro-locked settings)
// stay open after the sponsor flow completes, and the pitch with its live sponsor button reads
// as "sponsoring didn't work" to a fresh supporter — the toast alone is too transient for a
// money moment without a receipt behind it. null until the route is bound.
internal val state: StateFlow<State?> = combine(
routeFlow,
upgradeRepo.upgradeInfo,
handle.getStateFlow(KEY_SHOW_UPGRADE_OPTIONS, false),
) { route, info, showOptions ->
val view = when {
route == null -> null
info.isPro -> FossUpgradeView.STATUS_UPGRADED
route.manage && !showOptions -> FossUpgradeView.STATUS_FREE
else -> FossUpgradeView.PITCH
}
// Derived in the same emission as the view on purpose: a sibling flow would let the
// upgraded status render for a frame without the date it is supposed to carry.
view?.let {
State(
view = it,
supporterSince = info.upgradedAt.takeIf { _ -> it == FossUpgradeView.STATUS_UPGRADED },
)
}
}.safeStateIn(
initialValue = null,
onError = { State(view = FossUpgradeView.PITCH) },
)
// internal like FossUpgradeView: the view enum is a screen-local presentation detail.
internal data class State(
val view: FossUpgradeView,
val supporterSince: Instant? = null,
)
init {
routeFlow
.filterNotNull()
.take(1)
.onEach { route ->
// The manage route is the settings "upgrade status" entry — upgraded users must
// not be bounced out. Forced routes keep their existing don't-auto-close semantics.
if (!route.forced && !route.manage) {
upgradeRepo.upgradeInfo
.filter { it.isPro }
.take(1)
.onEach { navUp() }
.launchInViewModel()
}
}
.launchInViewModel()
upgradeRepo.upgradeInfo
.filter { !it.isPro && it.error != null }
.onEach { current ->
@Suppress("UNNECESSARY_NOT_NULL_ASSERTION")
errorEvents.tryEmit(current.error!!)
}
.launchInViewModel()
}
fun onShowUpgradeOptions() {
log(TAG) { "onShowUpgradeOptions()" }
// Handle-backed: surviving process recreation keeps the user on the pitch they asked for.
handle[KEY_SHOW_UPGRADE_OPTIONS] = true
}
/** Armed variant: the pitch's sponsor button, which starts the return-after-5s unlock heuristic. */
fun goGithubSponsors() {
log(TAG) { "goGithubSponsors()" }
if (hasPendingSponsorLaunch()) {
log(TAG) { "A sponsor launch is already awaiting its return" }
return
}
// Only arm the heuristic if the page actually opened; otherwise an unrelated later
// background/foreground round-trip would grant supporter status with no page ever shown.
if (!upgradeRepo.openGithubSponsorsPage()) {
log(TAG) { "Sponsor page didn't open; not arming the unlock heuristic" }
return
}
handle[KEY_SPONSOR_PRESSED_AT] = SystemClock.elapsedRealtime()
}
/**
* Unarmed variant: the status view's donate button. An existing supporter re-visiting the page
* must not re-arm the unlock heuristic — there is nothing left to unlock.
*/
fun openSponsors() {
log(TAG) { "openSponsors()" }
upgradeRepo.openGithubSponsorsPage()
}
/**
* Whether a sponsor-page launch is still awaiting its return.
*
* Handle-backed, so it survives process recreation while the browser is in front — the screen's
* in-memory return tracker does not, and gating on that alone drops the first return after a
* recreation.
*/
fun hasPendingSponsorLaunch(): Boolean = handle.contains(KEY_SPONSOR_PRESSED_AT)
fun checkSponsorReturn() = launch {
val pressedAt = handle.remove<Long>(KEY_SPONSOR_PRESSED_AT) ?: return@launch
try {
// Evaluated before the duration: an already upgraded supporter (recurring donation
// button) has nothing left to unlock, so this fast path exists for the UX — return
// quietly, no redundant write attempt and no thanks toast for an unlock that already
// happened. Data integrity is not this guard's job: the repo's create-only transaction
// owns that.
if (upgradeRepo.upgradeInfo.first().isPro) {
log(TAG) { "checkSponsorReturn(): Already upgraded, staying quiet" }
return@launch
}
val elapsed = SystemClock.elapsedRealtime() - pressedAt
log(TAG) { "checkSponsorReturn(): elapsed=${elapsed}ms" }
if (elapsed < SPONSOR_DELAY_MS) {
log(TAG) { "checkSponsorReturn(): Too quick, showing snackbar" }
snackbarEvents.tryEmit(R.string.upgrade_foss_sponsor_returned_early)
} else {
log(TAG) { "checkSponsorReturn(): Delay passed, persisting upgrade" }
val created = upgradeRepo.persistUpgrade()
if (created) {
toastEvents.tryEmit(R.string.upgrade_foss_supporter_thanks)
} else {
// The isPro fast-path read a stale emission; the transaction kept the existing record.
log(TAG) { "checkSponsorReturn(): Record already existed, staying quiet" }
}
}
} catch (e: Exception) {
// The marker was consumed above; neither a failed entitlement read nor a failed write may
// eat the user's valid sponsor visit — restore it so the next return/resume can retry the
// unlock. Conditional: the user may have armed a NEWER launch while this attempt was
// suspended, and that one must survive. The contains-check has a small check-then-act
// window against a concurrent new arm; accepted — the create-only transaction owns data
// integrity, a wrong winner only changes which REAL visit's timestamp gates the unlock.
// Rethrow unconditionally: cancellation is not swallowed, other errors surface via the
// normal error path. A restored marker after a successful persist is harmless — the next
// evaluation hits the quiet isPro path.
if (!handle.contains(KEY_SPONSOR_PRESSED_AT)) {
handle[KEY_SPONSOR_PRESSED_AT] = pressedAt
}
throw e
}
}
companion object {
private const val KEY_SPONSOR_PRESSED_AT = "sponsor_pressed_at"
private const val KEY_SHOW_UPGRADE_OPTIONS = "show_upgrade_options"
private const val SPONSOR_DELAY_MS = 5_000L
private val TAG = logTag("Upgrade", "ViewModel")
}
}
@@ -1,271 +0,0 @@
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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.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.Headphones
import androidx.compose.material.icons.twotone.Tune
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.style.TextAlign
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.AutoMirrored.TwoTone.Message, R.string.upgrade_benefit_popups),
Benefit(Icons.TwoTone.Widgets, R.string.upgrade_benefit_widgets),
Benefit(Icons.TwoTone.Tune, R.string.upgrade_benefit_device_settings),
Benefit(Icons.TwoTone.Headphones, R.string.upgrade_benefit_device_controls),
Benefit(Icons.TwoTone.Favorite, R.string.upgrade_benefit_support),
)
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) },
containerColor = MaterialTheme.colorScheme.surface,
) { paddingValues ->
Box(modifier = Modifier.padding(paddingValues)) {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Spacer(modifier = Modifier.height(48.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,
)
}
}
}
}
Text(
text = stringResource(R.string.upgrade_benefit_disclaimer),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 8.dp),
)
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
.align(Alignment.TopStart)
.padding(4.dp),
) {
Icon(
imageVector = Icons.AutoMirrored.TwoTone.ArrowBack,
contentDescription = null,
)
}
}
}
}
@Preview2
@Composable
private fun UpgradeScreenPreview() = PreviewWrapper {
UpgradeScreen(
onNavigateUp = {},
onSponsor = {},
)
}
@@ -1,48 +0,0 @@
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"
}
}
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Ondersteuner sedert %s</string>
<string name="upgrade_foss_supporter_thanks">Dankie dat jy CAPod se ontwikkeling ondersteun!</string>
<string name="upgrade_foss_sponsor_again_action">Maak borgblad oop</string>
<string name="upgrade_foss_sponsor_label">Ondersteun CAPod</string>
<string name="settings_upgrade_status_description">Jou ondersteunersstatus.</string>
<string name="upgrade_screen_why_title">Opgraderingsvoordele</string>
<string name="upgrade_screen_how_title">Hoe om te help</string>
<string name="upgrade_screen_how_body">Word \'n beskermheer en borg ontwikkeling! Tik die knoppie hieronder om alle ekstra funksies te aktiveer en my GitHub Sponsors profiel oop te maak.</string>
<string name="upgrade_screen_status_free_title">Gratis weergawe</string>
<string name="upgrade_screen_status_free_body">Jy gebruik die gratis weergawe van CAPod. Ekstra funksies kan ontsluit word deur die ontwikkeling te ondersteun.</string>
<string name="upgrade_screen_status_free_action">Sien opgraderingsopsies</string>
<string name="upgrade_screen_status_upgraded_title">Opgradering aktief</string>
<string name="upgrade_screen_recurring_title">Hou dit voort</string>
<string name="upgrade_screen_recurring_body">CAPod bly verbeter deur opdaterings en regstellings. As jy dit wil volhou, oorweeg \'n herhalende skenking via GitHub Sponsors.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">ደጋፊ ከ%s ጀምሮ</string>
<string name="upgrade_foss_supporter_thanks">የCAPod ልማትን ስለደገፉ እናመሰግናለን!</string>
<string name="upgrade_foss_sponsor_again_action">የስፖንሰር ገጽን ክፈት</string>
<string name="upgrade_foss_sponsor_label">CAPod ን ደግፍ</string>
<string name="settings_upgrade_status_description">የእርስዎ ደጋፊ ሁኔታ።</string>
<string name="upgrade_screen_why_title">የማሻሻል ጥቅማጥቅሞች</string>
<string name="upgrade_screen_how_title">እንዴት መርዳት እንደሚችሉ</string>
<string name="upgrade_screen_how_body">ደጋፊ ይሁኑ እና ልማትን ይደግፉ! ሁሉንም ተጨማሪ ባህሪያት ለማንቃት እና የእኔን GitHub Sponsors መገለጫ ለመክፈት ከታች ያለውን ቁልፍ ይንኩ።</string>
<string name="upgrade_screen_status_free_title">ነጻ ስሪት</string>
<string name="upgrade_screen_status_free_body">CAPod ነጻ ስሪት ይጠቀማሉ። ተጨማሪ ባህሪዎች በልማት ድጋፍ ሊከፈቱ ይችላሉ።</string>
<string name="upgrade_screen_status_free_action">የማሻሻል አማራጮችን ይመልከቱ</string>
<string name="upgrade_screen_status_upgraded_title">ማሻሻል ንቁ ነው</string>
<string name="upgrade_screen_recurring_title">ይህን ይቀጥሉ</string>
<string name="upgrade_screen_recurring_body">CAPod በዝመናዎች እና ማስተካከያዎች ሁሉ ጊዜ ይሻሻላል። ይህንን ለማቆየት ከወደዱ፣ በGitHub Sponsors ደግሞ ምስዋትን ያስቡ።</string>
</resources>
+20 -7
View File
@@ -1,11 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<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="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">برنامج كابود FOSS مجاني ومفتوح المصدر. إذا وجدته مفيدًا، ففكر في دعم تطويره للمساعدة في استمرار المشروع.</string>
<string name="upgrade_foss_sponsor_action">دعم التطوير</string>
<string name="upgrade_foss_sponsor_subtitle">لا إعلانات. لا تتبُّع. لا قيود من جوجل بلاي.</string>
<string name="upgrade_foss_sponsor_returned_early">هل عدت بالفعل؟ دعمك يُبقي CAPod حيًّا.</string>
<string name="upgrade_badge_label">البرمجيات الحرة والمفتوحة المصدر</string>
<string name="upgrade_foss_sponsor_subtitle">لا إعلانات. لا تتبع. لا قيود من جوجل بلاي.</string>
<string name="upgrade_foss_sponsor_returned_early">هل عدت بالفعل؟ دعمك يبقي كابود حيًّا.</string>
<string name="upgrade_foss_supporter_since">داعم منذ %s</string>
<string name="upgrade_foss_supporter_thanks">شكرًا لدعمك تطوير CAPod!</string>
<string name="upgrade_foss_sponsor_again_action">فتح صفحة الرعاية</string>
<string name="upgrade_foss_sponsor_label">دعم كابود</string>
<string name="settings_upgrade_status_description">حالة دعمك.</string>
<string name="upgrade_screen_why_title">مزايا الترقية</string>
<string name="upgrade_screen_how_title">كيفية المساعدة</string>
<string name="upgrade_screen_how_body">كُن راعيًا وساهم في تطوير المشروع! اضغط على الزرّ أدناه لتفعيل جميع الميزات الإضافية وفتح ملفّي الشخصي على GitHub Sponsors.</string>
<string name="upgrade_screen_status_free_title">النسخة المجّانية</string>
<string name="upgrade_screen_status_free_body">أنت تستخدم النسخة المجّانية من كابود. يمكنك فتح ميزات إضافية من خلال دعم التطوير.</string>
<string name="upgrade_screen_status_free_action">الاطّلاع على خيارات الترقية</string>
<string name="upgrade_screen_status_upgraded_title">الترقية نشطة</string>
<string name="upgrade_screen_recurring_title">أبقِ التطبيق مستمرًّا</string>
<string name="upgrade_screen_recurring_body">يستمرّ كابود في التطوّر من خلال التحديثات والإصلاحات. إذا كنت ترغب في دعم هذا التطوّر، ففكّر في التبرّع بشكل دوري عبر GitHub Sponsors.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">%s tarixindən bəri dəstəkçi</string>
<string name="upgrade_foss_supporter_thanks">CAPod-un inkişafını dəstəklədiyiniz üçün təşəkkürlər!</string>
<string name="upgrade_foss_sponsor_again_action">Sponsor səhifəsini aç</string>
<string name="upgrade_foss_sponsor_label">CAPod-u sponsorlaşdırın</string>
<string name="settings_upgrade_status_description">Sizin tərəfdar statusu.</string>
<string name="upgrade_screen_why_title">Yüksəltmə üstünləri</string>
<string name="upgrade_screen_how_title">Necə kömək etmək</string>
<string name="upgrade_screen_how_body">Himayədar olun və inkişafı sponsor edin! Bütün əlavə xüsusiyyətləri aktivləşdirmək və mənim GitHub Sponsors profilimi açmaq üçün aşağıdakı düyməni toxunun.</string>
<string name="upgrade_screen_status_free_title">Pulsuz versiya</string>
<string name="upgrade_screen_status_free_body">Siz CAPod-un pulsuz versiyasından istifadə edirsiniz. Əlavə xüsusiyyətlər inkişafı dəstəkləyərək açılır.</string>
<string name="upgrade_screen_status_free_action">Yüksəltmə variantlarına baxın</string>
<string name="upgrade_screen_status_upgraded_title">Yüksəltmə aktiv</string>
<string name="upgrade_screen_recurring_title">Davam etdirin</string>
<string name="upgrade_screen_recurring_body">CAPod yeniləmələr və düzəltmələr vasitəsilə təkamül etməkdə davam edir. Bunu dəstəkləmək istəsəniz, GitHub Sponsors vasitəsilə təkrari donasiya nəzərdən keçirin.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Спонсар з %s</string>
<string name="upgrade_foss_supporter_thanks">Дзякуй за падтрымку развіцця CAPod!</string>
<string name="upgrade_foss_sponsor_again_action">Адкрыць старонку спонсарства</string>
<string name="upgrade_foss_sponsor_label">Спонсараваць CAPod</string>
<string name="settings_upgrade_status_description">Ваш статус прыхільніка.</string>
<string name="upgrade_screen_why_title">Перавагі абнаўлення</string>
<string name="upgrade_screen_how_title">Як дапамагчы</string>
<string name="upgrade_screen_how_body">Станьце спонсарам распрацоўкі! Націсніце кнопку ніжэй, каб актывіраваць усе дадатковыя функцыі і адкрыць мой профіль на GitHub Sponsors.</string>
<string name="upgrade_screen_status_free_title">Бясплатная версія</string>
<string name="upgrade_screen_status_free_body">Вы выкарыстоўваеце бясплатную версію CAPod. Дадатковыя функцыі можна разблакаваць, падтрымаўшы развіццё.</string>
<string name="upgrade_screen_status_free_action">Прагледзець варыянты абнаўлення</string>
<string name="upgrade_screen_status_upgraded_title">Абнаўленне актыўнае</string>
<string name="upgrade_screen_recurring_title">Падтрымайце далей</string>
<string name="upgrade_screen_recurring_body">CAPod пастаянна развіваецца дзякуючы абнаўленням і выпраўленням. Калі вы хочаце гэта падтрымаць, разгледзьце магчымасць рэгулярнага ахвяравання праз GitHub Sponsors.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Поддържник от %s</string>
<string name="upgrade_foss_supporter_thanks">Благодарим ви, че подкрепяте развитието на CAPod!</string>
<string name="upgrade_foss_sponsor_again_action">Отвори страницата за спонсорство</string>
<string name="upgrade_foss_sponsor_label">Спонсорирайте CAPod</string>
<string name="settings_upgrade_status_description">Вашият статус на поддръжник.</string>
<string name="upgrade_screen_why_title">Преимущества на надстройката</string>
<string name="upgrade_screen_how_title">Как да помогнете</string>
<string name="upgrade_screen_how_body">Станете покровител и спонсорирайте разработката! Докоснете бутона по-долу, за да активирате всички допълнителни функции и да отворите моя GitHub Sponsors профил.</string>
<string name="upgrade_screen_status_free_title">Безплатна версия</string>
<string name="upgrade_screen_status_free_body">Използвате безплатната версия на CAPod. Допълнителни функции могат да бъдат отключени чрез подкрепа на разработката.</string>
<string name="upgrade_screen_status_free_action">Вижте опциите за надстройка</string>
<string name="upgrade_screen_status_upgraded_title">Надстройката е активна</string>
<string name="upgrade_screen_recurring_title">Поддържете това</string>
<string name="upgrade_screen_recurring_body">CAPod продължава да се развива чрез актуализации и корекции. Ако искате да подкрепите това, разгледайте възможност за редовно дарение чрез GitHub Sponsors.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">%s থেকে সমর্থক</string>
<string name="upgrade_foss_supporter_thanks">CAPod-এর উন্নয়নে সমর্থন করার জন্য আপনাকে ধন্যবাদ!</string>
<string name="upgrade_foss_sponsor_again_action">স্পনসর পৃষ্ঠা খুলুন</string>
<string name="upgrade_foss_sponsor_label">CAPod-কে স্পন্সর করুন</string>
<string name="settings_upgrade_status_description">আপনার সমর্থক স্থিতি।</string>
<string name="upgrade_screen_why_title">আপগ্রেড সুবিধা</string>
<string name="upgrade_screen_how_title">কিভাবে সাহায্য করতে পারি</string>
<string name="upgrade_screen_how_body">একটি পৃষ্ঠপোষক এবং স্পনসর উন্নয়ন হয়ে! সমস্ত অতিরিক্ত বৈশিষ্ট্য সক্রিয় করতে নীচের বোতামটি আলতো চাপুন এবং আমার GitHub স্পনসর প্রোফাইল খুলুন।</string>
<string name="upgrade_screen_status_free_title">বিনামূল্যে সংস্করণ</string>
<string name="upgrade_screen_status_free_body">আপনি CAPod এর বিনামূল্যে সংস্করণ ব্যবহার করছেন। উন্নয়ন সমর্থন করে অতিরিক্ত বৈশিষ্ট্য আনলক করা যায়।</string>
<string name="upgrade_screen_status_free_action">আপগ্রেড বিকল্পগুলি দেখুন</string>
<string name="upgrade_screen_status_upgraded_title">আপগ্রেড সক্রিয়</string>
<string name="upgrade_screen_recurring_title">এটি চালু রাখুন</string>
<string name="upgrade_screen_recurring_body">CAPod আপডেট এবং সংশোধনের মাধ্যমে ক্রমাগত উন্নত হচ্ছে। আপনি যদি তা বজায় রাখতে চান তবে GitHub Sponsors এর মাধ্যমে নিয়মিত দান বিবেচনা করুন।</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Col·laborador des de %s</string>
<string name="upgrade_foss_supporter_thanks">Gràcies per donar suport al desenvolupament del CAPod!</string>
<string name="upgrade_foss_sponsor_again_action">Obre la pàgina del patrocinador</string>
<string name="upgrade_foss_sponsor_label">Patrocineu el CAPod</string>
<string name="settings_upgrade_status_description">El vostre estat de col·laborador.</string>
<string name="upgrade_screen_why_title">Beneficis de la millora</string>
<string name="upgrade_screen_how_title">Com ajudar</string>
<string name="upgrade_screen_how_body">Convertiu-vos en mecenes i patrocinadors del desenvolupament! Toqueu el botó següent per activar totes les funcions addicionals i obrir el meu perfil de patrocinadors del GitHub.</string>
<string name="upgrade_screen_status_free_title">Versió gratuïta</string>
<string name="upgrade_screen_status_free_body">Esteu utilitzant la versió gratuïta de CAPod. Podeu desbloquejar funcions addicionals donant suport al desenvolupament.</string>
<string name="upgrade_screen_status_free_action">Veure opcions de millora</string>
<string name="upgrade_screen_status_upgraded_title">Millora activa</string>
<string name="upgrade_screen_recurring_title">Manteniu-ho en marxa</string>
<string name="upgrade_screen_recurring_body">CAPod continua evolucionant a través d\'actualitzacions i correccions. Si voleu sostenir-ho, considereu una donació recurrent a través de GitHub Sponsors.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Podporovatel od %s</string>
<string name="upgrade_foss_supporter_thanks">Děkujeme za podporu vývoje CAPod!</string>
<string name="upgrade_foss_sponsor_again_action">Otevřít stránku sponzora</string>
<string name="upgrade_foss_sponsor_label">Sponzorovat CAPod</string>
<string name="settings_upgrade_status_description">Váš stav podporovatele.</string>
<string name="upgrade_screen_why_title">Výhody upgradu</string>
<string name="upgrade_screen_how_title">Jak pomoci</string>
<string name="upgrade_screen_how_body">Staňte se patronem a sponzorem vývoje! Klepnutím na tlačítko níže aktivujete všechny další funkce a otevřete můj profil sponzora na GitHubu.</string>
<string name="upgrade_screen_status_free_title">Bezplatná verze</string>
<string name="upgrade_screen_status_free_body">Používáte bezplatnou verzi CAPod. Další funkce lze odemknout podporou vývoje.</string>
<string name="upgrade_screen_status_free_action">Zobrazit možnosti upgradu</string>
<string name="upgrade_screen_status_upgraded_title">Upgrade je aktivní</string>
<string name="upgrade_screen_recurring_title">Pokračuj v tom</string>
<string name="upgrade_screen_recurring_body">CAPod se neustále vyvíjí prostřednictvím aktualizací a oprav. Pokud to chcete podporovat, zvažte pravidelný příspěvek prostřednictvím GitHub sponzorů.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Støtte siden %s</string>
<string name="upgrade_foss_supporter_thanks">Tak fordi du støtter udviklingen af CAPod!</string>
<string name="upgrade_foss_sponsor_again_action">Åbn sponsorside</string>
<string name="upgrade_foss_sponsor_label">Sponsor CAPod</string>
<string name="settings_upgrade_status_description">Din supporterstatus.</string>
<string name="upgrade_screen_why_title">Opgraderingens fordele</string>
<string name="upgrade_screen_how_title">Sådan hjælper du</string>
<string name="upgrade_screen_how_body">Bliv patron og sponsorér udvikling! Tryk på knappen nedenfor for at aktivere alle ekstra funktioner og åbne min GitHub-sponsorprofil.</string>
<string name="upgrade_screen_status_free_title">Gratis version</string>
<string name="upgrade_screen_status_free_body">Du bruger den gratis version af CAPod. Ekstra funktioner kan låses op ved at støtte udviklingen.</string>
<string name="upgrade_screen_status_free_action">Se opgraderingsmuligheder</string>
<string name="upgrade_screen_status_upgraded_title">Opgradering aktiv</string>
<string name="upgrade_screen_recurring_title">Hold det kørende</string>
<string name="upgrade_screen_recurring_body">CAPod udvikles løbende gennem opdateringer og fejlrettelser. Hvis du gerne vil understøtte det, kan du overveje en tilbagevendende donation via GitHub Sponsors.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Unterstützer seit %s</string>
<string name="upgrade_foss_supporter_thanks">Vielen Dank, dass du die Entwicklung von CAPod unterstützt!</string>
<string name="upgrade_foss_sponsor_again_action">Sponsoring-Seite öffnen</string>
<string name="upgrade_foss_sponsor_label">CAPod unterstützen</string>
<string name="settings_upgrade_status_description">Dein Unterstützerstatus.</string>
<string name="upgrade_screen_why_title">Vorteile des Upgrades</string>
<string name="upgrade_screen_how_title">Wie man hilft</string>
<string name="upgrade_screen_how_body">Werde Unterstützer und fördere die Entwicklung! Tippe auf den Button unten, um alle Zusatzfunktionen zu aktivieren und mein GitHub-Sponsors-Profil zu öffnen.</string>
<string name="upgrade_screen_status_free_title">Kostenlose Version</string>
<string name="upgrade_screen_status_free_body">Du verwendest die kostenlose Version von CAPod. Zusätzliche Funktionen können durch die Unterstützung der Entwicklung freigeschaltet werden.</string>
<string name="upgrade_screen_status_free_action">Upgrade-Optionen ansehen</string>
<string name="upgrade_screen_status_upgraded_title">Upgrade aktiv</string>
<string name="upgrade_screen_recurring_title">Projekt unterstützen</string>
<string name="upgrade_screen_recurring_body">CAPod entwickelt sich durch Updates und Fehlerbehebungen kontinuierlich weiter. Wenn du das unterstützen möchtest, ziehe eine wiederkehrende Spende über GitHub Sponsors in Betracht.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Υποστηρικτής από %s</string>
<string name="upgrade_foss_supporter_thanks">Σας ευχαριστούμε που υποστηρίζετε την ανάπτυξη του CAPod!</string>
<string name="upgrade_foss_sponsor_again_action">Άνοιγμα σελίδας χορηγίας</string>
<string name="upgrade_foss_sponsor_label">Χορηγήστε το CAPod</string>
<string name="settings_upgrade_status_description">Η κατάστασή σας ως υποστηρικτή.</string>
<string name="upgrade_screen_why_title">Πλεονεκτήματα αναβάθμισης</string>
<string name="upgrade_screen_how_title">Πώς να βοηθήσετε</string>
<string name="upgrade_screen_how_body">Γίνετε patron και χορηγήστε την ανάπτυξη! Πατήστε το κουμπί παρακάτω για να ενεργοποιήσετε όλα τα επιπλέον χαρακτηριστικά και ανοίξτε το προφίλ μου GitHub Sponsors.</string>
<string name="upgrade_screen_status_free_title">Δωρεάν έκδοση</string>
<string name="upgrade_screen_status_free_body">Χρησιμοποιείτε τη δωρεάν έκδοση του CAPod. Επιπλέον δυνατότητες μπορούν να ξεκλειδωθούν υποστηρίζοντας την ανάπτυξη.</string>
<string name="upgrade_screen_status_free_action">Δείτε τις επιλογές αναβάθμισης</string>
<string name="upgrade_screen_status_upgraded_title">Αναβάθμιση ενεργή</string>
<string name="upgrade_screen_recurring_title">Κρατήστε το ζωντανό</string>
<string name="upgrade_screen_recurring_body">Το CAPod συνεχίζει να εξελίσσεται μέσω ενημερώσεων και διορθώσεων. Αν θέλετε να το στηρίξετε, σκεφτείτε μια επαναλαμβανόμενη δωρεά μέσω του GitHub Sponsors.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Colaborador desde %s</string>
<string name="upgrade_foss_supporter_thanks">¡Gracias por apoyar el desarrollo de CAPod!</string>
<string name="upgrade_foss_sponsor_again_action">Abrir página de patrocinio</string>
<string name="upgrade_foss_sponsor_label">Apoyá CAPod</string>
<string name="settings_upgrade_status_description">Tu estado de patrocinador.</string>
<string name="upgrade_screen_why_title">Beneficios de la mejora</string>
<string name="upgrade_screen_how_title">Como ayudar</string>
<string name="upgrade_screen_how_body">¡Conviértete en patrocinador y patrocina el desarrollo! Toque el botón de abajo para activar todas las funciones adicionales y abrir mi perfil de Patrocinadores de GitHub.</string>
<string name="upgrade_screen_status_free_title">Versión gratuita</string>
<string name="upgrade_screen_status_free_body">Estás usando la versión gratuita de CAPod. Se pueden desbloquear características adicionales apoyando el desarrollo.</string>
<string name="upgrade_screen_status_free_action">Ver opciones de mejora</string>
<string name="upgrade_screen_status_upgraded_title">Mejora activa</string>
<string name="upgrade_screen_recurring_title">Mantené el impulso</string>
<string name="upgrade_screen_recurring_body">CAPod sigue evolucionando a través de actualizaciones y correcciones. Si querés sostener eso, considera una donación recurrente a través de GitHub Sponsors.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Colaborador desde %s</string>
<string name="upgrade_foss_supporter_thanks">¡Gracias por apoyar el desarrollo de CAPod!</string>
<string name="upgrade_foss_sponsor_again_action">Abrir página de patrocinio</string>
<string name="upgrade_foss_sponsor_label">Patrocina CAPod</string>
<string name="settings_upgrade_status_description">Tu estado de patrocinador.</string>
<string name="upgrade_screen_why_title">Beneficios de la actualización</string>
<string name="upgrade_screen_how_title">Cómo ayudar</string>
<string name="upgrade_screen_how_body">¡Conviértete en un patrocinador y apoya el desarrollo constante de la aplicación! Presiona el botón a continuación para activar todas las funciones adicionales además de abrir mi perfil de Patrocinadores en GitHub.</string>
<string name="upgrade_screen_status_free_title">Versión gratuita</string>
<string name="upgrade_screen_status_free_body">Estás usando la versión gratuita de CAPod. Puedes desbloquear características extra apoyando el desarrollo.</string>
<string name="upgrade_screen_status_free_action">Ver opciones de actualización</string>
<string name="upgrade_screen_status_upgraded_title">Actualización activa</string>
<string name="upgrade_screen_recurring_title">Sigue adelante</string>
<string name="upgrade_screen_recurring_body">CAPod sigue evolucionando con actualizaciones y correcciones. Si quieres sostener eso, considera una donación recurrente a través de GitHub Sponsors.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Colaborador desde %s</string>
<string name="upgrade_foss_supporter_thanks">¡Gracias por apoyar el desarrollo de CAPod!</string>
<string name="upgrade_foss_sponsor_again_action">Abrir página de patrocinio</string>
<string name="upgrade_foss_sponsor_label">Patrocinar CAPod</string>
<string name="settings_upgrade_status_description">Tu estado como patrocinador.</string>
<string name="upgrade_screen_why_title">Beneficios de la actualización</string>
<string name="upgrade_screen_how_title">Como ayudar</string>
<string name="upgrade_screen_how_body">¡Conviértete en patrocinador y patrocina el desarrollo! Toque el botón de abajo para activar todas las funciones adicionales y abrir mi perfil de Patrocinadores de GitHub.</string>
<string name="upgrade_screen_status_free_title">Versión gratuita</string>
<string name="upgrade_screen_status_free_body">Estás usando la versión gratuita de CAPod. Puedes desbloquear funciones adicionales apoyando el desarrollo.</string>
<string name="upgrade_screen_status_free_action">Ver opciones de actualización</string>
<string name="upgrade_screen_status_upgraded_title">Actualización activa</string>
<string name="upgrade_screen_recurring_title">Apóyalo</string>
<string name="upgrade_screen_recurring_body">CAPod sigue evolucionando gracias a actualizaciones y correcciones. Si quieres mantener eso en marcha, considera una donación recurrente a través de GitHub Sponsors.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Toetaja alates %s</string>
<string name="upgrade_foss_supporter_thanks">Täname, et toetad CAPodi arendust!</string>
<string name="upgrade_foss_sponsor_again_action">Ava sponsorlehekülg</string>
<string name="upgrade_foss_sponsor_label">Rahasta CAPodi</string>
<string name="settings_upgrade_status_description">Sinu toetaja staatus.</string>
<string name="upgrade_screen_why_title">Uuendamise eelised</string>
<string name="upgrade_screen_how_title">Kuidas aidata?</string>
<string name="upgrade_screen_how_body">Hakake toetajaks ja rahastage arendamist. Kõikide lisavõimaluste kasutamiseks ja minu GitHub Sponsors profiili avamiseks puudutage alumist nuppu.</string>
<string name="upgrade_screen_status_free_title">Tasuta versioon</string>
<string name="upgrade_screen_status_free_body">Kasutate CAPodi tasuta versiooni. Lisavõimaluste kasutamiseks toetage arendust.</string>
<string name="upgrade_screen_status_free_action">Vaadake täiendamise valikuid</string>
<string name="upgrade_screen_status_upgraded_title">Uuendus kasutatav</string>
<string name="upgrade_screen_recurring_title">Jätkake nii</string>
<string name="upgrade_screen_recurring_body">CAPod areneb pidevalt värskenduste ja paranduste kaudu. Kui soovite seda toetada, kaaluge korduvat annetust GitHub Sponsorsi kaudu.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Laguntzaile %s(e)tik aurrera</string>
<string name="upgrade_foss_supporter_thanks">Eskerrik asko CAPod-en garapena babesten duzulako!</string>
<string name="upgrade_foss_sponsor_again_action">Ireki babesle-orria</string>
<string name="upgrade_foss_sponsor_label">CAPod babeslatu</string>
<string name="settings_upgrade_status_description">Zure babesle egoera</string>
<string name="upgrade_screen_why_title">Hobekuntza-onurak</string>
<string name="upgrade_screen_how_title">Nola lagundu</string>
<string name="upgrade_screen_how_body">Izan babesle eta garapen babeslari! Egin klik beheko botoian eginbide gehigarri guztiak aktibatzeko eta nire GitHub Sponsors profila irekitzeko.</string>
<string name="upgrade_screen_status_free_title">Doako bertsioa</string>
<string name="upgrade_screen_status_free_body">CAPod-en doako bertsioa erabiltzen ari zara. Funtzio gehigarriak desblokeatu ditzakezu garapena lagunduz.</string>
<string name="upgrade_screen_status_free_action">Hobekuntza-aukerak ikusi</string>
<string name="upgrade_screen_status_upgraded_title">Hobekuntza aktiboa</string>
<string name="upgrade_screen_recurring_title">Jarraitu</string>
<string name="upgrade_screen_recurring_body">CAPod eguneratzen eta konpondutzen jarraitzen du. Hori mantendu nahi baduzu, dohaintza periodikoa GitHub Sponsors bidez kontuan hartu.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">منبع‌آزاد</string>
<string name="upgrade_foss_supporter_since">حامی از %s</string>
<string name="upgrade_foss_supporter_thanks">از حمایت شما از توسعه CAPod سپاسگزاریم!</string>
<string name="upgrade_foss_sponsor_again_action">باز کردن صفحه حمایت مالی</string>
<string name="upgrade_foss_sponsor_label">حمایت از CAPod</string>
<string name="settings_upgrade_status_description">وضعیت حمایتی شما.</string>
<string name="upgrade_screen_why_title">مزایای ارتقا</string>
<string name="upgrade_screen_how_title">چگونه کمک کنیم</string>
<string name="upgrade_screen_how_body">به حامی تبدیل شوید و توسعه را حمایت کنید! برای فعال کردن همه ویژگی‌های اضافی و باز کردن نمایه GitHub Sponsors من، روی دکمه زیر ضربه بزنید.</string>
<string name="upgrade_screen_status_free_title">نسخه رایگان</string>
<string name="upgrade_screen_status_free_body">شما از نسخه رایگان CAPod استفاده می‌کنید. ویژگی‌های اضافی با حمایت از توسعه فعال می‌شوند.</string>
<string name="upgrade_screen_status_free_action">مشاهده گزینه‌های ارتقا</string>
<string name="upgrade_screen_status_upgraded_title">ارتقا فعال</string>
<string name="upgrade_screen_recurring_title">ادامه دهید</string>
<string name="upgrade_screen_recurring_body">CAPod از طریق بروزرسانی‌ها و رفع‌های خطا به تکامل خود ادامه می‌دهد. اگر می‌خواهید این تکامل ادامه یابد، کمک مالی مکرر از طریق GitHub Sponsors را در نظر بگیرید.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Tukija %s lähtien</string>
<string name="upgrade_foss_supporter_thanks">Kiitos, että tuet CAPodin kehitystä!</string>
<string name="upgrade_foss_sponsor_again_action">Avaa sponsorisivu</string>
<string name="upgrade_foss_sponsor_label">Tue CAPodia</string>
<string name="settings_upgrade_status_description">Kannattajantila</string>
<string name="upgrade_screen_why_title">Päivityksen edut</string>
<string name="upgrade_screen_how_title">Kuinka auttaa</string>
<string name="upgrade_screen_how_body">Ryhdy tukijaksi ja sponsoroi kehitystä! Napauta alla olevaa painiketta aktivoidaksesi kaikki lisäominaisuudet ja avataksesi GitHub Sponsors -profiilini.</string>
<string name="upgrade_screen_status_free_title">Ilmainen versio</string>
<string name="upgrade_screen_status_free_body">Käytät CAPodin ilmaista versiota. Lisäominaisuudet voidaan avata tukemalla kehitystä.</string>
<string name="upgrade_screen_status_free_action">Katso päivitysvaihtoehdot</string>
<string name="upgrade_screen_status_upgraded_title">Päivitys aktiivinen</string>
<string name="upgrade_screen_recurring_title">Pidä se käynnissä</string>
<string name="upgrade_screen_recurring_body">CAPod kehittyy jatkuvasti päivitysten ja korjausten kautta. Jos haluat tukea sitä, harkitse toistuvaa lahjoitusta GitHub Sponsorien kautta.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Tagasuporta simula %s</string>
<string name="upgrade_foss_supporter_thanks">Salamat sa pagsuporta sa development ng CAPod!</string>
<string name="upgrade_foss_sponsor_again_action">Buksan ang sponsor page</string>
<string name="upgrade_foss_sponsor_label">Suportahan ang CAPod</string>
<string name="settings_upgrade_status_description">Ang iyong status bilang supporter.</string>
<string name="upgrade_screen_why_title">Mga benepisyo ng upgrade</string>
<string name="upgrade_screen_how_title">Paano tumulong</string>
<string name="upgrade_screen_how_body">Maging patron at mag-sponsor ng development! I-tap ang button sa ibaba para ma-activate ang lahat ng extra features at buksan ang aking GitHub Sponsors profile.</string>
<string name="upgrade_screen_status_free_title">Libreng bersyon</string>
<string name="upgrade_screen_status_free_body">Gumagamit ka ng libreng bersyon ng CAPod. Ang mga extra features ay maaaring ma-unlock sa pamamagitan ng pag-suporta sa development.</string>
<string name="upgrade_screen_status_free_action">Tingnan ang mga opsyon sa upgrade</string>
<string name="upgrade_screen_status_upgraded_title">Aktibong upgrade</string>
<string name="upgrade_screen_recurring_title">Panatilihin ang pagpapatuloy</string>
<string name="upgrade_screen_recurring_body">Ang CAPod ay patuloy na umuunlad sa pamamagitan ng updates at fixes. Kung gusto mong suportahan iyan, isaalang-alang ang recurring donation sa pamamagitan ng GitHub Sponsors.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">Logiciel libre</string>
<string name="upgrade_foss_supporter_since">Soutien depuis %s</string>
<string name="upgrade_foss_supporter_thanks">Merci de soutenir le développement de CAPod !</string>
<string name="upgrade_foss_sponsor_again_action">Ouvrir la page de parrainage</string>
<string name="upgrade_foss_sponsor_label">Soutenir CAPod</string>
<string name="settings_upgrade_status_description">Votre statut de supporter.</string>
<string name="upgrade_screen_why_title">Avantages de la mise à niveau</string>
<string name="upgrade_screen_how_title">Comment aider</string>
<string name="upgrade_screen_how_body">Devenez mécène et soutenez le développement. Touchez le bouton ci-dessous pour activer toutes les fonctions supplémentaires et ouvrir mon profil GitHub Sponsors.</string>
<string name="upgrade_screen_status_free_title">Version gratuite</string>
<string name="upgrade_screen_status_free_body">Vous utilisez la version gratuite de CAPod. Des fonctionnalités supplémentaires peuvent être déverrouillées en soutenant le développement.</string>
<string name="upgrade_screen_status_free_action">Voir les options de mise à niveau</string>
<string name="upgrade_screen_status_upgraded_title">Mise à niveau active</string>
<string name="upgrade_screen_recurring_title">Gardez ça en route</string>
<string name="upgrade_screen_recurring_body">CAPod continue à évoluer grâce aux mises à jour et aux corrections. Si vous souhaitez soutenir cela, envisagez un don récurrent via GitHub Sponsors.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Colaborador desde %s</string>
<string name="upgrade_foss_supporter_thanks">Grazas por apoiar o desenvolvemento de CAPod!</string>
<string name="upgrade_foss_sponsor_again_action">Abrir a páxina de patrocinio</string>
<string name="upgrade_foss_sponsor_label">Apoia CAPod</string>
<string name="settings_upgrade_status_description">O teu estado de apoiador.</string>
<string name="upgrade_screen_why_title">Vantaxes da actualización</string>
<string name="upgrade_screen_how_title">Como axudar</string>
<string name="upgrade_screen_how_body">Convértete en mecenas e patrocina o desenvolvemento! Toca o botón de abaixo para activar todas as características extra e abrir o meu perfil de GitHub Sponsors.</string>
<string name="upgrade_screen_status_free_title">Versión gratuita</string>
<string name="upgrade_screen_status_free_body">Estás usando a versión gratuita de CAPod. As características adicionais pódense desbloquear apoiando o desenvolvemento.</string>
<string name="upgrade_screen_status_free_action">Ver opcións de actualización</string>
<string name="upgrade_screen_status_upgraded_title">Actualización activa</string>
<string name="upgrade_screen_recurring_title">Manténo en marcha</string>
<string name="upgrade_screen_recurring_body">CAPod segue evolucionando a través de actualizacións e correccións. Se queres mantelo así, considera unha doación recorrente a través de GitHub Sponsors.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">%s से समर्थक</string>
<string name="upgrade_foss_supporter_thanks">CAPod के विकास का समर्थन करने के लिए धन्यवाद!</string>
<string name="upgrade_foss_sponsor_again_action">स्पॉन्सर पेज खोलें</string>
<string name="upgrade_foss_sponsor_label">CAPod को प्रायोजित करें</string>
<string name="settings_upgrade_status_description">आपकी समर्थक स्थिति।</string>
<string name="upgrade_screen_why_title">अपग्रेड के लाभ</string>
<string name="upgrade_screen_how_title">कैसे सहायता करें</string>
<string name="upgrade_screen_how_body">संरक्षक बनें और विकास को प्रायोजित करें! सभी अतिरिक्त सुविधाओं को सक्रिय करने और मेरी GitHub Sponsors प्रोफ़ाइल खोलने के लिए नीचे दिए गए बटन को टैप करें।</string>
<string name="upgrade_screen_status_free_title">मुफ़्त संस्करण</string>
<string name="upgrade_screen_status_free_body">आप CAPod के मुफ़्त संस्करण का उपयोग कर रहे हैं। विकास का समर्थन करके अतिरिक्त सुविधाओं को अनलॉक किया जा सकता है।</string>
<string name="upgrade_screen_status_free_action">अपग्रेड विकल्प देखें</string>
<string name="upgrade_screen_status_upgraded_title">अपग्रेड सक्रिय</string>
<string name="upgrade_screen_recurring_title">जारी रखें</string>
<string name="upgrade_screen_recurring_body">CAPod अपडेट और सुधार के माध्यम से विकसित होता रहता है। यदि आप इसे जारी रखना चाहते हैं, तो GitHub Sponsors के माध्यम से आवर्ती दान पर विचार करें।</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Podržavatelj od %s</string>
<string name="upgrade_foss_supporter_thanks">Hvala vam što podržavate razvoj CAPoda!</string>
<string name="upgrade_foss_sponsor_again_action">Otvori stranicu za sponzoriranje</string>
<string name="upgrade_foss_sponsor_label">Sponzorirajte CAPod</string>
<string name="settings_upgrade_status_description">Vaš status sponzora</string>
<string name="upgrade_screen_why_title">Prednosti nadogradnje</string>
<string name="upgrade_screen_how_title">Kako pomoći</string>
<string name="upgrade_screen_how_body">Postanite pokrovitelj i sponzorirajte razvoj! Dodirnite gumb ispod da biste aktivirali sve dodatne značajke i otvorili moj GitHub sponzorski profil.</string>
<string name="upgrade_screen_status_free_title">Besplatna verzija</string>
<string name="upgrade_screen_status_free_body">Koristite besplatnu verziju aplikacije CAPod. Dodatne mogućnosti mogu se otključati podržavanjem razvoja.</string>
<string name="upgrade_screen_status_free_action">Pogledajte mogućnosti nadogradnje</string>
<string name="upgrade_screen_status_upgraded_title">Nadogradnja je aktivna</string>
<string name="upgrade_screen_recurring_title">Nastavi dalje</string>
<string name="upgrade_screen_recurring_body">CAPod se nastavlja razvijati kroz ažuriranja i ispravke. Ako želite održati to, razmislite o ponavljajućoj donaciji putem GitHub Sponsors.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Támogató %s óta</string>
<string name="upgrade_foss_supporter_thanks">Köszönjük, hogy támogatod a CAPod fejlesztését!</string>
<string name="upgrade_foss_sponsor_again_action">Támogatói oldal megnyitása</string>
<string name="upgrade_foss_sponsor_label">Támogasson CAPod-ot</string>
<string name="settings_upgrade_status_description">Az Ön támogató státusza.</string>
<string name="upgrade_screen_why_title">Frissítési előnyök</string>
<string name="upgrade_screen_how_title">Hogyan lehet segíteni</string>
<string name="upgrade_screen_how_body">Legyen patron támogató és szponzorálja a fejlesztést! Kattintson az alábbi gombra az összes extra funkció aktiválásához és a GitHub Sponsors profilom megnyitásához.</string>
<string name="upgrade_screen_status_free_title">Ingyenes verzió</string>
<string name="upgrade_screen_status_free_body">A CAPod ingyenes verzióját használod. A további funkciókat a fejlesztés támogatásával feloldhatod.</string>
<string name="upgrade_screen_status_free_action">Frissítési lehetőségek</string>
<string name="upgrade_screen_status_upgraded_title">Frissítés aktív</string>
<string name="upgrade_screen_recurring_title">Folytasd a támogatást</string>
<string name="upgrade_screen_recurring_body">A CAPod folyamatosan fejlődik frissítéseken és hibajavításokon keresztül. Ha szeretnéd ezt fenntartani, fontold meg az ismétlődő adományt a GitHub Sponsors-on.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Հովանավոր է %s-ից</string>
<string name="upgrade_foss_supporter_thanks">Շնորհակալություն CAPod-ի զարգացումն աջակցելու համար!</string>
<string name="upgrade_foss_sponsor_again_action">Բացել հովանավորության էջը</string>
<string name="upgrade_foss_sponsor_label">Աջակեք CAPod-ին</string>
<string name="settings_upgrade_status_description">Ձեր աջակցողի վիճակը:</string>
<string name="upgrade_screen_why_title">Թարմացման առավելությունները</string>
<string name="upgrade_screen_how_title">Ինչպես օգնել</string>
<string name="upgrade_screen_how_body">Դարձեք հովանավոր և հովանավորեք զարգացումը: Սեղմեք ստորև գտնվող կոճակը՝ բոլոր լրացուցիչ գործառույթները ակտիվացնելու և իմ GitHub Sponsors պրոֆիլը բացելու համար:</string>
<string name="upgrade_screen_status_free_title">Անվճար տարբերակ</string>
<string name="upgrade_screen_status_free_body">Դուք օգտագործում եք CAPod-ի անվճար տարբերակը: Լրացուցիչ գործառույթները կարող են բացվել, աջակցելով զարգացմանը:</string>
<string name="upgrade_screen_status_free_action">Տեսնել թարմացման տարբերակները</string>
<string name="upgrade_screen_status_upgraded_title">Թարմացումն ակտիվ է</string>
<string name="upgrade_screen_recurring_title">Շարունակեք</string>
<string name="upgrade_screen_recurring_body">CAPod-ը շարունակում է զարգանալ թարմացումների և ուղղումների միջոցով: Եթե ցանկանում եք աջակցել դրան, դիտարկեք պարբերական նվիրատվություն GitHub Sponsors-ի միջոցով:</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Pendukung sejak %s</string>
<string name="upgrade_foss_supporter_thanks">Terima kasih karena telah mendukung pengembangan CAPod!</string>
<string name="upgrade_foss_sponsor_again_action">Buka halaman sponsor</string>
<string name="upgrade_foss_sponsor_label">Sponsori CAPod</string>
<string name="settings_upgrade_status_description">Status pendukung Anda.</string>
<string name="upgrade_screen_why_title">Manfaat upgrade</string>
<string name="upgrade_screen_how_title">Cara membantu</string>
<string name="upgrade_screen_how_body">Jadilah pelindung dan sponsor pengembangan! Ketuk tombol di bawah untuk mengaktifkan semua fitur tambahan dan membuka profil GitHub Sponsors saya.</string>
<string name="upgrade_screen_status_free_title">Versi gratis</string>
<string name="upgrade_screen_status_free_body">Anda menggunakan versi gratis CAPod. Fitur tambahan dapat dibuka dengan mendukung pengembangan.</string>
<string name="upgrade_screen_status_free_action">Lihat opsi upgrade</string>
<string name="upgrade_screen_status_upgraded_title">Upgrade aktif</string>
<string name="upgrade_screen_recurring_title">Terus berkembang</string>
<string name="upgrade_screen_recurring_body">CAPod terus berkembang melalui pembaruan dan perbaikan. Jika Anda ingin mendukung itu, pertimbangkan donasi berulang melalui GitHub Sponsors.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Styrktaraðili síðan %s</string>
<string name="upgrade_foss_supporter_thanks">Takk fyrir að styðja þróun CAPod!</string>
<string name="upgrade_foss_sponsor_again_action">Opna styrktarsíðu</string>
<string name="upgrade_foss_sponsor_label">Styrktu CAPod</string>
<string name="settings_upgrade_status_description">Staða þín sem stuðningsmaður</string>
<string name="upgrade_screen_why_title">Uppfærslukostir</string>
<string name="upgrade_screen_how_title">Hvernig á að hjálpa</string>
<string name="upgrade_screen_how_body">Vertu styrktaraðili og styrktu þróun! Ýttu á hnappinn hér að neðan til að virkja alla viðbótareiginleika og opna GitHub Sponsors prófílinn minn.</string>
<string name="upgrade_screen_status_free_title">Ókeypis útgáfa</string>
<string name="upgrade_screen_status_free_body">Þú ert að nota ókeypis útgáfu CAPod. Hægt er að aflása aukaeiginleikum með því að styðja þróun.</string>
<string name="upgrade_screen_status_free_action">Skoðaðu uppfærsluvalkosti</string>
<string name="upgrade_screen_status_upgraded_title">Uppfærsla virk</string>
<string name="upgrade_screen_recurring_title">Haltu því gangandi</string>
<string name="upgrade_screen_recurring_body">CAPod heldur áfram að þróast með uppfærslum og lagfæringum. Ef þú vilt standa við það, skaltu íhuga endurtekna framlög í gegnum GitHub Sponsors.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Sostenitore dal %s</string>
<string name="upgrade_foss_supporter_thanks">Grazie per supportare lo sviluppo di CAPod!</string>
<string name="upgrade_foss_sponsor_again_action">Apri la pagina sponsor</string>
<string name="upgrade_foss_sponsor_label">Sponsorizza CAPod</string>
<string name="settings_upgrade_status_description">Il tuo status di sostenitore.</string>
<string name="upgrade_screen_why_title">Vantaggi dell\'aggiornamento</string>
<string name="upgrade_screen_how_title">Come aiutare</string>
<string name="upgrade_screen_how_body">Diventa un sostenitore e sponsorizza lo sviluppo! Clicca il pulsante qui sotto per attivare tutte le funzioni extra e apri il mio profilo Sponsor Github.</string>
<string name="upgrade_screen_status_free_title">Versione gratuita</string>
<string name="upgrade_screen_status_free_body">Stai usando la versione gratuita di CAPod. Funzionalità aggiuntive possono essere sbloccate supportando lo sviluppo.</string>
<string name="upgrade_screen_status_free_action">Visualizza opzioni di aggiornamento</string>
<string name="upgrade_screen_status_upgraded_title">Aggiornamento attivo</string>
<string name="upgrade_screen_recurring_title">Continua così</string>
<string name="upgrade_screen_recurring_body">CAPod continua a evolversi con aggiornamenti e correzioni. Se desideri sostenerlo, considera una donazione ricorrente via GitHub Sponsors.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">תומך מאז %s</string>
<string name="upgrade_foss_supporter_thanks">תודה שאתה תומך בפיתוח של CAPod!</string>
<string name="upgrade_foss_sponsor_again_action">פתח את דף התמיכה</string>
<string name="upgrade_foss_sponsor_label">תמוך ב-CAPod</string>
<string name="settings_upgrade_status_description">סטטוס התומך שלך.</string>
<string name="upgrade_screen_why_title">יתרונות השדרוג</string>
<string name="upgrade_screen_how_title">איך לעזור</string>
<string name="upgrade_screen_how_body">הפוך לפטרון ונותן חסות לפיתוח! הקש על הכפתור למטה כדי להפעיל את כל התכונות הנוספות ולפתוח את פרופיל הספונסרים בגיטהאב שלי.</string>
<string name="upgrade_screen_status_free_title">גרסה חינם</string>
<string name="upgrade_screen_status_free_body">אתה משתמש בגרסה החינם של CAPod. ניתן לפתוח תכונות נוספות על ידי תמיכה בפיתוח.</string>
<string name="upgrade_screen_status_free_action">צפה באפשרויות השדרוג</string>
<string name="upgrade_screen_status_upgraded_title">השדרוג פעיל</string>
<string name="upgrade_screen_recurring_title">תמשך בכך</string>
<string name="upgrade_screen_recurring_body">CAPod ממשיך להתפתח דרך עדכונים ותיקונים. אם אתה רוצה לתמוך בכך, שקול תרומה חוזרת דרך GitHub Sponsors.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">%sからのサポーター</string>
<string name="upgrade_foss_supporter_thanks">CAPodの開発を支援していただきありがとうございます!</string>
<string name="upgrade_foss_sponsor_again_action">スポンサーページを開く</string>
<string name="upgrade_foss_sponsor_label">CAPod をスポンサーする</string>
<string name="settings_upgrade_status_description">サポーターステータス。</string>
<string name="upgrade_screen_why_title">アップグレードの特典</string>
<string name="upgrade_screen_how_title">協力するには</string>
<string name="upgrade_screen_how_body">パトロンとスポンサーの開発になりましょう!下のボタンをタップすると、すべての追加機能が有効になり、GitHub Sponsors プロファイルを開きます。</string>
<string name="upgrade_screen_status_free_title">無料版</string>
<string name="upgrade_screen_status_free_body">CAPod の無料版を使用しています。開発を支援することで、追加機能をアンロックできます。</string>
<string name="upgrade_screen_status_free_action">アップグレード オプションを確認</string>
<string name="upgrade_screen_status_upgraded_title">アップグレード有効</string>
<string name="upgrade_screen_recurring_title">サポート継続</string>
<string name="upgrade_screen_recurring_body">CAPod は更新と修正を通じて進化し続けています。サポートを続けたい場合は、GitHub Sponsors を通じて定期的な寄付を検討してください。</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">მხარდამჭერი %s-დან</string>
<string name="upgrade_foss_supporter_thanks">მადლობა CAPod-ის განვითარების მხარდაჭერისთვის!</string>
<string name="upgrade_foss_sponsor_again_action">სპონსორის გვერდის გახსნა</string>
<string name="upgrade_foss_sponsor_label">მხარი დაუჭიროთ CAPod-ს</string>
<string name="settings_upgrade_status_description">თქვენი მხარდამჭერის მდგომარეობა.</string>
<string name="upgrade_screen_why_title">განახლების უპირატესობები</string>
<string name="upgrade_screen_how_title">როგორ დაგეხმაროთ</string>
<string name="upgrade_screen_how_body">გახდით მფარველი და სპონსორობა გაუწიეთ განვითარებას! შეეხეთ ქვემოთ მოცემულ ღილაკს, რათა ააქტიუროთ ყველა დამატებითი ფუნქცია და გახსნათ ჩემი GitHub Sponsors პროფილი.</string>
<string name="upgrade_screen_status_free_title">უფასო ვერსია</string>
<string name="upgrade_screen_status_free_body">თქვენ იყენებთ CAPod-ის უფასო ვერსიას. დამატებითი ფუნქციები შეიძლება აშვებული იყოს განვითარების მხარდასაჭერით.</string>
<string name="upgrade_screen_status_free_action">განახლების ვარიანტების ნახვა</string>
<string name="upgrade_screen_status_upgraded_title">განახლება აქტიური</string>
<string name="upgrade_screen_recurring_title">ამის გაგრძელება</string>
<string name="upgrade_screen_recurring_body">CAPod განაგრძობს განვითარებას განახლებებისა და შეასწორებების მეშვეობით. თუ გსურთ ამის შენარჩუნება, განიხილეთ რეგულარული დონაცია GitHub Sponsors-ის მეშვეობით.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">អ្នកគាំទ្រតាំងពី %s</string>
<string name="upgrade_foss_supporter_thanks">សូមអរគុណសម្រាប់ការគាំទ្រការអភិវឌ្ឍន៍ CAPod!</string>
<string name="upgrade_foss_sponsor_again_action">បើកទំព័រឧបត្ថម្ភ</string>
<string name="upgrade_foss_sponsor_label">ឧបត្ថម្ភ CAPod</string>
<string name="settings_upgrade_status_description">ស្ថានភាពអ្នកគាំទ្ររបស់អ្នក។</string>
<string name="upgrade_screen_why_title">អត្ថប្រយោជន៍លើកកម្ពស់</string>
<string name="upgrade_screen_how_title">របៀបជួយ</string>
<string name="upgrade_screen_how_body">ក្លាយជាអ្នកឧបត្ថម្ភ និងឧបត្ថម្ភការអភិវឌ្ឍន៍! ចុចប៊ូតុងខាងក្រោមដើម្បីធ្វើឱ្យមុខងារបន្ថែមទាំងអស់សកម្ម និងបើកប្រវត្តិរូប GitHub Sponsors របស់ខ្ញុំ។</string>
<string name="upgrade_screen_status_free_title">កំណែឥតគិតថ្លៃ</string>
<string name="upgrade_screen_status_free_body">អ្នកកំពុងប្រើប្រាស់កំណែឥតគិតថ្លៃរបស់ CAPod។ មុខងារលម្អិតបន្ថែមលេចឡើងដោយការគាំទ័របង្កើនលេខ។</string>
<string name="upgrade_screen_status_free_action">មើលជម្រើសលើកកម្ពស់</string>
<string name="upgrade_screen_status_upgraded_title">លើកកម្ពស់សកម្ម</string>
<string name="upgrade_screen_recurring_title">បន្តដង្ហើម</string>
<string name="upgrade_screen_recurring_body">CAPod បន្តលូតលាស់តាមរយៈការធ្វើឱ្យថ្មីនិងការកែប្រែ។ ប្រសិនបើអ្នកចង់រក្សាវាឱ្យរស់រាន, ពិចារណាការរួមចំណែកម្តងម្កាលរបស់អ្នកតាមរយៈ GitHub Sponsors។</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Piştgir ji %s ve</string>
<string name="upgrade_foss_supporter_thanks">Spas ji bo piştgiriya te ya pêşxistina CAPod!</string>
<string name="upgrade_foss_sponsor_again_action">Rûpela piştgiriyê veke</string>
<string name="upgrade_foss_sponsor_label">CAPod-ê piştgirî bike</string>
<string name="settings_upgrade_status_description">Rewşa piştgiriya te.</string>
<string name="upgrade_screen_why_title">Feydeyên Bardiyê</string>
<string name="upgrade_screen_how_title">Çawa Alîkarî Bike</string>
<string name="upgrade_screen_how_body">Bibin patronê û pîşesaziyê xwehûnand bikin! Bişka jêrîn dakirin da ku hemî taybetiyên zêde çalak bikin û profîla GitHub Sponsors-a min vekirin.</string>
<string name="upgrade_screen_status_free_title">Guhertoya Belaş</string>
<string name="upgrade_screen_status_free_body">Hûn guhertoya belaş ya CAPod bikar tîne. Taybetiyên zêde dikarin bê pîşesaziyê xwehûnandê tê de vebirîn.</string>
<string name="upgrade_screen_status_free_action">Vebijarkên Bardiya Bibînin</string>
<string name="upgrade_screen_status_upgraded_title">Bardiya Çalak e</string>
<string name="upgrade_screen_recurring_title">Berdewam Bike</string>
<string name="upgrade_screen_recurring_body">CAPod di navberê nûkariyan û raskariyan de cardin pêşve diçe. Ger hûn dixwazin vê berdewam bikin, hemî kirînên danûstî di riya GitHub Sponsors-a de bifikirin.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">%s ರಿಂದ ಬೆಂಬಲಿಗ</string>
<string name="upgrade_foss_supporter_thanks">CAPod ಅಭಿವೃದ್ಧಿಗೆ ಬೆಂಬಲ ನೀಡಿದ್ದಕ್ಕಾಗಿ ಧನ್ಯವಾದಗಳು!</string>
<string name="upgrade_foss_sponsor_again_action">ಪ್ರಾಯೋಜಕ ಪುಟ ತೆರೆಯಿರಿ</string>
<string name="upgrade_foss_sponsor_label">CAPod ಬೆಂಬಲಿಸಿ</string>
<string name="settings_upgrade_status_description">ನಿಮ್ಮ ಬೆಂಬಲಕಾರ ಸ್ಥಿತಿ.</string>
<string name="upgrade_screen_why_title">ನವೀಕರಣ ಪ್ರಯೋಜನಗಳು</string>
<string name="upgrade_screen_how_title">ಸಹಾಯ ಮಾಡುವುದು ಹೇಗೆ</string>
<string name="upgrade_screen_how_body">ಆಶ್ರಯದಾತ ಮತ್ತು ಅಭಿವೃದ್ಧಿ ಸ್ಪನ್ಸರ್ ಆಗಿ ಮಾರ್ಪಡಿ! ಎಲ್ಲಾ ಹೆಚ್ಚುವರಿ ವೈಶಿಷ್ಟ್ಯಗಳನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಲು ಮತ್ತು ನನ್ನ ಗಿಟ್‌ಹಬ್ ಸ್ಪನ್ಸರ್‌ಗಳ ಪ್ರೊಫೈಲ್ ತೆರೆಯಲು ಕೆಳಗಿನ ಬಟನ್ ಟ್ಯಾಪ್ ಮಾಡಿ.</string>
<string name="upgrade_screen_status_free_title">ಉಚಿತ ಆವೃತ್ತಿ</string>
<string name="upgrade_screen_status_free_body">ನೀವು ಸಿಎಪಿಪಾಡ್‌ನ ಉಚಿತ ಆವೃತ್ತಿಯನ್ನು ಬಳಸುತ್ತಿದ್ದೀರಿ. ಅಭಿವೃದ್ಧಿಯನ್ನು ಬೆಂಬಲಿಸುವ ಮೂಲಕ ಹೆಚ್ಚುವರಿ ವೈಶಿಷ್ಟ್ಯಗಳನ್ನು ಅನ್‌ಲಾಕ್ ಮಾಡಬಹುದು.</string>
<string name="upgrade_screen_status_free_action">ನವೀಕರಣ ಆಯ್ಕೆಗಳನ್ನು ನೋಡಿ</string>
<string name="upgrade_screen_status_upgraded_title">ನವೀಕರಣ ಕ್ರಿಯಾಶೀಲ</string>
<string name="upgrade_screen_recurring_title">ಅದನ್ನು ಮುಂದುವರಿಸಿ</string>
<string name="upgrade_screen_recurring_body">CAPod ನವೀಕರಣ ಮತ್ತು ಸುಧಾರೆಗಳ ಮೂಲಕ ನಿರಂತರವಾಗಿ ಅಭಿವೃದ್ಧಿ ಆಗುತ್ತಿದೆ. ನೀವು ಇದನ್ನು ಮುಂದುವರಿಸಲು ಬಯಸಿದರೆ, GitHub Sponsors ಮೂಲಕ ನಿಯಮಿತ ದಾನವನ್ನು ಪರಿಗಣಿಸಿ.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">%s부터 후원자</string>
<string name="upgrade_foss_supporter_thanks">CAPod 개발을 후원해 주셔서 감사합니다!</string>
<string name="upgrade_foss_sponsor_again_action">후원 페이지 열기</string>
<string name="upgrade_foss_sponsor_label">CAPod 후원하기</string>
<string name="settings_upgrade_status_description">후원자 상태</string>
<string name="upgrade_screen_why_title">업그레이드 혜택</string>
<string name="upgrade_screen_how_title">도움 주기</string>
<string name="upgrade_screen_how_body">후원자가 되어 개발을 지원하세요! 아래 버튼을 눌러 Github Sponsors 프로필을 열고 추가 기능을 잠금해제하세요.</string>
<string name="upgrade_screen_status_free_title">무료 버전</string>
<string name="upgrade_screen_status_free_body">CAPod의 무료 버전을 사용 중입니다. 개발을 지원하여 추가 기능을 잠금 해제할 수 있습니다.</string>
<string name="upgrade_screen_status_free_action">업그레이드 옵션 보기</string>
<string name="upgrade_screen_status_upgraded_title">업그레이드 활성화됨</string>
<string name="upgrade_screen_recurring_title">계속 지원하기</string>
<string name="upgrade_screen_recurring_body">CAPod는 업데이트와 수정을 통해 계속 발전합니다. 이를 지속하고 싶다면 GitHub Sponsors를 통한 정기 기부를 고려해 주세요.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">%s\'дан бери колдоочу</string>
<string name="upgrade_foss_supporter_thanks">CAPod\'дун өнүгүшүн колдогонуңуз үчүн рахмат!</string>
<string name="upgrade_foss_sponsor_again_action">Демөөрчүлүк баракчасын ачуу</string>
<string name="upgrade_foss_sponsor_label">CAPod-ды спонсорлоо</string>
<string name="settings_upgrade_status_description">Колдоочу абалы</string>
<string name="upgrade_screen_why_title">Жогорулоонун пайдалары</string>
<string name="upgrade_screen_how_title">Жардам бергүүнүн жолу</string>
<string name="upgrade_screen_how_body">Демеки болуп, иштөөсүнө спонсорлик кылыңыз! Бардык кошумча өзгөчөлүктөрдүн ишке кирүүсүнө жана GitHub Sponsors профилимди ачуу үчүн төмөндөгү баскычын басыңыз.</string>
<string name="upgrade_screen_status_free_title">Акысыз версия</string>
<string name="upgrade_screen_status_free_body">CAPod программасынын акысыз версиясын колдонуп жатасыз. Иштөөнүн өнүгүүсүнө спонсорлик кылсаңыз кошумча өзгөчөлүктөрдүн ишке кирүүсүн ачуп ала аласыз.</string>
<string name="upgrade_screen_status_free_action">Жогорулоо вариантларын көрүңүз</string>
<string name="upgrade_screen_status_upgraded_title">Жогорулоо активтүү</string>
<string name="upgrade_screen_recurring_title">Улантуу</string>
<string name="upgrade_screen_recurring_body">CAPod жаңыланууларды жана оңдуулар аркылуу өнүгүп турат. Эгерде сиз муну сактоону кааласаңыз, GitHub Sponsors аркылуу спонсорлук туурасында ойлонуңүз.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">ຜູ້ສະໜັບສະໜູນຕັ້ງແຕ່ %s</string>
<string name="upgrade_foss_supporter_thanks">ຂອບຊູ່ສຳລັບການສະໜັບສະໜູນການພັດທະນາຂອງ CAPod!</string>
<string name="upgrade_foss_sponsor_again_action">ເປິດໝ້າສະໜັບສະໜູນ</string>
<string name="upgrade_foss_sponsor_label">ສະໜັບສະໜູນ CAPod</string>
<string name="settings_upgrade_status_description">ສະຖານະຜູ້ສະໜັບສະໜູນຂອງທ່ານ</string>
<string name="upgrade_screen_why_title">ປະໂຫຍດຂອງການອັບເກຣດ</string>
<string name="upgrade_screen_how_title">ວິທີການຊ່ວຍເຫຼືອ</string>
<string name="upgrade_screen_how_body">ກາຍເປັນຜູ້ອຸປະຖໍາ ແລະ ໃຫ້ການສະໜັບສະໜູນການພັດທະນາ! ກົດປຸ່ມຂ້າງລຸ່ມເພື່ອເປີດໃຊ້ງານຄຸນສົມບັດເພີ່ມເຕີມທັງໝົດ ແລະ ເປີດໂປຣໄຟລ໌ GitHub Sponsors ຂອງຂ້ອຍ.</string>
<string name="upgrade_screen_status_free_title">ເວີຊັນຟຣີ</string>
<string name="upgrade_screen_status_free_body">ທ່ານກຳລັງໃຊ້ເວີຊັນຟຣີຂອງ CAPod. ຄຸນສົມບັດພິເສດສາມາດປົດລັອກໄດ້ໂດຍການສະຫນັບສະຫນູນການພັດທະນາ.</string>
<string name="upgrade_screen_status_free_action">ເບິ່ງທາງເລືອກການອັບເກຣດ</string>
<string name="upgrade_screen_status_upgraded_title">ອັບເກຣດເປີດໃຊ້ງານ</string>
<string name="upgrade_screen_recurring_title">ສືບຕໍ່ໃຫ້ໄປ</string>
<string name="upgrade_screen_recurring_body">CAPod ສືບຕໍ່ວິວັດທະນາການຜ່ານການອັບເດດ ແລະການແກ້ໄຂ. ຖ້າທ່ານຕ້ອງການຮັກສາສະຖານະນັ້ນ, ກະລຸນາພິຈາລະນາການບໍລິຈາກປະຈຳຜ່ານ GitHub Sponsors.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Rėmėjas nuo %s</string>
<string name="upgrade_foss_supporter_thanks">Ačiū, kad remiate CAPod kūrimą!</string>
<string name="upgrade_foss_sponsor_again_action">Atidaryti rėmėjo puslapį</string>
<string name="upgrade_foss_sponsor_label">Remti CAPod</string>
<string name="settings_upgrade_status_description">Jūsų rėmėjo statusas.</string>
<string name="upgrade_screen_why_title">Atnaujinimo privalumai</string>
<string name="upgrade_screen_how_title">Kaip padėti</string>
<string name="upgrade_screen_how_body">Tapkite globėju ir rėmėju plėtros! Palieskite žemiau esantį mygtuką, kad suaktyvintumėte visas papildomas funkcijas ir atidarytumėte mano GitHub Sponsors profilį.</string>
<string name="upgrade_screen_status_free_title">Nemokama versija</string>
<string name="upgrade_screen_status_free_body">Jūs naudojate nemokamą CAPod versiją. Papildomos funkcijos gali būti atblokuotos palaikant kūrimą.</string>
<string name="upgrade_screen_status_free_action">Žiūrėti atnaujinimo galimybes</string>
<string name="upgrade_screen_status_upgraded_title">Atnaujinimas aktyvus</string>
<string name="upgrade_screen_recurring_title">Tęskime toliau</string>
<string name="upgrade_screen_recurring_body">CAPod nuolat tobulėja per atnaujinimus ir pataisas. Jei norite tai palaikyti, apsvarstyti pasikartojančią donaciją per GitHub Sponsors.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<string name="upgrade_foss_sponsor_action">Sponsorēt attīstību</string>
<string name="upgrade_foss_sponsor_subtitle">Bez reklamām. Bez izsekošanas. Bez Google Play saistībām.</string>
<string name="upgrade_foss_sponsor_returned_early">Jau atpakaļ? Tavs atbalsts uztur CAPod esamību.</string>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Atbalstītājs kopš %s</string>
<string name="upgrade_foss_supporter_thanks">Paldies, ka atbalsti CAPod izstrādi!</string>
<string name="upgrade_foss_sponsor_again_action">Atvērt sponsorēšanas lapu</string>
<string name="upgrade_foss_sponsor_label">Atbalstīt CAPod</string>
<string name="settings_upgrade_status_description">Jūsu atbalstītāja statuss.</string>
<string name="upgrade_screen_why_title">Jaunināšanas priekšrocības</string>
<string name="upgrade_screen_how_title">Kā palīdzēt</string>
<string name="upgrade_screen_how_body">Kļūstiet par aizbildni un sponsorējiet attīstību! Pieskarieties pogas zemāk, lai aktivizētu visas papildu funkcijas un atvērtu manu GitHub Sponsors profilu.</string>
<string name="upgrade_screen_status_free_title">Bezmaksas versija</string>
<string name="upgrade_screen_status_free_body">Jūs izmantojat CAPod bezmaksas versiju. Papildu funkcijas var atbloķēt, atbalstot izstrādi.</string>
<string name="upgrade_screen_status_free_action">Skatīt jaunināšanas opcijas</string>
<string name="upgrade_screen_status_upgraded_title">Jaunināšana aktīva</string>
<string name="upgrade_screen_recurring_title">Turpiniet atbalstu</string>
<string name="upgrade_screen_recurring_body">CAPod nepārtraukti attīstās caur atjauninājumiem un labojumiem. Ja vēlaties to atbalstīt, izsveriet regulāru ziedojumu caur GitHub Sponsors.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Поддржувач од %s</string>
<string name="upgrade_foss_supporter_thanks">Ти благодариме што го поддржуваш развојот на CAPod!</string>
<string name="upgrade_foss_sponsor_again_action">Отвори ја страницата за спонзорство</string>
<string name="upgrade_foss_sponsor_label">Поддржи CAPod</string>
<string name="settings_upgrade_status_description">Твојот статус на поддржувач.</string>
<string name="upgrade_screen_why_title">Предности на надградување</string>
<string name="upgrade_screen_how_title">Како да помогнете</string>
<string name="upgrade_screen_how_body">Станете покровител и спонзорирајте го развојот! Допрете го копчето подолу за да ги активирате сите дополнителни функции и да го отворите мојот GitHub Sponsors профил.</string>
<string name="upgrade_screen_status_free_title">Бесплатна верзија</string>
<string name="upgrade_screen_status_free_body">Користете ја бесплатната верзија на CAPod. Дополнителни функции може да ги отклучите со поддршка на развојот.</string>
<string name="upgrade_screen_status_free_action">Видете опции за надградување</string>
<string name="upgrade_screen_status_upgraded_title">Активна надградување</string>
<string name="upgrade_screen_recurring_title">Подржи развој</string>
<string name="upgrade_screen_recurring_body">CAPod постојано се развива преку ажурирања и поправки. Ако сакате да го поддржите тоа, размислувајте за повторена донација преку GitHub Sponsors.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">%s മുതൽ പിന്തുണക്കാൻ</string>
<string name="upgrade_foss_supporter_thanks">CAPod-ന്റെ വികസനത്തെ പിന്തുണച്ചതിന് നന്ദി!</string>
<string name="upgrade_foss_sponsor_again_action">സ്പോൺസർ പേജ് തുറക്കുക</string>
<string name="upgrade_foss_sponsor_label">CAPod നെ സ്പോൺസർ ചെയ്യുക</string>
<string name="settings_upgrade_status_description">നിങ്ങളുടെ പിന്തുണയ സ്ഥിതി.</string>
<string name="upgrade_screen_why_title">അപ്‌ഗ്രേഡ് നേട്ടങ്ങൾ</string>
<string name="upgrade_screen_how_title">എങ്ങനെ സഹായിക്കാം</string>
<string name="upgrade_screen_how_body">പാട്രോൺ ആകുകയും വികസനം സ്പോൺസർ ചെയ്യുകയും ചെയ്യുക! എല്ലാ അധിക സവിശേഷതകളും സജീവമാക്കാനും എന്റെ GitHub Sponsors പ്രൊഫൈൽ തുറക്കാനും താഴെയുള്ള ബട്ടൺ ടാപ്പ് ചെയ്യുക.</string>
<string name="upgrade_screen_status_free_title">സൗജന്യ പതിപ്പ്</string>
<string name="upgrade_screen_status_free_body">നിങ്ങൾ CAPod-ന്റെ സൗജന്യ പതിപ്പ് ഉപയോഗിക്കുന്നു. വികസനത്തെ പിന്തുണയ്ക്കുന്നതിലൂടെ അതിരിക്ത ഫീച്ചറുകൾ അൻലോക്ക് ചെയ്യാൻ കഴിയും.</string>
<string name="upgrade_screen_status_free_action">അപ്‌ഗ്രേഡ് ഓപ്ഷനുകൾ നോക്കുക</string>
<string name="upgrade_screen_status_upgraded_title">അപ്‌ഗ്രേഡ് സജീവമാണ്</string>
<string name="upgrade_screen_recurring_title">ഇത് തുടരുക</string>
<string name="upgrade_screen_recurring_body">CAPod അപ്‌ഡേറ്റുകളിലൂടെയും പരിഷ്കാരങ്ങളിലൂടെയും വികസിച്ചുകൊണ്ടിരിക്കുന്നു. നിങ്ങൾ അത് നിലനിർത്താൻ ആഗ്രഹിക്കുന്നുണ്ടെങ്കിൽ, GitHub സ്പോണ്സറുകൾ മുഖേന ആവർത്തിച്ച് ദാനം നൽകാൻ പരിഗണിക്കുക.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">%s-с хойш дэмжигч</string>
<string name="upgrade_foss_supporter_thanks">CAPod-ын хөгжлийг дэмжсэнд баярлалаа!</string>
<string name="upgrade_foss_sponsor_again_action">Дэмжигчийн хуудсыг нээх</string>
<string name="upgrade_foss_sponsor_label">CAPod-ыг спонсорлох</string>
<string name="settings_upgrade_status_description">Таны дэмжигч статус.</string>
<string name="upgrade_screen_why_title">Давуу талууд</string>
<string name="upgrade_screen_how_title">Хэрхэн тусламж үзүүлэх</string>
<string name="upgrade_screen_how_body">Ивээн тэтгэгч болж хөгжлийг дэмжээрэй! Бүх нэмэлт боломжуудыг идэвхжүүлж, миний GitHub Sponsors профайлыг нээхийн тулд доорх товчийг дарна уу.</string>
<string name="upgrade_screen_status_free_title">Үнэгүй хувилбар</string>
<string name="upgrade_screen_status_free_body">Та CAPod-ын үнэгүй хувилбарыг ашиглаж байна. Хөгжлийг дэмжих замаар нэмэлт функцийг нээх боломжтой.</string>
<string name="upgrade_screen_status_free_action">Сонголтыг харах</string>
<string name="upgrade_screen_status_upgraded_title">Идэвхтэй</string>
<string name="upgrade_screen_recurring_title">Үргэлжүүлэх</string>
<string name="upgrade_screen_recurring_body">CAPod шинэчлэлт, засварын тусламжтайгаар үргэлжлэн хөгжиж байна. Үүнийг дэмжихийг хүссэн бол GitHub Sponsors-ээр давтан төлбөрийн хандив хийхийг авч үзэхэй.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">%s पासून समर्थक</string>
<string name="upgrade_foss_supporter_thanks">CAPod च्या विकासाला पाठिंबा दिल्याबद्दल धन्यवाद!</string>
<string name="upgrade_foss_sponsor_again_action">प्रायोजक पृष्ठ उघडा</string>
<string name="upgrade_foss_sponsor_label">CAPod ला प्रायोजित करा</string>
<string name="settings_upgrade_status_description">आपल्या समर्थक स्थिती.</string>
<string name="upgrade_screen_why_title">अपग्रेड लाभ</string>
<string name="upgrade_screen_how_title">मदत कसे करायची</string>
<string name="upgrade_screen_how_body">संरक्षक बनून विकास प्रायोजित करा! सर्व अतिरिक्त वैशिष्ट्ये सक्रिय करण्यासाठी आणि माझे GitHub Sponsors प्रोफाइल उघडण्यासाठी खाली दिलेल्या बटणावर टॅप करा.</string>
<string name="upgrade_screen_status_free_title">विनामूल्य आवृत्ती</string>
<string name="upgrade_screen_status_free_body">आप CAPod चे विनामूल्य आवृत्ती वापरत आहात. विकासाला समर्थन देऊन अतिरिक्त वैशिष्ट्ये अनलॉक केली जाऊ शकतात.</string>
<string name="upgrade_screen_status_free_action">अपग्रेड पर्याय पहा</string>
<string name="upgrade_screen_status_upgraded_title">अपग्रेड सक्रिय</string>
<string name="upgrade_screen_recurring_title">चालू ठेवा</string>
<string name="upgrade_screen_recurring_body">CAPod अद्यतने आणि दुरुस्त्यांद्वारे विकसित होत राहते. जर आपल्याला तेच टिकवून ठेवायचे असेल तर GitHub Sponsors द्वारे आवर्ती दान विचारात घ्या.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Penyokong sejak %s</string>
<string name="upgrade_foss_supporter_thanks">Terima kasih kerana menyokong pembangunan CAPod!</string>
<string name="upgrade_foss_sponsor_again_action">Buka halaman penaja</string>
<string name="upgrade_foss_sponsor_label">Menajakan CAPod</string>
<string name="settings_upgrade_status_description">Status penyokong anda.</string>
<string name="upgrade_screen_why_title">Manfaat peningkatan</string>
<string name="upgrade_screen_how_title">Bagaimana untuk membantu</string>
<string name="upgrade_screen_how_body">Menjadi penaung dan penaja pembangunan! Ketik butang di bawah untuk mengaktifkan semua ciri tambahan dan buka profil Penaja GitHub saya.</string>
<string name="upgrade_screen_status_free_title">Versi percuma</string>
<string name="upgrade_screen_status_free_body">Anda menggunakan versi percuma CAPod. Ciri-ciri tambahan boleh dibuka kunci dengan menyokong pembangunan.</string>
<string name="upgrade_screen_status_free_action">Lihat pilihan peningkatan</string>
<string name="upgrade_screen_status_upgraded_title">Peningkatan aktif</string>
<string name="upgrade_screen_recurring_title">Teruskan</string>
<string name="upgrade_screen_recurring_body">CAPod terus berkembang melalui kemas kini dan pembetulan. Jika anda ingin menopang itu, pertimbangkan sumbangan berulang melalui GitHub Sponsors.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">%s မှစ၍ ပံ့ပိုးသူ</string>
<string name="upgrade_foss_supporter_thanks">CAPod ၏ ဖွံ့ဖြိုးတိုးတက်မှုကို ပံ့ပိုးပေးသည့်အတွက် ကျေးဇူးတင်ပါသည်!</string>
<string name="upgrade_foss_sponsor_again_action">ပံ့ပိုးသူစာမျက်နှာကို ဖွင့်ပါ</string>
<string name="upgrade_foss_sponsor_label">CAPod ကို ပံ့ပိုးပါ</string>
<string name="settings_upgrade_status_description">သင်၏ ပံ့ပိုးသူ အခြေအနေ။</string>
<string name="upgrade_screen_why_title">အဆင့်မြှင့်တင်မှု အကျိုးအကျေးမများ</string>
<string name="upgrade_screen_how_title">ကူညီနည်းလမ်း</string>
<string name="upgrade_screen_how_body">ကူညီသူဖြစ်ပြီး ဖွံ့ဖြိုးတိုးတက်မှုကို ကမကထပံ့ပိုးပါ! အပိုဝန်ဆောင်မှုများအားလုံးကို ဖွင့်ပြီး ကျွန်ုပ်၏ GitHub Sponsors ပရိုဖိုင်းကို ဖွင့်ရန် အောက်ပါခလုတ်ကို နှိပ်ပါ။</string>
<string name="upgrade_screen_status_free_title">အခမဲ့ ဗားရှင်း</string>
<string name="upgrade_screen_status_free_body">သင် CAPod ၏ အခမဲ့ ဗားရှင်း ကို အသုံးပြုနေသည်။ ဖွံ့ဖြိုးတိုးတက်မှုကို ပံ့ပိုးခြင်းအားဖြင့် ထပ်ဆောင်း လုပ်ဆောင်ချက်များ အသုံးပြုခွင့်ရှိနိုင်သည်။</string>
<string name="upgrade_screen_status_free_action">အဆင့်မြှင့်တင်မှု ရွေးချယ်စရာများ ကြည့်ပါ</string>
<string name="upgrade_screen_status_upgraded_title">အဆင့်မြှင့်တင်မှု ကျင်လည်နေသည်</string>
<string name="upgrade_screen_recurring_title">ဆက်လက်စွာ ပံ့ပိုးပါ</string>
<string name="upgrade_screen_recurring_body">CAPod သည် အဆင့်မြှင့်တင်မှု နှင့် ပြုပြင်ဖြေရှင်းမှုများ မှတစ်ဆင့် ဆက်လက်စွာ ကောင်းမွန်လာသည်။ ထိုကဲ့သို့ ကျေးဇူးတင်သည့်အတွက် GitHub Sponsors မှ ပုံမှန် လှူဒါန်းမှုကို စဉ်းစားကြည့်ပါ။</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Støttespiller siden %s</string>
<string name="upgrade_foss_supporter_thanks">Takk for at du støtter utviklingen av CAPod!</string>
<string name="upgrade_foss_sponsor_again_action">Åpne sponsorside</string>
<string name="upgrade_foss_sponsor_label">Støtt CAPod</string>
<string name="settings_upgrade_status_description">Din status som støtter.</string>
<string name="upgrade_screen_why_title">Oppgraderingsfordeler</string>
<string name="upgrade_screen_how_title">Hvordan hjelpe</string>
<string name="upgrade_screen_how_body">Bli en patron og støtt utviklingen! Trykk på knappen under for å aktivere alle ekstra funksjoner og åpne sponsorprofilen min på GitHub.</string>
<string name="upgrade_screen_status_free_title">Gratis versjon</string>
<string name="upgrade_screen_status_free_body">Du bruker den gratis versjonen av CAPod. Ekstra funksjoner kan låses opp ved å støtte utviklingen.</string>
<string name="upgrade_screen_status_free_action">Se oppgraderingsalternativene</string>
<string name="upgrade_screen_status_upgraded_title">Oppgradering aktiv</string>
<string name="upgrade_screen_recurring_title">Hold det gående</string>
<string name="upgrade_screen_recurring_body">CAPod fortsetter å utvikle seg gjennom oppdateringer og rettinger. Hvis du vil opprettholde det, kan du vurdere en gjentakende donasjon via GitHub Sponsors.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">%s देखि समर्थक</string>
<string name="upgrade_foss_supporter_thanks">CAPod को विकासलाई समर्थन गर्नुभएकोमा धन्यवाद!</string>
<string name="upgrade_foss_sponsor_again_action">प्रायोजक पृष्ठ खोल्नुहोस्</string>
<string name="upgrade_foss_sponsor_label">CAPod को प्रायोजन गर्नुहोस्</string>
<string name="settings_upgrade_status_description">आफ्नो समर्थक स्थिति।</string>
<string name="upgrade_screen_why_title">अपग्रेड लाभहरु</string>
<string name="upgrade_screen_how_title">कसरी मद्दत गर्ने</string>
<string name="upgrade_screen_how_body">संरक्षक बन्नुहोस् र विकासलाई प्रायोजन गर्नुहोस्! सबै अतिरिक्त सुविधाहरू सक्रिय पार्न र मेरो GitHub Sponsors प्रोफाइल खोल्न तलको बटन ट्याप गर्नुहोस्।</string>
<string name="upgrade_screen_status_free_title">निःशुल्क संस्करण</string>
<string name="upgrade_screen_status_free_body">तपाईं CAPod को निःशुल्क संस्करण प्रयोग गरिरहनुभएको छ। विकास समर्थन गरेर अतिरिक्त सुविधा अनलक गर्न सकिन्छ।</string>
<string name="upgrade_screen_status_free_action">अपग्रेड विकल्पहरु हेर्नुहोस्</string>
<string name="upgrade_screen_status_upgraded_title">अपग्रेड सक्रिय छ</string>
<string name="upgrade_screen_recurring_title">यो जारी राख्नुहोस्</string>
<string name="upgrade_screen_recurring_body">CAPod अद्यावधिक र सुधार मार्फत विकसित भइरहेको छ। यदि तपाई यो जारी राख्न चाहनुहुन्छ भने, GitHub Sponsors मार्फत आवर्ती दान विचार गर्नुहोस्।</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Supporter sinds %s</string>
<string name="upgrade_foss_supporter_thanks">Bedankt voor je steun aan de ontwikkeling van CAPod!</string>
<string name="upgrade_foss_sponsor_again_action">Open de sponsorpagina</string>
<string name="upgrade_foss_sponsor_label">Sponsor CAPod</string>
<string name="settings_upgrade_status_description">Je ondersteunersstatus.</string>
<string name="upgrade_screen_why_title">Voordelen van de upgrade</string>
<string name="upgrade_screen_how_title">Hoe kun je helpen</string>
<string name="upgrade_screen_how_body">Word mecenas en sponsor ontwikkelaar! Tik op de onderstaande knop om alle extra functies te activeren en mijn GitHub Sponsors-profiel te openen.</string>
<string name="upgrade_screen_status_free_title">Gratis versie</string>
<string name="upgrade_screen_status_free_body">Je gebruikt de gratis versie van CAPod. Extra functies kunnen worden ontgrendeld door de ontwikkeling te ondersteunen.</string>
<string name="upgrade_screen_status_free_action">Bekijk upgrade-opties</string>
<string name="upgrade_screen_status_upgraded_title">Upgrade actief</string>
<string name="upgrade_screen_recurring_title">Ga zo door</string>
<string name="upgrade_screen_recurring_body">CAPod blijft zich ontwikkelen door middel van updates en bugfixes. Als je dit wilt blijven ondersteunen, kun je overwegen om een terugkerende donatie te doen via GitHub Sponsors.</string>
</resources>
+15 -2
View File
@@ -3,9 +3,22 @@
<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_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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Wspierający od %s</string>
<string name="upgrade_foss_supporter_thanks">Dziękuję za wsparcie rozwoju CAPod!</string>
<string name="upgrade_foss_sponsor_again_action">Otwórz stronę wspierającego</string>
<string name="upgrade_foss_sponsor_label">Sponsoruj CAPod</string>
<string name="settings_upgrade_status_description">Status twojego wsparcia.</string>
<string name="upgrade_screen_why_title">Korzyści z uaktualnienia</string>
<string name="upgrade_screen_how_title">Jak pomóc</string>
<string name="upgrade_screen_how_body">Zostań patronem i wspieraj rozwój! Dotknij przycisku poniżej, aby aktywować wszystkie dodatkowe funkcje i otworzyć mój profil GitHub Sponsors.</string>
<string name="upgrade_screen_status_free_title">Wersja darmowa</string>
<string name="upgrade_screen_status_free_body">Korzystasz z bezpłatnej wersji CAPod. Dodatkowe funkcje można odblokować poprzez wsparcie programisty.</string>
<string name="upgrade_screen_status_free_action">Zobacz opcje aktualizacji</string>
<string name="upgrade_screen_status_upgraded_title">Aktualizacja aktywna</string>
<string name="upgrade_screen_recurring_title">Wspieraj dalej</string>
<string name="upgrade_screen_recurring_body">CAPod stale się rozwija dzięki aktualizacjom i poprawkom. Jeśli chcesz go wspierać, rozważ cykliczną darowiznę za pośrednictwem GitHub Sponsors.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Apoiador desde %s</string>
<string name="upgrade_foss_supporter_thanks">Obrigado por apoiar o desenvolvimento do CAPod!</string>
<string name="upgrade_foss_sponsor_again_action">Abrir página de patrocínio</string>
<string name="upgrade_foss_sponsor_label">Patrocine o CAPod</string>
<string name="settings_upgrade_status_description">Seu status de apoiador.</string>
<string name="upgrade_screen_why_title">Benefícios da atualização</string>
<string name="upgrade_screen_how_title">Como ajudar</string>
<string name="upgrade_screen_how_body">Torne-se um patrocinador e financie o desenvolvimento! Clique no botão abaixo para ativar todos os recursos extras e abrir meu perfil Github Sponsors.</string>
<string name="upgrade_screen_status_free_title">Versão gratuita</string>
<string name="upgrade_screen_status_free_body">Você está usando a versão gratuita do CAPod. Recursos extras podem ser desbloqueados apoiando o desenvolvimento.</string>
<string name="upgrade_screen_status_free_action">Ver opções de atualização</string>
<string name="upgrade_screen_status_upgraded_title">Atualização ativa</string>
<string name="upgrade_screen_recurring_title">Mantenha assim</string>
<string name="upgrade_screen_recurring_body">CAPod continua evoluindo com atualizações e correções. Se você quiser sustentá-lo, considere fazer uma doação recorrente via GitHub Sponsors.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Apoiante desde %s</string>
<string name="upgrade_foss_supporter_thanks">Obrigado por apoiares o desenvolvimento do CAPod!</string>
<string name="upgrade_foss_sponsor_again_action">Abrir página de patrocínio</string>
<string name="upgrade_foss_sponsor_label">Apoie o CAPod</string>
<string name="settings_upgrade_status_description">O seu estado de apoiante.</string>
<string name="upgrade_screen_why_title">Benefícios da atualização</string>
<string name="upgrade_screen_how_title">Como ajudar</string>
<string name="upgrade_screen_how_body">Torne-se patrono e apoie o desenvolvimento! Toque no botão abaixo para ativar todas as funcionalidades adicionais e abrir o meu perfil no GitHub Sponsors.</string>
<string name="upgrade_screen_status_free_title">Versão gratuita</string>
<string name="upgrade_screen_status_free_body">Você está usando a versão gratuita do CAPod. Recursos extras podem ser desbloqueados ao apoiar o desenvolvimento.</string>
<string name="upgrade_screen_status_free_action">Ver opções de atualização</string>
<string name="upgrade_screen_status_upgraded_title">Atualização ativa</string>
<string name="upgrade_screen_recurring_title">Continue a apoiar</string>
<string name="upgrade_screen_recurring_body">O CAPod continua evoluindo por meio de atualizações e correções. Se você quiser ajudar a manter isso, considere fazer uma doação recorrente pelo GitHub Sponsors.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Sustegnider dapi %s</string>
<string name="upgrade_foss_supporter_thanks">Grazia per sustegnair il svilup da CAPod!</string>
<string name="upgrade_foss_sponsor_again_action">Avrir la pagina da sponsurisaziun</string>
<string name="upgrade_foss_sponsor_label">Sponsurar CAPod</string>
<string name="settings_upgrade_status_description">Tes status da supporter.</string>
<string name="upgrade_screen_why_title">Bials da l\'upgrade</string>
<string name="upgrade_screen_how_title">Sco gidar</string>
<string name="upgrade_screen_how_body">Vign patrun e sustegna l\'svilup! Tucha il buttun sut per activar tut las funcziuns extrasas e per avrir miu profil da GitHub Sponsors.</string>
<string name="upgrade_screen_status_free_title">Versiun gratuitusa</string>
<string name="upgrade_screen_status_free_body">Tu utilizzas la versiun gratuitusa da CAPod. Funcziuns extrasas pon vegnir deblochadas cun sustegnair l\'svilup.</string>
<string name="upgrade_screen_status_free_action">Vesair las opcziuns da upgrade</string>
<string name="upgrade_screen_status_upgraded_title">Upgrade activà</string>
<string name="upgrade_screen_recurring_title">Mantegnair d\'en viva</string>
<string name="upgrade_screen_recurring_body">CAPod continua a sa sviluppar tras actualizaziuns e reparas. Sche ti vuls sustegnair quai, cunsiglia ina donazziun ricorrent via GitHub Sponsors.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Susținător din %s</string>
<string name="upgrade_foss_supporter_thanks">Mulțumim că sprijini dezvoltarea CAPod!</string>
<string name="upgrade_foss_sponsor_again_action">Deschide pagina de sponsorizare</string>
<string name="upgrade_foss_sponsor_label">Sponsorizează CAPod</string>
<string name="settings_upgrade_status_description">Statusul tău de susținător.</string>
<string name="upgrade_screen_why_title">Beneficiile upgrade-ului</string>
<string name="upgrade_screen_how_title">Cum să ajuți</string>
<string name="upgrade_screen_how_body">Devino susținător și sponsorizează dezvoltarea! Apasă butonul de mai jos pentru a activa toate funcțiile suplimentare și a deschide profilul meu GitHub Sponsors.</string>
<string name="upgrade_screen_status_free_title">Versiune gratuită</string>
<string name="upgrade_screen_status_free_body">Folosești versiunea gratuită a CAPod. Poți debloca funcții suplimentare susținând dezvoltarea.</string>
<string name="upgrade_screen_status_free_action">Vezi opțiunile de upgrade</string>
<string name="upgrade_screen_status_upgraded_title">Upgrade activ</string>
<string name="upgrade_screen_recurring_title">Ține-o în viață</string>
<string name="upgrade_screen_recurring_body">CAPod continuă să evolueze prin actualizări și corecturi. Dacă ai dori să susții asta, ia în considerare o donație recurentă prin GitHub Sponsors.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Спонсор с %s</string>
<string name="upgrade_foss_supporter_thanks">Спасибо за поддержку разработки CAPod!</string>
<string name="upgrade_foss_sponsor_again_action">Открыть страницу спонсорства</string>
<string name="upgrade_foss_sponsor_label">Поддержать CAPod</string>
<string name="settings_upgrade_status_description">Статус Вашей поддержки.</string>
<string name="upgrade_screen_why_title">Преимущества обновления</string>
<string name="upgrade_screen_how_title">Как помочь</string>
<string name="upgrade_screen_how_body">Станьте спонсором и поддержите разработку! Нажмите кнопку ниже, чтобы активировать все дополнительные функции и открыть мой профиль GitHub Sponsors.</string>
<string name="upgrade_screen_status_free_title">Бесплатная версия</string>
<string name="upgrade_screen_status_free_body">Вы используете бесплатную версию CAPod. Дополнительные функции можно разблокировать, поддерживая разработку.</string>
<string name="upgrade_screen_status_free_action">Посмотреть варианты обновления</string>
<string name="upgrade_screen_status_upgraded_title">Обновление активно</string>
<string name="upgrade_screen_recurring_title">Поддержите развитие</string>
<string name="upgrade_screen_recurring_body">CAPod продолжает развиваться через обновления и исправления. Если Вы хотите осуществить поддержку, рассмотрите возможность периодического пожертвования через спонсоров GitHub.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Sustentadore dae su %s</string>
<string name="upgrade_foss_supporter_thanks">Gràtzias pro sustènnere s\'isvilupu de CAPod!</string>
<string name="upgrade_foss_sponsor_again_action">Aberi sa pàgina de sponsorizatzione</string>
<string name="upgrade_foss_sponsor_label">Sostene CAPod</string>
<string name="settings_upgrade_status_description">S\'istadu tuo de sustentadore.</string>
<string name="upgrade_screen_why_title">Benefitzios de s\'agiornamentu</string>
<string name="upgrade_screen_how_title">Comente agiudare</string>
<string name="upgrade_screen_how_body">Diventa patronu e sponsoriza s\'isvilupu! Tocca su butone in bassu pro ativare totu sas funtzionalidades addizionales e abèrrere su profilu meu de GitHub Sponsors.</string>
<string name="upgrade_screen_status_free_title">Versione lìbera</string>
<string name="upgrade_screen_status_free_body">Tù ses usende sa versione lìbera de CAPod. Benefitzios extras podent esse desblocados si supportas su sviluppu.</string>
<string name="upgrade_screen_status_free_action">Bide sas optziones de agiornamentu</string>
<string name="upgrade_screen_status_upgraded_title">Agiornamentu attivu</string>
<string name="upgrade_screen_recurring_title">Fide an\'abantis</string>
<string name="upgrade_screen_recurring_body">CAPod cuntìnua a evolutionare cun agiornamentos e corretziones. Si tù bolis mantenere custu, pentziona a una donatzione reciclica pro via GitHub Sponsors.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">%s සිට අනුග්‍රාහකයෙකි</string>
<string name="upgrade_foss_supporter_thanks">CAPod හි සංවර්ධනයට සහාය දැක්වූ ඔබට ස්තූතියි!</string>
<string name="upgrade_foss_sponsor_again_action">අනුග්‍රාහක පිටුව විවෘත කරන්න</string>
<string name="upgrade_foss_sponsor_label">CAPod සඳහා අනුග්‍රහක්</string>
<string name="settings_upgrade_status_description">ඔබේ සහයෝගිතා තත්‍වය.</string>
<string name="upgrade_screen_why_title">උඩ්ග්‍රේඩ් ප්‍රතිලාභ</string>
<string name="upgrade_screen_how_title">උදව් කරන්නේ කෙසේද</string>
<string name="upgrade_screen_how_body">අනුග්‍රාහකයෙකු වී සංවර්ධනයට අනුග්‍රහ කරන්න! සියලු අමතර විශේෂාංග සක්‍රිය කර මගේ GitHub Sponsors පැතිකඩ විවෘත කිරීමට පහත බොත්තම ස්පර්ශ කරන්න.</string>
<string name="upgrade_screen_status_free_title">නිදහස් සංස්කරණය</string>
<string name="upgrade_screen_status_free_body">ඔබ CAPod හි නිදහස් සංස්කරණය භාවිතා කරමින් පවතී. අතිරේක විශේෂාංග සංවර්ධනයට සහයෝගය දීමෙන් අඩුවිය හැක.</string>
<string name="upgrade_screen_status_free_action">උඩ්ග්‍රේඩ් විකල්ප බලන්න</string>
<string name="upgrade_screen_status_upgraded_title">උඩ්ග්‍රේඩ් සක්‍රිය</string>
<string name="upgrade_screen_recurring_title">එයින් දිගටම</string>
<string name="upgrade_screen_recurring_body">CAPod දිගටම සංවර්ධනය වෙමින් පවතී යාවත්කාලීනයන් සහ නිවැරදි කිරීම් මගින්. ඔබ එය පවතින්නට කිරීමට අවශ්‍ය නම්, GitHub Sponsors හරහා පුනරාවර්තක පරිත්‍යාගයක් සලකා බලන්න.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Podporovateľ od %s</string>
<string name="upgrade_foss_supporter_thanks">Ďakujeme, že podporujete vývoj CAPod!</string>
<string name="upgrade_foss_sponsor_again_action">Otvoriť stránku sponzorstva</string>
<string name="upgrade_foss_sponsor_label">Podporiť CAPod</string>
<string name="settings_upgrade_status_description">Váš status podporovateľa.</string>
<string name="upgrade_screen_why_title">Výhody upgradu</string>
<string name="upgrade_screen_how_title">Ako pomôcť</string>
<string name="upgrade_screen_how_body">Staňte sa patrónom a sponzorujte rozvoj! Klepnutím na tlačidlo nižšie aktivujete všetky ďalšie funkcie a otvoríte môj profil sponzorov GitHub.</string>
<string name="upgrade_screen_status_free_title">Bezplatná verzia</string>
<string name="upgrade_screen_status_free_body">Používate bezplatnú verziu aplikácie CAPod. Dodatočné funkcie je možné odomknúť podporou vývoja.</string>
<string name="upgrade_screen_status_free_action">Zobraziť možnosti upgradu</string>
<string name="upgrade_screen_status_upgraded_title">Upgrade aktívny</string>
<string name="upgrade_screen_recurring_title">Pokračuj ďalej</string>
<string name="upgrade_screen_recurring_body">CAPod sa neustále vyvíja prostredníctvom aktualizácií a oprav. Ak chcete to podporiť, zvážte si opakujúcu sa donáciu prostredníctvom GitHub Sponsors.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Podpornik od %s</string>
<string name="upgrade_foss_supporter_thanks">Hvala, ker podpirate razvoj CAPod-a!</string>
<string name="upgrade_foss_sponsor_again_action">Odpri stran za sponzorstvo</string>
<string name="upgrade_foss_sponsor_label">Podpri CAPod</string>
<string name="settings_upgrade_status_description">Tvoj status podpornika.</string>
<string name="upgrade_screen_why_title">Prednosti nadgradnje</string>
<string name="upgrade_screen_how_title">Kako pomagati.</string>
<string name="upgrade_screen_how_body">Postanite pokrovitelj in prispevajte k razvoju! Dotaknite se spodnjega gumba, da omogočite dodatne funkcije in odprete moj pokroviteljski profil na GitHubu.</string>
<string name="upgrade_screen_status_free_title">Brezplačna različica</string>
<string name="upgrade_screen_status_free_body">Uporabljate brezplačno različico CAPoda. Dodatne funkcionalnosti se odklenejo s podporo razvoju.</string>
<string name="upgrade_screen_status_free_action">Prikaži možnosti nadgradnje</string>
<string name="upgrade_screen_status_upgraded_title">Nadgradnja je aktivna</string>
<string name="upgrade_screen_recurring_title">Nadaljuj s tem</string>
<string name="upgrade_screen_recurring_body">CAPod se stalno razvija z posodobitvami in popravki. Če želite to podpreti, razmislite o ponavljajočem se prispevku prek GitHub Sponsors.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Mbështetës që nga %s</string>
<string name="upgrade_foss_supporter_thanks">Faleminderit për mbështetjen e zhvillimit të CAPod-it!</string>
<string name="upgrade_foss_sponsor_again_action">Hap faqen e sponsorizimit</string>
<string name="upgrade_foss_sponsor_label">Mbështesni CAPod</string>
<string name="settings_upgrade_status_description">Statusi juaj si mbështetës.</string>
<string name="upgrade_screen_why_title">Përfitime të përmirësimit</string>
<string name="upgrade_screen_how_title">Si të ndihmoj</string>
<string name="upgrade_screen_how_body">Bëhuni një mbrojtës dhe sponsor i zhvillimit! Prekni butonin më poshtë për të aktivizuar të gjitha veçoritë shtesë dhe për të hapur profilin tim të sponsorëve të GitHub.</string>
<string name="upgrade_screen_status_free_title">Versioni falas</string>
<string name="upgrade_screen_status_free_body">Jeni duke përdorur versionin falas të CAPod. Veçoritë shtesë mund të aktivizohen duke mbështetur zhvillimin.</string>
<string name="upgrade_screen_status_free_action">Shikoni opsionet e përmirësimit</string>
<string name="upgrade_screen_status_upgraded_title">Përmirësimi aktiv</string>
<string name="upgrade_screen_recurring_title">Mbajeni të vazhdueshme</string>
<string name="upgrade_screen_recurring_body">CAPod vazhdon të zhvillohet përmes përditësimeve dhe përmirësimesh. Nëse dëshironi ta mbështetin atë, shqyrtoni një donacion të përsëritur përmes GitHub Sponsors.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Подржавалац од %s</string>
<string name="upgrade_foss_supporter_thanks">Хвала вам што подржавате развој CAPod-а!</string>
<string name="upgrade_foss_sponsor_again_action">Отворите страницу за спонзорство</string>
<string name="upgrade_foss_sponsor_label">Подржите CAPod</string>
<string name="settings_upgrade_status_description">Статус вашег подржавања.</string>
<string name="upgrade_screen_why_title">Предности надградње</string>
<string name="upgrade_screen_how_title">Како помоћи</string>
<string name="upgrade_screen_how_body">Постаните покровитељ и спонзоришите развој! Додирните дугме испод да активирате све додатне функције и отворите мој GitHub Sponsors профил.</string>
<string name="upgrade_screen_status_free_title">Бесплатна верзија</string>
<string name="upgrade_screen_status_free_body">Користите бесплатну верзију CAPod. Додатне функције можете откључати подржавањем развоја.</string>
<string name="upgrade_screen_status_free_action">Погледајте опције надградње</string>
<string name="upgrade_screen_status_upgraded_title">Надградња је активна</string>
<string name="upgrade_screen_recurring_title">Подржите развој</string>
<string name="upgrade_screen_recurring_body">CAPod се наставља развијати кроз ажурирања и исправке. Ако желите да то подржите, размотрите редовну донацију преко GitHub Sponsors.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Supporter sedan %s</string>
<string name="upgrade_foss_supporter_thanks">Tack för att du stödjer utvecklingen av CAPod!</string>
<string name="upgrade_foss_sponsor_again_action">Öppna sponsorsidan</string>
<string name="upgrade_foss_sponsor_label">Sponsra CAPod</string>
<string name="settings_upgrade_status_description">Din supporterstatus.</string>
<string name="upgrade_screen_why_title">Uppgraderingsfördelar</string>
<string name="upgrade_screen_how_title">Hur du kan hjälpa</string>
<string name="upgrade_screen_how_body">Bli en beskyddare och sponsra utvecklingen! Tryck på knappen nedan för att aktivera alla extra funktioner och öppna min GitHub Sponsors-profil.</string>
<string name="upgrade_screen_status_free_title">Gratis version</string>
<string name="upgrade_screen_status_free_body">Du använder den kostnadsfria versionen av CAPod. Extrafunktioner kan låsas upp genom att stödja utvecklingen.</string>
<string name="upgrade_screen_status_free_action">Se uppgraderingsalternativ</string>
<string name="upgrade_screen_status_upgraded_title">Uppgradering aktiv</string>
<string name="upgrade_screen_recurring_title">Håll det igång</string>
<string name="upgrade_screen_recurring_body">CAPod utvecklas ständigt genom uppdateringar och felkorrigeringar. Om du vill stödja det, överväg en återkommande donation via GitHub Sponsors.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Msaidizi tangu %s</string>
<string name="upgrade_foss_supporter_thanks">Asante kwa kuunga mkono maendeleo ya CAPod!</string>
<string name="upgrade_foss_sponsor_again_action">Fungua ukurasa wa udhamini</string>
<string name="upgrade_foss_sponsor_label">Kuunga mkono CAPod</string>
<string name="settings_upgrade_status_description">Hali yako ya kuunga mkono.</string>
<string name="upgrade_screen_why_title">Faida za kuboreshwa</string>
<string name="upgrade_screen_how_title">Jinsi ya kusaidia</string>
<string name="upgrade_screen_how_body">Kuwa mfumo na mfadhili wa maendeleo! Gusa kitufe kilicho hapa chini ili kuwezesha vipengele vyote vya ziada na kufungua wasifu wangu wa GitHub Sponsors.</string>
<string name="upgrade_screen_status_free_title">Toleo la bure</string>
<string name="upgrade_screen_status_free_body">Unatumia toleo la bure la CAPod. Huduma za ziada zinaweza kufunguliwa kwa kusaidia maendeleo.</string>
<string name="upgrade_screen_status_free_action">Tazama chaguo za kuboreshwa</string>
<string name="upgrade_screen_status_upgraded_title">Kuboreshwa kimefanya kazi</string>
<string name="upgrade_screen_recurring_title">Endelea na hilo</string>
<string name="upgrade_screen_recurring_body">CAPod inaendelea kupitia sasisho na marekebisho. Ikiwa ungependa kusaidia, fikiria kumfanya mchango wa mara kwa mara kupitia GitHub Sponsors.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">%s முதல் ஆதரவாளர்</string>
<string name="upgrade_foss_supporter_thanks">CAPod-இன் மேம்பாட்டை ஆதரித்ததற்கு நன்றி!</string>
<string name="upgrade_foss_sponsor_again_action">ஸ்பான்சர் பக்கத்தைத் திற</string>
<string name="upgrade_foss_sponsor_label">CAPod ஐ ஆதரிக்கவும்</string>
<string name="settings_upgrade_status_description">உங்கள் ஆதரவாளர் நிலை.</string>
<string name="upgrade_screen_why_title">மேம்படுத்தல் நன்மைகள்</string>
<string name="upgrade_screen_how_title">எப்படி உதவுவது</string>
<string name="upgrade_screen_how_body">புரவலராக மாறி மேம்பாட்டை ஸ்பான்சர் செய்யுங்கள்! அனைத்து கூடுதல் அம்சங்களையும் செயல்படுத்த மற்றும் எனது GitHub Sponsors சுயவிவரத்தைத் திறக்க கீழே உள்ள பொத்தானைத் தட்டவும்.</string>
<string name="upgrade_screen_status_free_title">இலவச பதிப்பு</string>
<string name="upgrade_screen_status_free_body">நீங்கள் CAPod இன் இலவச பதிப்பைப் பயன்படுத்துகிறீர்கள். வளர்ச்சியை ஆதரிப்பதன் மூலம் கூடுதல் அம்சங்களைத் திறக்க முடியும்.</string>
<string name="upgrade_screen_status_free_action">மேம்படுத்தல் விருப்பங்களைப் பார்க்கவும்</string>
<string name="upgrade_screen_status_upgraded_title">மேம்படுத்தல் செயல்பட்டுள்ளது</string>
<string name="upgrade_screen_recurring_title">இதைத் தொடரவும்</string>
<string name="upgrade_screen_recurring_body">CAPod புதுப்பிப்புகள் மற்றும் திருத்தங்களின் மூலம் தொடர்ந்து உருவாகிறது. நீங்கள் அதை நிலைத்து வைக்க விரும்பினால், GitHub Sponsors மூலம் தொடர்ச்சியான தான வழங்கலை பரிசீலியுங்கள்.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">%s నుండి సపోర్టర్</string>
<string name="upgrade_foss_supporter_thanks">CAPod అభివృద్ధికి మద్దతు ఇచ్చినందుకు ధన్యవాదాలు!</string>
<string name="upgrade_foss_sponsor_again_action">స్పాన్సర్ పేజీని తెరవండి</string>
<string name="upgrade_foss_sponsor_label">CAPodకు స్పాన్సర్ చేయండి</string>
<string name="settings_upgrade_status_description">మీ సపోర్టర్ స్థితి.</string>
<string name="upgrade_screen_why_title">అప్‌గ్రేడ్ ప్రయోజనాలు</string>
<string name="upgrade_screen_how_title">ఎలా సహాయం చేయాలి</string>
<string name="upgrade_screen_how_body">పేట్రన్ అవ్వండి మరియు అభివృద్ధిని స్పాన్సర్ చేయండి! అన్ని అదనపు ఫీచర్లను యాక్టివేట్ చేయడానికి మరియు నా GitHub Sponsors ప్రొఫైల్‌ను తెరవడానికి దిగువ బటన్‌ను నొక్కండి.</string>
<string name="upgrade_screen_status_free_title">ఉచిత సంస్కరణ</string>
<string name="upgrade_screen_status_free_body">మీరు CAPod యొక్క ఉచిత సంస్కరణని ఉపయోగిస్తున్నారు. అభివృద్ధిని సపోర్ట్ చేయడం ద్వారా అతిరిక్త ఫీచర్‌లను అన్‌లాక్ చేయవచ్చు.</string>
<string name="upgrade_screen_status_free_action">అప్‌గ్రేడ్ ఎంపికలను చూడండి</string>
<string name="upgrade_screen_status_upgraded_title">అప్‌గ్రేడ్ క్రియాశీలమైనది</string>
<string name="upgrade_screen_recurring_title">దీన్నిని కొనసాగించండి</string>
<string name="upgrade_screen_recurring_body">CAPod నవీకరణలు మరియు సరిదిద్దుకుల ద్వారా వికసిస్తూ ఉంది. మీరు దీన్నిని కొనసాగించాలనుకుంటే, GitHub Sponsors ద్వారా పునరావృత దానం గురించి ఆలోచించండి.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">บังคับ</string>
<string name="upgrade_foss_supporter_since">ผู้สนับสนุนตั้งแต่ %s</string>
<string name="upgrade_foss_supporter_thanks">ขอบคุณที่สนับสนุนการพัฒนา CAPod!</string>
<string name="upgrade_foss_sponsor_again_action">เปิดหน้าสนับสนุน</string>
<string name="upgrade_foss_sponsor_label">สนับสนุน CAPod</string>
<string name="settings_upgrade_status_description">สถานะผู้สนับสนุนของคุณ</string>
<string name="upgrade_screen_why_title">ประโยชน์ของการอัปเกรด</string>
<string name="upgrade_screen_how_title">วิธีการช่วยเหลือ</string>
<string name="upgrade_screen_how_body">เป็นผู้อุปถัมภ์และสปอนเซอร์การพัฒนา! แตะปุ่มด้านล่างเพื่อเปิดใช้ฟีเจอร์เพิ่มเติมทั้งหมดและเปิดโปรไฟล์ GitHub Sponsors ของฉัน</string>
<string name="upgrade_screen_status_free_title">เวอร์ชันฟรี</string>
<string name="upgrade_screen_status_free_body">คุณกำลังใช้เวอร์ชันฟรีของ CAPod คุณลักษณะเพิ่มเติมสามารถปลดล็อกได้โดยการสนับสนุนการพัฒนา</string>
<string name="upgrade_screen_status_free_action">ดูตัวเลือกการอัปเกรด</string>
<string name="upgrade_screen_status_upgraded_title">เปิดใช้งานการอัปเกรด</string>
<string name="upgrade_screen_recurring_title">ทำให้มันดำเนินต่อไป</string>
<string name="upgrade_screen_recurring_body">CAPod ยังคงพัฒนาต่อไปผ่านการอัปเดตและการแก้ไข หากคุณต้องการสนับสนุนสิ่งนั้นต่อไป โปรดพิจารณาการบริจาคที่เกิดขึ้นซ้ำผ่าน GitHub Sponsors</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">%s tarihinden beri destekçi</string>
<string name="upgrade_foss_supporter_thanks">CAPod\'un geliştirilmesini desteklediğin için teşekkürler!</string>
<string name="upgrade_foss_sponsor_again_action">Sponsor sayfasını</string>
<string name="upgrade_foss_sponsor_label">CAPod\'u destekle</string>
<string name="settings_upgrade_status_description">Destekçi durumun.</string>
<string name="upgrade_screen_why_title">Yükseltme Avantajları</string>
<string name="upgrade_screen_how_title">Nasıl yardım edilir</string>
<string name="upgrade_screen_how_body">Haminiz olun ve gelişime sponsor olun! Tüm ekstra özellikleri etkinleştirmek ve GitHub Sponsorları profilimi açmak için aşağıdaki düğmeye dokunun.</string>
<string name="upgrade_screen_status_free_title">Ücretsiz Sürüm</string>
<string name="upgrade_screen_status_free_body">CAPod\'un ücretsiz sürümünü kullanıyorsunuz. Ek özellikler, geliştirmeyi destekleyerek kilidini açabilir.</string>
<string name="upgrade_screen_status_free_action">Yükseltme Seçeneklerini Gör</string>
<string name="upgrade_screen_status_upgraded_title">Yükseltme Etkin</string>
<string name="upgrade_screen_recurring_title">Devam Ettirin</string>
<string name="upgrade_screen_recurring_body">CAPod güncellemeler ve düzeltmeler aracılığıyla gelişmeye devam ediyor. Bunu sürdürmek istiyorsanız, GitHub Sponsors aracılığıyla yinelenen bir bağış yapmayı düşünün.</string>
</resources>
+14 -1
View File
@@ -7,5 +7,18 @@
<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>
<string name="upgrade_badge_label">FOSS</string>
<string name="upgrade_foss_supporter_since">Підтримує з %s</string>
<string name="upgrade_foss_supporter_thanks">Дякуємо за підтримку розробки CAPod!</string>
<string name="upgrade_foss_sponsor_again_action">Відкрити сторінку спонсорства</string>
<string name="upgrade_foss_sponsor_label">Спонсорувати CAPod</string>
<string name="settings_upgrade_status_description">Ваш статус прихильника.</string>
<string name="upgrade_screen_why_title">Переваги оновлення</string>
<string name="upgrade_screen_how_title">Як допомогти</string>
<string name="upgrade_screen_how_body">Стань меценатом і спонсором розробки! Торкніться кнопки нижче, щоб активувати всі додаткові функції та відкрити мій профіль спонсорів GitHub.</string>
<string name="upgrade_screen_status_free_title">Безплатна версія</string>
<string name="upgrade_screen_status_free_body">Ви використовуєте безплатну версію CAPod. Додаткові функції можна розблокувати, підтримавши розробку.</string>
<string name="upgrade_screen_status_free_action">Переглянути варіанти оновлення</string>
<string name="upgrade_screen_status_upgraded_title">Оновлення активне</string>
<string name="upgrade_screen_recurring_title">Підтримайте розвиток</string>
<string name="upgrade_screen_recurring_body">CAPod постійно розвивається завдяки оновленням та виправленням. Якщо ви хочете підтримати це, розгляньте можливість регулярного пожертвування через GitHub Sponsors.</string>
</resources>

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