mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
Compare commits
332
Commits
v5.1.0-rc0
...
8a57257f03
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a57257f03 | ||
|
|
fc4d2674e9 | ||
|
|
1a176e3df8 | ||
|
|
1fff40b9d4 | ||
|
|
9a9d4452d1 | ||
|
|
43740d8b2e | ||
|
|
83849efe80 | ||
|
|
ed66731207 | ||
|
|
a41bef21b5 | ||
|
|
91b49536b7 | ||
|
|
8690072ee2 | ||
|
|
92c829c7e6 | ||
|
|
1c67a78130 | ||
|
|
7a64a23d1e | ||
|
|
e2fcbbfdfd | ||
|
|
29639cdcd1 | ||
|
|
d8a6ef2286 | ||
|
|
e68927595c | ||
|
|
aa928de265 | ||
|
|
3662904d75 | ||
|
|
cec36268e0 | ||
|
|
5895ccbc27 | ||
|
|
798ac32ec9 | ||
|
|
6ed0bf8257 | ||
|
|
39a6c3c89f | ||
|
|
2ba90e1d18 | ||
|
|
92dac661c4 | ||
|
|
7712b18388 | ||
|
|
d7988189a0 | ||
|
|
ff5d0cedfa | ||
|
|
f82adeba42 | ||
|
|
cc99ad59ab | ||
|
|
b66c756741 | ||
|
|
fbbdb4e131 | ||
|
|
0bf6bf7b5f | ||
|
|
865a609203 | ||
|
|
9f213cf499 | ||
|
|
1330c77ed8 | ||
|
|
2356726417 | ||
|
|
03c09917da | ||
|
|
70830cf2ca | ||
|
|
43140005f2 | ||
|
|
59e2a16a39 | ||
|
|
11d6ddf3fa | ||
|
|
b10b42eede | ||
|
|
5801b8ea56 | ||
|
|
b9461d3259 | ||
|
|
7960144be6 | ||
|
|
0f95538f39 | ||
|
|
fec512175b | ||
|
|
a427fcbf4e | ||
|
|
c49cd62b2a | ||
|
|
abf28f335c | ||
|
|
540e6d1dbc | ||
|
|
76e1128e93 | ||
|
|
cbf681ccfc | ||
|
|
30f17b4791 | ||
|
|
cd6aaaf165 | ||
|
|
289dcd660e | ||
|
|
b07428f894 | ||
|
|
dbd1d422c6 | ||
|
|
0b83e86c5e | ||
|
|
b396ce457d | ||
|
|
734e15c94d | ||
|
|
edf84248ab | ||
|
|
83dfdb9219 | ||
|
|
70b9c2981e | ||
|
|
971fcbd34c | ||
|
|
cebed0a60d | ||
|
|
8817da309f | ||
|
|
27cc0e8d00 | ||
|
|
cecd3088be | ||
|
|
412ea369f1 | ||
|
|
22ca3a2a46 | ||
|
|
92feea8a10 | ||
|
|
3af1af872e | ||
|
|
1f00925cd8 | ||
|
|
50079e2b7a | ||
|
|
02ec76980c | ||
|
|
52ad90f77e | ||
|
|
563c6e71f7 | ||
|
|
a357d77b00 | ||
|
|
abc82b0f09 | ||
|
|
3e6541deec | ||
|
|
33a972001e | ||
|
|
fe67ce0365 | ||
|
|
511944c21c | ||
|
|
007282d3f5 | ||
|
|
5ffdf66b9d | ||
|
|
7e90d115e0 | ||
|
|
ffc3ffb718 | ||
|
|
6b358cb101 | ||
|
|
7cbcde50d3 | ||
|
|
5365a5da29 | ||
|
|
908acc4ea0 | ||
|
|
5a635b424b | ||
|
|
f1e4ffb099 | ||
|
|
d012799d49 | ||
|
|
06fd256889 | ||
|
|
2f715ccac9 | ||
|
|
4dba593c13 | ||
|
|
ed5ec8152b | ||
|
|
11232e9169 | ||
|
|
19976c89f6 | ||
|
|
323b10224c | ||
|
|
39b8fcb8f8 | ||
|
|
5ef3965a0d | ||
|
|
1e1f0b13c6 | ||
|
|
7716221053 | ||
|
|
26707dc0e5 | ||
|
|
e8b7f73c6b | ||
|
|
442f6bcc88 | ||
|
|
e7cb53f0b8 | ||
|
|
ed940b3b3d | ||
|
|
c696e523a9 | ||
|
|
f9327930b5 | ||
|
|
e7ee126f7e | ||
|
|
ed5e5ba7e1 | ||
|
|
2dbe86ad5e | ||
|
|
6fcc2c4348 | ||
|
|
375af943ae | ||
|
|
65f7cbe8b7 | ||
|
|
2833639892 | ||
|
|
495f58c23b | ||
|
|
9b60ed945f | ||
|
|
b77f4a9581 | ||
|
|
91dafe8a04 | ||
|
|
1dfed1c21c | ||
|
|
26633b940d | ||
|
|
d98c118f37 | ||
|
|
8f0ec5a374 | ||
|
|
53b4150ff6 | ||
|
|
6b2536c74f | ||
|
|
72fb1d3b17 | ||
|
|
31c3b47bcd | ||
|
|
7cb31ce783 | ||
|
|
2f69090bc2 | ||
|
|
0c39ca89c8 | ||
|
|
10317b373f | ||
|
|
7fb1f7aabd | ||
|
|
54d3c9d824 | ||
|
|
b616696a41 | ||
|
|
e364a5b02c | ||
|
|
3651bb3d55 | ||
|
|
0192ae7081 | ||
|
|
8c1b57a47c | ||
|
|
fb48ff9d43 | ||
|
|
2d604c1e6a | ||
|
|
5e28eeed38 | ||
|
|
edd3cf0c2b | ||
|
|
9f322f0a3f | ||
|
|
de287fcb66 | ||
|
|
28ad09e96d | ||
|
|
8f71af8c5d | ||
|
|
305a6d6c2a | ||
|
|
26b04854c4 | ||
|
|
7fc13ac2c5 | ||
|
|
7c75ec02bd | ||
|
|
f39ea20690 | ||
|
|
b573a79582 | ||
|
|
82cc9764e5 | ||
|
|
c9a5db1ac8 | ||
|
|
a95377cf7b | ||
|
|
80ea6fbd68 | ||
|
|
c08c6f1129 | ||
|
|
73dc10aee9 | ||
|
|
37278af34e | ||
|
|
c822569a63 | ||
|
|
eb41d84a0f | ||
|
|
c072b90876 | ||
|
|
6175bd3bb1 | ||
|
|
36c54d5d19 | ||
|
|
f629f05f23 | ||
|
|
2e969d3439 | ||
|
|
809305c4bd | ||
|
|
3398da68ba | ||
|
|
061aa21c85 | ||
|
|
28b5b85be2 | ||
|
|
825892df74 | ||
|
|
bfc0e1cfaa | ||
|
|
f4facd3e86 | ||
|
|
c8179c5c19 | ||
|
|
bd36c5e5d6 | ||
|
|
778d45170f | ||
|
|
b52c2cbe6f | ||
|
|
69febaac28 | ||
|
|
2f1cbe34e5 | ||
|
|
8dcc4f8489 | ||
|
|
3ddd5f3cfd | ||
|
|
31f4d4aafa | ||
|
|
e06dc933b8 | ||
|
|
1418be999d | ||
|
|
df06a49949 | ||
|
|
b6dcc74383 | ||
|
|
fead8381bc | ||
|
|
2ab9f8ace6 | ||
|
|
81effcb349 | ||
|
|
3dd1f8151a | ||
|
|
272a7121a9 | ||
|
|
52a7850c1d | ||
|
|
b297ad8564 | ||
|
|
a3810353f7 | ||
|
|
d4dddb1f38 | ||
|
|
21560a1f3a | ||
|
|
bdfb6bb073 | ||
|
|
aac0590017 | ||
|
|
afba33dd83 | ||
|
|
5de5dee52f | ||
|
|
ba3e253569 | ||
|
|
4488af0ab7 | ||
|
|
aae8ad62bd | ||
|
|
9606a33f15 | ||
|
|
f626d70538 | ||
|
|
a91ef44686 | ||
|
|
e3bef4e74b | ||
|
|
358bf5e244 | ||
|
|
fba2d3a59d | ||
|
|
0abba4ece2 | ||
|
|
e9b0694b67 | ||
|
|
49bbf1bc85 | ||
|
|
f5b6a5c605 | ||
|
|
fccc6cedd5 | ||
|
|
a03766448e | ||
|
|
f0c9d318ff | ||
|
|
5c5cc6f28e | ||
|
|
246b96bfb4 | ||
|
|
352e020b54 | ||
|
|
72f5690ea6 | ||
|
|
1ed1199574 | ||
|
|
fd4a43de96 | ||
|
|
e406fd4cc1 | ||
|
|
1e555cc712 | ||
|
|
227a6b2a7b | ||
|
|
5f661d71a6 | ||
|
|
2804543b9d | ||
|
|
380397321c | ||
|
|
d9c8902d34 | ||
|
|
a1b9abd4cb | ||
|
|
5d30844fa8 | ||
|
|
af1b1b4249 | ||
|
|
2cce9ae56c | ||
|
|
4bab7c61a9 | ||
|
|
81e2bc1dba | ||
|
|
df2783d8a1 | ||
|
|
20a6953b54 | ||
|
|
fdd4f69247 | ||
|
|
5ee777b196 | ||
|
|
dd827af3cd | ||
|
|
948205667b | ||
|
|
dbbfa75429 | ||
|
|
78790f73df | ||
|
|
2950d8b3f9 | ||
|
|
4d3a9bc165 | ||
|
|
7d57845502 | ||
|
|
83ef111599 | ||
|
|
0e6848d886 | ||
|
|
1eeeb63a76 | ||
|
|
a4ba302223 | ||
|
|
8c9094d85a | ||
|
|
4529b107dd | ||
|
|
17663bc759 | ||
|
|
278257998b | ||
|
|
9d41979499 | ||
|
|
985b73754e | ||
|
|
e4e4190523 | ||
|
|
ebfb034fdd | ||
|
|
8a81d354e6 | ||
|
|
ac0c4693dc | ||
|
|
4fd7a41c53 | ||
|
|
cc1caf47f0 | ||
|
|
8b9f953818 | ||
|
|
d541de8c38 | ||
|
|
2821da434d | ||
|
|
07c72dc86c | ||
|
|
7d93e772b4 | ||
|
|
82dc88faac | ||
|
|
06b41ec8eb | ||
|
|
acc6e92b2d | ||
|
|
39922df792 | ||
|
|
5a8c4b94ef | ||
|
|
0ffa87deea | ||
|
|
fc98db09a2 | ||
|
|
e1c0a702dd | ||
|
|
3f310016b1 | ||
|
|
d399a6d908 | ||
|
|
fb104cbe61 | ||
|
|
97cbca7a6e | ||
|
|
02278dab17 | ||
|
|
2240bd7ae5 | ||
|
|
bc4096cb57 | ||
|
|
5a0ba1b0bd | ||
|
|
d04abdf80c | ||
|
|
0609352cc9 | ||
|
|
abde0b7d6a | ||
|
|
b952aff38d | ||
|
|
c8f5ceda34 | ||
|
|
e6dbd2d660 | ||
|
|
228d7d1f0c | ||
|
|
67fead3589 | ||
|
|
d0aab583d2 | ||
|
|
367c8d28ff | ||
|
|
b04e1fe628 | ||
|
|
ea3ef1ff3b | ||
|
|
ff8b9cf6ed | ||
|
|
96bc3f5df7 | ||
|
|
1891a6d73a | ||
|
|
698415237d | ||
|
|
7ef672a11f | ||
|
|
f67e8aa7d1 | ||
|
|
bcb4bdff08 | ||
|
|
70d9432b2b | ||
|
|
9d06839a1c | ||
|
|
97f294e5f7 | ||
|
|
a40599c1dc | ||
|
|
175aef9e2f | ||
|
|
36ae35918a | ||
|
|
60ad1943b6 | ||
|
|
5e727e907d | ||
|
|
5360f54ea8 | ||
|
|
486efe6e65 | ||
|
|
c2ff1f111e | ||
|
|
26b72726a7 | ||
|
|
f35fe968c6 | ||
|
|
498f040a57 | ||
|
|
59b3106c3b | ||
|
|
89e8b83be4 | ||
|
|
f0531796dd | ||
|
|
a701fdfba7 | ||
|
|
118eaa8d08 | ||
|
|
016914ec05 | ||
|
|
0aadec0b75 | ||
|
|
e6cf507937 |
Binary file not shown.
|
Before Width: | Height: | Size: 332 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 760 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 761 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 605 KiB |
@@ -1,2 +1,3 @@
|
||||
worktrees/
|
||||
tmp/
|
||||
scheduled_tasks.lock
|
||||
+40
-17
@@ -4,10 +4,7 @@ Android app that detects and monitors AirPods via Bluetooth LE. Displays battery
|
||||
|
||||
## Project Structure
|
||||
|
||||
| Module | Description |
|
||||
|--------|-------------|
|
||||
| `app/` | Main Android app (FOSS and Google Play flavors) |
|
||||
| `app-common/` | Shared code between phone and Wear OS apps |
|
||||
Single Gradle module `app/` with multiple source sets (`main`, `foss`, `gplay`, `debug`, `test`, `testFoss`, `testGplay`, `screenshotTest`). A previous `app-common/` module was merged into `app/`.
|
||||
|
||||
## Build Flavors
|
||||
|
||||
@@ -20,10 +17,10 @@ Quick build check: `./gradlew assembleFossDebug`
|
||||
|
||||
| Path | Contains |
|
||||
|------|----------|
|
||||
| `app/src/main/java/` | Main app source (activities, fragments, services) |
|
||||
| `app-common/src/main/java/` | Shared logic (monitor, bluetooth, models) |
|
||||
| `app/src/main/java/` | Main app source (Compose screens, services, receivers, monitor, bluetooth, models) |
|
||||
| `app/src/foss/java/`, `app/src/gplay/java/` | Flavor-specific code (e.g. upgrade/billing) |
|
||||
| `app/src/main/res/` | Layouts, drawables, strings |
|
||||
| `app-common/src/test/` | Unit tests |
|
||||
| `app/src/test/`, `app/src/testFoss/`, `app/src/testGplay/` | Unit tests (shared + flavor-specific) |
|
||||
| `app/build.gradle.kts` | App build config, dependencies, flavors |
|
||||
| `app/src/debug/java/.../screenshots/` | Play Store screenshot content composables |
|
||||
| `fastlane/` | Screenshot generation scripts, Play Store metadata |
|
||||
@@ -31,19 +28,45 @@ Quick build check: `./gradlew assembleFossDebug`
|
||||
## Development Tips
|
||||
|
||||
- Use `assembleFossDebug` as the fastest build variant for iteration
|
||||
- Shared code goes in `app-common/`, app-specific code in `app/`
|
||||
- Follow existing patterns — the codebase uses MVVM + Hilt + Coroutines
|
||||
- Always use string resources for user-facing text (see localization rules)
|
||||
- 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
|
||||
| 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.
|
||||
|
||||
@@ -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.
|
||||
@@ -1,84 +1,61 @@
|
||||
---
|
||||
description: Architecture overview, module structure, key components, data flow, and dependencies
|
||||
globs:
|
||||
- "app/**/*.kt"
|
||||
- "app-common/**/*.kt"
|
||||
- "**/*.gradle.kts"
|
||||
description: Load-bearing architectural invariants that are not obvious from reading the code
|
||||
---
|
||||
|
||||
# Architecture
|
||||
|
||||
## Multi-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.
|
||||
|
||||
- **app/**: Main Android application with FOSS and Google Play flavors
|
||||
- **app-common/**: Shared code between main app and Wear OS app
|
||||
## BLE vs AAP
|
||||
|
||||
## Core Patterns
|
||||
Two independent data paths. Which one a feature can use decides whether it is even possible.
|
||||
|
||||
- **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
|
||||
| | BLE (advertisements) | AAP (L2CAP session) |
|
||||
|---|---|---|
|
||||
| Direction | Read-only, passive | Bidirectional commands + events |
|
||||
| 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 |
|
||||
|
||||
## Key Components
|
||||
A figure BLE never advertises cannot be obtained without a bonded AAP session, and anything
|
||||
requiring a write is AAP-only.
|
||||
|
||||
### PodMonitor System
|
||||
## `DeviceMonitor` is the state merge boundary
|
||||
|
||||
- `PodMonitor`: Core service that detects and tracks AirPods via Bluetooth LE
|
||||
- `MonitorControl`: Manages MonitorService lifecycle
|
||||
- `MonitorService`: Foreground service that continuously scans for AirPods
|
||||
- `BluetoothEventReceiver`: Handles system Bluetooth events
|
||||
`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.
|
||||
|
||||
### Reaction System
|
||||
The invariant is about **state**, not about the whole AAP layer:
|
||||
|
||||
- `ReactionSettingsFragment`: Configuration for popup notifications
|
||||
- `PopUpWindow`: Displays AirPods status when case is opened
|
||||
- `PopUpPodViewFactory`: Creates UI components for different pod models
|
||||
- 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
|
||||
|
||||
### Common Utilities
|
||||
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.
|
||||
|
||||
- `EdgeToEdgeHelper`: Handles Android edge-to-edge display insets
|
||||
## `AapConnectionManager` owns sessions
|
||||
|
||||
## Build Configuration
|
||||
It holds every open AAP session keyed by `BluetoothAddress`. Consumers call `sendCommand(...)` and
|
||||
observe `allStates`.
|
||||
|
||||
### Flavors
|
||||
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.
|
||||
|
||||
- **FOSS**: Open-source version without Google Play dependencies
|
||||
- **Google Play (gplay)**: Version with billing client for in-app purchases
|
||||
## FOSS is not "always pro"
|
||||
|
||||
### Build Types
|
||||
`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.
|
||||
|
||||
- **debug**: Unobfuscated, full logging, no minification
|
||||
- **beta**: Obfuscated, production-ready with strict lint checks
|
||||
- **release**: Fully optimized for production distribution
|
||||
## Navigation is mid-migration
|
||||
|
||||
## Data Flow
|
||||
|
||||
The app follows a unidirectional data flow:
|
||||
|
||||
1. `BluetoothEventReceiver` detects Bluetooth events
|
||||
2. `MonitorService` scans for AirPods beacon data
|
||||
3. `PodMonitor` processes and stores device information
|
||||
4. ViewModels observe monitor data via repositories
|
||||
5. UI components react to ViewModel state changes
|
||||
6. `ReactionSystem` triggers popups and notifications
|
||||
|
||||
## Bluetooth LE Implementation
|
||||
|
||||
The app uses Android's Bluetooth LE APIs to scan for Apple device advertisements. The core scanning logic is in `MonitorService` which runs as a foreground service.
|
||||
|
||||
## Multi-Platform Considerations
|
||||
|
||||
Code shared between phone and Wear OS apps is placed in `app-common`. When modifying shared functionality, ensure compatibility across both platforms.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
- **Unit Tests**: Located in `app-common/src/test/` for shared logic
|
||||
- **Test Flavors**: Separate test configurations for FOSS and Google Play variants
|
||||
|
||||
## Key Dependencies
|
||||
|
||||
- **Hilt**: Dependency injection framework
|
||||
- **AndroidX Navigation**: Fragment navigation with SafeArgs
|
||||
- **kotlinx.serialization**: JSON serialization for configuration and caching
|
||||
- **Material Design**: UI components following Material Design guidelines
|
||||
Navigation3 (`addNavigation3()`) drives current Compose routing, but legacy `androidx.navigation`
|
||||
helpers still exist (`NavDirectionsExtensions`, `ViewModel3`). Don't assume SafeArgs is fully gone.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -1,7 +1,5 @@
|
||||
---
|
||||
description: Git commit message format and conventions
|
||||
globs:
|
||||
- "**"
|
||||
---
|
||||
|
||||
# Commit Guidelines
|
||||
@@ -21,6 +19,7 @@ Use the existing commit history as reference. Common prefixes:
|
||||
- **fix**: Bug fixes (e.g., `fix: Handle display cutouts in landscape mode`)
|
||||
- **feat**: New features
|
||||
- **refactor**: Code restructuring without behavior change
|
||||
- **ui**: Visual/layout-only tweaks that aren't a full feature or refactor (e.g., `ui(overview): ...`)
|
||||
- **chore**: Maintenance, dependency updates, build config
|
||||
- **docs**: Documentation changes
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -9,7 +9,7 @@ globs:
|
||||
When adding new user-facing strings:
|
||||
|
||||
- **Always use string resources**: Never hardcode user-facing text in layouts or code
|
||||
- **Follow naming conventions**: Use descriptive, hierarchical naming (e.g., `profiles_name_default`, `settings_bluetooth_enabled`)
|
||||
- **Follow naming conventions**: Use descriptive, hierarchical naming (e.g., `profiles_name_default`, `settings_monitor_mode_label`)
|
||||
- **Provide context**: String names should indicate usage and location
|
||||
- **Consider pluralization**: Use Android plural resources (`<plurals>`) when quantities vary
|
||||
|
||||
@@ -17,5 +17,9 @@ When adding new user-facing strings:
|
||||
|
||||
- `profiles_create_title` (screen title)
|
||||
- `profiles_name_label` (form field label)
|
||||
- `profiles_delete_confirmation` (dialog message)
|
||||
- `error_network_unavailable` (error message)
|
||||
- `profiles_name_default` (default value)
|
||||
- `troubleshooter_ble_result_failure_title` (status/error title)
|
||||
|
||||
Common prefixes currently in use: `device_`, `settings_`, `support_`, `profiles_`, `press_`, `general_`, `pods_`, `upgrade_`, `widget_`, `debug_`, `permission_`, `troubleshooter_`, `overview_`, `anc_`, `onboarding_`. There is no `error_*` prefix — error labels live under the relevant feature (e.g. `general_error_label`, `troubleshooter_*_failure_*`).
|
||||
|
||||
String context, character limits and file context are managed on Crowdin through the android-translation plugin's `crowdin-annotate` skill. XML comments in `values/strings.xml` no longer reach translators once a string's context has been written on Crowdin; change it there.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
description: Release guardrails — what never to do by hand. Full procedure is the /release skill.
|
||||
---
|
||||
|
||||
# Release
|
||||
|
||||
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.
|
||||
|
||||
- **`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.
|
||||
@@ -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,25 +30,35 @@ ScreenshotContent.kt (mock data + composables)
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `app/src/debug/java/.../screenshots/ScreenshotContent.kt` | Mock data composables for each screen (8 screens) |
|
||||
| `app/src/screenshotTest/kotlin/.../screenshots/PlayStoreScreenshots.kt` | `@PreviewTest` functions that wire content to locale annotations |
|
||||
| `app/src/screenshotTest/kotlin/.../screenshots/PlayStoreLocales.kt` | Multi-preview annotations (auto-generated by batch script) |
|
||||
| `fastlane/generate_screenshots.sh` | Batched generation across 68 locales |
|
||||
| `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. The committed content is an en-US placeholder, not meaningful data — `generate_screenshots.sh` rewrites it per batch and restores it from a `.bak` on exit. A run killed hard leaves that `.bak` behind, so the script now refuses to start until it is restored by hand |
|
||||
| `fastlane/generate_screenshots.sh` | Batched generation; locale list (`ALL_LOCALES`) and `BATCH_SIZE` are defined inside the script |
|
||||
| `fastlane/copy_screenshots.sh` | Copies rendered PNGs into fastlane structure |
|
||||
|
||||
## Commit policy
|
||||
|
||||
Only `en-US` has `phoneScreenshots/*.png` checked into the repo — 7 PNGs, ~1 MB tracked. Every other locale is excluded by `.gitignore`.
|
||||
|
||||
`--smoke` still *renders* 6 locales (en-US, de-DE, ja-JP, ar, zh-CN, pt-BR), but only en-US is committed. The other five cover LTR, RTL and CJK layout so a render that breaks on non-Latin script fails during generation, and the resulting PNGs sit in the working tree for manual inspection. Nothing compares them against a baseline, so this is render coverage plus eyeballing, not regression checking.
|
||||
|
||||
Play Store's `supply` only uploads what's present in `fastlane/metadata/android/<locale>/images/phoneScreenshots/`. For locales not in the upload, Play Store retains whatever was last pushed. So full localization on Play Store is maintained by an **occasional manual** full regen + `:screenshots_only` upload — not by every PR.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Full run — all 68 locales, ~12 batches, ~7 minutes
|
||||
./fastlane/generate_screenshots.sh
|
||||
|
||||
# Smoke test — 6 locales (en, de, ja, ar, zh-CN, pt-BR), single batch
|
||||
# Default — smoke set (6 locales × 7 screens, ~42 PNGs, single batch).
|
||||
# Use this for local iteration and PRs that touch screenshot content.
|
||||
./fastlane/generate_screenshots.sh --smoke
|
||||
|
||||
# Full run — all 68 locales. Use only when intending to upload to Play Store
|
||||
# (the non-smoke output is .gitignored and should not be committed).
|
||||
./fastlane/generate_screenshots.sh
|
||||
|
||||
# Copy into fastlane directories (run after generate)
|
||||
./fastlane/copy_screenshots.sh
|
||||
|
||||
# Clean copy (removes old screenshots first)
|
||||
# Clean copy (removes old screenshots first) — REQUIRED when screens are removed or renamed
|
||||
./fastlane/copy_screenshots.sh --clean
|
||||
```
|
||||
|
||||
@@ -47,20 +68,48 @@ ScreenshotContent.kt (mock data + composables)
|
||||
2. Add a `@PreviewTest` function in `PlayStoreScreenshots.kt` that calls it
|
||||
3. Add the function name → filename mapping in `copy_screenshots.sh` `SCREEN_MAP`
|
||||
4. Update the expected count in `generate_screenshots.sh` (composables per locale)
|
||||
5. Run the full pipeline: `generate_screenshots.sh` then `copy_screenshots.sh`
|
||||
5. Run the smoke pipeline: `generate_screenshots.sh --smoke` then `copy_screenshots.sh --clean`
|
||||
|
||||
## Removing or Renaming a Screenshot
|
||||
|
||||
1. Remove the `@PreviewTest` entry and its `SCREEN_MAP` mapping
|
||||
2. Run `generate_screenshots.sh --smoke`
|
||||
3. Run `copy_screenshots.sh --clean` — **`--clean` is required** here; without it, old files (e.g. a renamed `8_reaction_settings.png`) stay in `fastlane/metadata/android/<smoke locale>/images/phoneScreenshots/` and get uploaded to Play Store
|
||||
|
||||
## After UI Changes
|
||||
|
||||
When modifying a screen that appears in screenshots (check `ScreenshotContent.kt`), regenerate:
|
||||
When modifying a screen that appears in screenshots (check `ScreenshotContent.kt`), regenerate the smoke set:
|
||||
|
||||
```bash
|
||||
./fastlane/generate_screenshots.sh
|
||||
./fastlane/generate_screenshots.sh --smoke
|
||||
./fastlane/copy_screenshots.sh --clean
|
||||
```
|
||||
|
||||
## Refreshing all locales on Play Store
|
||||
|
||||
Periodic, manual operation — not per-PR:
|
||||
|
||||
```bash
|
||||
./fastlane/generate_screenshots.sh # full, ~30 min, 476 PNGs (68 locales x 7)
|
||||
./fastlane/copy_screenshots.sh --clean
|
||||
git add fastlane/metadata/android/en-US/images/phoneScreenshots/
|
||||
if bundle exec fastlane screenshots_only; then
|
||||
git checkout -- fastlane/metadata/android/ &&
|
||||
git commit --only -m "chore(screenshots): Refresh Play Store screenshots" -- \
|
||||
fastlane/metadata/android/en-US/images/phoneScreenshots/
|
||||
else
|
||||
git checkout -- fastlane/metadata/android/
|
||||
echo "Upload failed; the refreshed en-US screenshots remain staged for retry."
|
||||
fi
|
||||
```
|
||||
|
||||
The `git add` has to happen before the upload. The final `git checkout` restores every tracked file under that path **from the index**, so staging the refreshed English set is precisely what makes it survive the checkout — skip the `git add` and the checkout silently reverts the refresh while the store still receives the new images.
|
||||
|
||||
Restoring is the checkout's job otherwise: `screenshots_only` runs `remove_unsupported_languages.sh`, which deletes 9 tracked locale directories (es-AR, sc-IT, sq-AL, uz, kmr-TR, ur-IN, zu, si-LK, nb) from the working tree before uploading — 35 tracked files, a subset of the 309 tracked non-screenshot metadata files under that path, all of them put back by the checkout. It does **not** touch the regenerated non-English PNGs: those are untracked and ignored, so they stay on disk and never show up in `git status`. Because the checkout discards any uncommitted metadata text edits too, run this refresh only with an otherwise-clean metadata tree. The final commit is path-limited on purpose, so an unrelated staged change can't ride along, and it is gated on `screenshots_only` succeeding rather than merely sequenced after it: if the upload fails, the refreshed English files stay staged for a retry instead of being committed as though they were deployed. The deleted locale directories are restored on either path.
|
||||
|
||||
## Technical Notes
|
||||
|
||||
- Batch size defaults to 2 locales (16 renders) to avoid layoutlib memory leak (~10MB/image)
|
||||
- Batch size defaults to 2 locales; renders per batch = `BATCH_SIZE × screen count` (currently 2 × 7 = 14). Small batches avoid layoutlib memory leaks (~10MB/image)
|
||||
- Gradle daemon is stopped between batches to release memory
|
||||
- `PlayStoreLocales.kt` is temporarily rewritten per batch and restored via trap
|
||||
- Device spec: 1080x2400px @ 428 DPI (Pixel-class phone)
|
||||
|
||||
@@ -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.
|
||||
@@ -7,11 +7,12 @@
|
||||
"deny": []
|
||||
},
|
||||
"enabledPlugins": {
|
||||
"android-translation@claude-code-cafe": true,
|
||||
"debugbadger@claude-code-cafe": true,
|
||||
"jvm-tools@claude-code-cafe": true,
|
||||
"frontend-design@claude-plugins-official": true,
|
||||
"kotlin-lsp@claude-plugins-official": true,
|
||||
"support-investigator@claude-code-cafe": true
|
||||
"android-translation@clanker-cafe": true,
|
||||
"debugbadger@clanker-cafe": true,
|
||||
"jvm-tools@clanker-cafe": true,
|
||||
"support@clanker-cafe": true,
|
||||
"google-play@clanker-cafe": true,
|
||||
"devtools@clanker-cafe": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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` | 100% |
|
||||
| `-rc*` | `assembleFossRelease` | full release | `production` | **`beta`** | 100% |
|
||||
|
||||
`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 100% — 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`.
|
||||
@@ -30,13 +30,4 @@ runs:
|
||||
~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-caches-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', 'buildSrc/**/*.kt') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-caches-
|
||||
|
||||
- name: Cache Android Global Build-Cache
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 #v5.0.3
|
||||
with:
|
||||
path: |
|
||||
~/.android/build-cache
|
||||
key: ${{ runner.os }}-android-build-cache-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-android-build-cache-
|
||||
${{ runner.os }}-gradle-caches-
|
||||
@@ -3,6 +3,9 @@ name: Code tests & eval
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths-ignore:
|
||||
- VERSION
|
||||
- version.properties
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
@@ -78,4 +81,27 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Validate metadata lengths
|
||||
run: bash fastlane/check_metadata_length.sh
|
||||
run: bash fastlane/check_metadata_length.sh
|
||||
|
||||
check-release-tooling:
|
||||
name: Release tooling
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install bats and shellcheck
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y bats shellcheck
|
||||
|
||||
- name: Shellcheck bump.sh
|
||||
run: shellcheck tools/release/bump.sh
|
||||
|
||||
- name: Run bump.sh unit tests
|
||||
run: bats tools/release/bump.bats
|
||||
|
||||
- name: Verify version.properties + VERSION are consistent
|
||||
run: ./tools/release/bump.sh --mode=check
|
||||
|
||||
@@ -4,6 +4,9 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- VERSION
|
||||
- version.properties
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
name: Deploy GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- _config.yml
|
||||
- _layouts/**
|
||||
- README.md
|
||||
- CHANGELOG.md
|
||||
- PRIVACY_POLICY.md
|
||||
- CNAME
|
||||
- .github/workflows/pages.yml
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: pages
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Jekyll site
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Configure Pages
|
||||
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d #v6.0.0
|
||||
|
||||
- name: Build with Jekyll
|
||||
uses: actions/jekyll-build-pages@44a6e6beabd48582f863aeeb6cb2151cc1716697 #v1.0.13
|
||||
with:
|
||||
source: ./
|
||||
destination: ./_site
|
||||
env:
|
||||
JEKYLL_GITHUB_TOKEN: ${{ github.token }}
|
||||
|
||||
- name: Sanity-check Jekyll output
|
||||
run: test -f _site/index.html && test -f _site/CNAME
|
||||
|
||||
- name: Upload Pages artifact
|
||||
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 #v5.0.0
|
||||
with:
|
||||
path: ./_site
|
||||
|
||||
deploy:
|
||||
name: Deploy to GitHub Pages
|
||||
needs: build
|
||||
if: github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
pages: write
|
||||
id-token: write
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 #v5.0.0
|
||||
@@ -0,0 +1,235 @@
|
||||
name: Release prepare
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
bump_kind:
|
||||
description: 'How to bump the version (ignored if version_override is set)'
|
||||
type: choice
|
||||
options: [build, patch, minor, major]
|
||||
default: build
|
||||
version_type:
|
||||
description: 'Channel for the new version (keep-current preserves current type)'
|
||||
type: choice
|
||||
options: [keep-current, rc, beta]
|
||||
default: keep-current
|
||||
version_override:
|
||||
description: 'Explicit version, e.g. 5.1.2-rc0 (overrides bump_kind/version_type)'
|
||||
type: string
|
||||
default: ''
|
||||
expected_current:
|
||||
description: 'Optional safety check: fail if current version.properties does not match (e.g. 5.1.1-rc0)'
|
||||
type: string
|
||||
default: ''
|
||||
dry_run:
|
||||
description: 'When true: compute and validate only. When false: commit, tag, push, dispatch release-tag.yml.'
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: release-prepare-main
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
compute-and-validate:
|
||||
name: Compute and validate
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
new_name: ${{ steps.plan.outputs.new_name }}
|
||||
new_code: ${{ steps.plan.outputs.new_code }}
|
||||
current_name: ${{ steps.plan.outputs.current_name }}
|
||||
env:
|
||||
INPUT_BUMP_KIND: ${{ inputs.bump_kind }}
|
||||
INPUT_VERSION_TYPE: ${{ inputs.version_type }}
|
||||
INPUT_VERSION_OVERRIDE: ${{ inputs.version_override }}
|
||||
INPUT_EXPECTED_CURRENT: ${{ inputs.expected_current }}
|
||||
steps:
|
||||
- name: Guard ref must be main
|
||||
run: |
|
||||
if [[ "${GITHUB_REF}" != "refs/heads/main" ]]; then
|
||||
echo "Must dispatch from main, got ${GITHUB_REF}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Checkout main
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Compute and validate
|
||||
id: plan
|
||||
run: |
|
||||
set -euo pipefail
|
||||
args=("--mode=plan")
|
||||
if [[ -n "${INPUT_VERSION_OVERRIDE}" ]]; then
|
||||
args+=("--version-override=${INPUT_VERSION_OVERRIDE}")
|
||||
else
|
||||
args+=("--bump-kind=${INPUT_BUMP_KIND}")
|
||||
args+=("--version-type=${INPUT_VERSION_TYPE}")
|
||||
fi
|
||||
if [[ -n "${INPUT_EXPECTED_CURRENT}" ]]; then
|
||||
args+=("--expected-current=${INPUT_EXPECTED_CURRENT}")
|
||||
fi
|
||||
./tools/release/bump.sh "${args[@]}" | tee plan.txt
|
||||
{
|
||||
grep -E '^current_name=' plan.txt
|
||||
grep -E '^new_name=' plan.txt
|
||||
grep -E '^new_code=' plan.txt
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Tag collision check (local + remote)
|
||||
env:
|
||||
NEW_NAME: ${{ steps.plan.outputs.new_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if git rev-parse --verify "refs/tags/v${NEW_NAME}" >/dev/null 2>&1; then
|
||||
echo "Local tag v${NEW_NAME} already exists" >&2
|
||||
exit 1
|
||||
fi
|
||||
if git ls-remote --exit-code --tags origin "refs/tags/v${NEW_NAME}" >/dev/null; then
|
||||
echo "Remote tag v${NEW_NAME} already exists" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Write step summary
|
||||
env:
|
||||
CURRENT_NAME: ${{ steps.plan.outputs.current_name }}
|
||||
NEW_NAME: ${{ steps.plan.outputs.new_name }}
|
||||
NEW_CODE: ${{ steps.plan.outputs.new_code }}
|
||||
DRY_RUN: ${{ inputs.dry_run }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
{
|
||||
echo "## Release plan"
|
||||
echo
|
||||
echo "| | |"
|
||||
echo "|---|---|"
|
||||
echo "| Current | \`${CURRENT_NAME}\` |"
|
||||
echo "| New | \`${NEW_NAME}\` (code ${NEW_CODE}) |"
|
||||
echo "| Tag | \`v${NEW_NAME}\` |"
|
||||
echo "| Dry run | \`${DRY_RUN}\` |"
|
||||
echo
|
||||
echo "### bump.sh output"
|
||||
echo
|
||||
echo '```'
|
||||
cat plan.txt
|
||||
echo '```'
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
push-and-dispatch:
|
||||
name: Push and dispatch
|
||||
needs: compute-and-validate
|
||||
if: ${{ !inputs.dry_run }}
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
contents: read
|
||||
env:
|
||||
INPUT_BUMP_KIND: ${{ inputs.bump_kind }}
|
||||
INPUT_VERSION_TYPE: ${{ inputs.version_type }}
|
||||
INPUT_VERSION_OVERRIDE: ${{ inputs.version_override }}
|
||||
NEW_NAME: ${{ needs.compute-and-validate.outputs.new_name }}
|
||||
NEW_CODE: ${{ needs.compute-and-validate.outputs.new_code }}
|
||||
CURRENT_NAME_AT_PLAN: ${{ needs.compute-and-validate.outputs.current_name }}
|
||||
steps:
|
||||
- name: Mint App token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 #v3.1.1
|
||||
with:
|
||||
client-id: ${{ secrets.RELEASE_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Resolve bot identity
|
||||
id: bot
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
user_id=$(gh api "/users/${APP_SLUG}%5Bbot%5D" --jq .id)
|
||||
echo "user_name=${APP_SLUG}[bot]" >> "$GITHUB_OUTPUT"
|
||||
echo "user_email=${user_id}+${APP_SLUG}[bot]@users.noreply.github.com" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Checkout main with credentials
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
persist-credentials: true
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- name: Verify state still matches plan from Job 1
|
||||
run: |
|
||||
set -euo pipefail
|
||||
./tools/release/bump.sh --mode=check --expected-current="${CURRENT_NAME_AT_PLAN}"
|
||||
|
||||
- name: Re-check tag collision (state may have moved between jobs)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if git rev-parse --verify "refs/tags/v${NEW_NAME}" >/dev/null 2>&1; then
|
||||
echo "Local tag v${NEW_NAME} already exists" >&2
|
||||
exit 1
|
||||
fi
|
||||
if git ls-remote --exit-code --tags origin "refs/tags/v${NEW_NAME}" >/dev/null; then
|
||||
echo "Remote tag v${NEW_NAME} already exists" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Apply bump
|
||||
run: |
|
||||
set -euo pipefail
|
||||
args=("--mode=write" "--expected-current=${CURRENT_NAME_AT_PLAN}")
|
||||
if [[ -n "${INPUT_VERSION_OVERRIDE}" ]]; then
|
||||
args+=("--version-override=${INPUT_VERSION_OVERRIDE}")
|
||||
else
|
||||
args+=("--bump-kind=${INPUT_BUMP_KIND}")
|
||||
args+=("--version-type=${INPUT_VERSION_TYPE}")
|
||||
fi
|
||||
./tools/release/bump.sh "${args[@]}"
|
||||
|
||||
- name: Verify post-write state matches plan
|
||||
run: |
|
||||
set -euo pipefail
|
||||
./tools/release/bump.sh --mode=check --expected-current="${NEW_NAME}"
|
||||
|
||||
- name: Configure git identity
|
||||
env:
|
||||
BOT_USER_NAME: ${{ steps.bot.outputs.user_name }}
|
||||
BOT_USER_EMAIL: ${{ steps.bot.outputs.user_email }}
|
||||
run: |
|
||||
git config user.name "${BOT_USER_NAME}"
|
||||
git config user.email "${BOT_USER_EMAIL}"
|
||||
|
||||
- name: Commit and tag
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git add version.properties VERSION
|
||||
git commit -m "Release: ${NEW_NAME}"
|
||||
git tag -a "v${NEW_NAME}" -m "Release v${NEW_NAME}"
|
||||
|
||||
- name: Atomic push (commit + tag)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git push --atomic origin "HEAD:refs/heads/main" "refs/tags/v${NEW_NAME}"
|
||||
|
||||
- name: Write step summary
|
||||
run: |
|
||||
set -euo pipefail
|
||||
{
|
||||
echo "## Released"
|
||||
echo
|
||||
echo "| | |"
|
||||
echo "|---|---|"
|
||||
echo "| Tag | \`v${NEW_NAME}\` |"
|
||||
echo "| Version code | \`${NEW_CODE}\` |"
|
||||
echo "| Bump commit | on \`main\` |"
|
||||
echo "| Downstream | dispatched \`release-tag.yml\` |"
|
||||
echo
|
||||
echo "Watch the [Tagged releases](../../actions/workflows/release-tag.yml) workflow for the build + upload."
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -14,11 +14,54 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: release-${{ github.ref_name }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
validate-tag:
|
||||
name: Validate tag
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check tag-name format
|
||||
env:
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ ! "${REF_NAME}" =~ ^v[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,2}-(rc|beta)[0-9]{1,2}$ ]]; then
|
||||
echo "Tag '${REF_NAME}' does not match v<M.m.p-(rc|beta)n>" >&2
|
||||
echo "Releases must be cut via the 'Release prepare' workflow." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
|
||||
with:
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
|
||||
- name: Verify version.properties matches tag
|
||||
env:
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Strip leading 'v' to get the bare version name.
|
||||
tag_name="${REF_NAME#v}"
|
||||
# bump.sh in check mode emits current_name=...
|
||||
parsed=$(./tools/release/bump.sh --mode=check)
|
||||
current_name=$(echo "$parsed" | grep -E '^current_name=' | cut -d= -f2)
|
||||
if [[ "$current_name" != "$tag_name" ]]; then
|
||||
echo "Tag '${REF_NAME}' does not match version.properties name '$current_name'" >&2
|
||||
echo "version.properties + VERSION must equal the tag — releases must be cut via 'Release prepare'." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
release-github:
|
||||
needs: validate-tag
|
||||
name: Create GitHub release
|
||||
permissions:
|
||||
contents: write
|
||||
actions: write
|
||||
runs-on: ubuntu-22.04
|
||||
environment: foss-production
|
||||
steps:
|
||||
@@ -61,7 +104,7 @@ jobs:
|
||||
|
||||
- name: Create pre-release
|
||||
if: contains(github.ref_name, '-beta') && !(github.event_name == 'workflow_dispatch' && inputs.dry_run)
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b #v2.5.0
|
||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda #v3.0.0
|
||||
with:
|
||||
prerelease: true
|
||||
tag_name: ${{ github.ref_name }}
|
||||
@@ -73,7 +116,7 @@ jobs:
|
||||
|
||||
- name: Create release
|
||||
if: "!contains(github.ref_name, '-beta') && !(github.event_name == 'workflow_dispatch' && inputs.dry_run)"
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b #v2.5.0
|
||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda #v3.0.0
|
||||
with:
|
||||
prerelease: false
|
||||
tag_name: ${{ github.ref_name }}
|
||||
@@ -83,10 +126,19 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Trigger GitHub Pages deployment
|
||||
if: "!(github.event_name == 'workflow_dispatch' && inputs.dry_run)"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: gh workflow run pages.yml --ref main
|
||||
|
||||
release-gplay:
|
||||
needs: validate-tag
|
||||
name: Create Google Play release
|
||||
runs-on: ubuntu-22.04
|
||||
environment: gplay-production
|
||||
env:
|
||||
BUNDLE_GEMFILE: ${{ github.workspace }}/fastlane/Gemfile
|
||||
steps:
|
||||
- name: Decode Keystore
|
||||
env:
|
||||
@@ -122,6 +174,10 @@ jobs:
|
||||
with:
|
||||
ruby-version: 3.3.6
|
||||
bundler-cache: true
|
||||
working-directory: fastlane
|
||||
|
||||
- name: Verify fastlane Bundler wiring
|
||||
run: bundle exec fastlane --version
|
||||
|
||||
- name: Assemble beta and upload to Google Play
|
||||
if: contains(github.ref_name, '-beta') && !(github.event_name == 'workflow_dispatch' && inputs.dry_run)
|
||||
@@ -137,4 +193,15 @@ jobs:
|
||||
env:
|
||||
STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
|
||||
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
|
||||
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
|
||||
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
|
||||
|
||||
# The Play flavor is obfuscated; the bundle embeds this mapping, but keep it reachable
|
||||
# for retracing user-submitted logs without going through Play Console.
|
||||
- name: Archive R8 mapping
|
||||
if: always() && !(github.event_name == 'workflow_dispatch' && inputs.dry_run)
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
|
||||
with:
|
||||
name: r8-mapping-${{ github.ref_name }}
|
||||
path: app/build/outputs/mapping/gplay*/mapping.txt
|
||||
if-no-files-found: warn
|
||||
overwrite: true
|
||||
|
||||
@@ -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
|
||||
+9
-1
@@ -17,5 +17,13 @@
|
||||
.kotlin
|
||||
# Screenshot test reference images (ephemeral, regenerated on demand)
|
||||
app/src/screenshotTest*/reference/
|
||||
# Play Store phone screenshots: commit only en-US.
|
||||
# Full localization is uploaded by occasional manual regen + screenshots_only;
|
||||
# Play Store retains previously-uploaded screenshots for locales not pushed.
|
||||
fastlane/metadata/android/*/images/phoneScreenshots/*.png
|
||||
!fastlane/metadata/android/en-US/images/phoneScreenshots/*.png
|
||||
.codex
|
||||
protocol-research/
|
||||
protocol-research/
|
||||
_site/
|
||||
.jekyll-cache/
|
||||
vendor/bundle/
|
||||
@@ -81,8 +81,7 @@ Currently supported models:
|
||||
|
||||
## Screenshots
|
||||
|
||||
<img src="https://github.com/d4rken-org/capod/raw/main/.assets/screenshots/1.png" width="200"><img src="https://github.com/d4rken-org/capod/raw/main/.assets/screenshots/2.png" width="200"><img src="https://github.com/d4rken-org/capod/raw/main/.assets/screenshots/3.png" width="200"><img src="https://github.com/d4rken-org/capod/raw/main/.assets/screenshots/4.png" width="200">
|
||||
<img src="https://raw.githubusercontent.com/d4rken-org/capod/main/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png" width="200">
|
||||
<img src="https://github.com/d4rken-org/capod/raw/main/fastlane/metadata/android/en-US/images/phoneScreenshots/1_dashboard_light.png" width="100"><img src="https://github.com/d4rken-org/capod/raw/main/fastlane/metadata/android/en-US/images/phoneScreenshots/2_dashboard_dark.png" width="100"><img src="https://github.com/d4rken-org/capod/raw/main/fastlane/metadata/android/en-US/images/phoneScreenshots/3_case_popup.png" width="100"><img src="https://github.com/d4rken-org/capod/raw/main/fastlane/metadata/android/en-US/images/phoneScreenshots/4_widget_configuration.png" width="100"><img src="https://github.com/d4rken-org/capod/raw/main/fastlane/metadata/android/en-US/images/phoneScreenshots/5_device_profiles.png" width="100"><img src="https://github.com/d4rken-org/capod/raw/main/fastlane/metadata/android/en-US/images/phoneScreenshots/6_add_profile.png" width="100"><img src="https://github.com/d4rken-org/capod/raw/main/fastlane/metadata/android/en-US/images/phoneScreenshots/7_device_settings_reactions.png" width="100">
|
||||
|
||||
## Thanks to
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB |
@@ -20,9 +20,6 @@ exclude:
|
||||
- buildSrc
|
||||
- gradle/wrapper
|
||||
- fastlane
|
||||
- Gemfile
|
||||
- Gemfile.lock
|
||||
- crowdin*
|
||||
- app
|
||||
- app-common
|
||||
- CONTRIBUTING.md
|
||||
|
||||
+15
-2
@@ -55,10 +55,12 @@ android {
|
||||
includeInApk = false
|
||||
includeInBundle = false
|
||||
}
|
||||
proguardFiles("proguard-rules-foss.pro")
|
||||
}
|
||||
create("gplay") {
|
||||
dimension = "version"
|
||||
signingConfig = signingConfigs["releaseGplay"]
|
||||
proguardFiles("proguard-rules-gplay.pro")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,6 +132,7 @@ android {
|
||||
//noinspection WrongGradleMethod
|
||||
tasks.withType<Test> {
|
||||
useJUnitPlatform()
|
||||
maxHeapSize = "4g"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -188,14 +191,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")
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# The FOSS build is open source and ships on GitHub/F-Droid; readable stack traces are worth more
|
||||
# than smaller identifiers there. Only the Google Play flavor obfuscates (proguard-rules-gplay.pro).
|
||||
-dontobfuscate
|
||||
@@ -0,0 +1,21 @@
|
||||
# Google Play scores the obfuscation share of every uploaded bundle and restricts listings that
|
||||
# stay below its threshold, so the Play flavor obfuscates. The FOSS flavor does not
|
||||
# (proguard-rules-foss.pro).
|
||||
|
||||
# Keep stack traces retraceable with the mapping file that the bundle embeds.
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
-renamesourcefileattribute SourceFile
|
||||
|
||||
# AAP session logs identify settings and commands by their simple class name
|
||||
# (AapSessionEngine, AapAncController, AapOutboundController). Names only; unused classes are
|
||||
# still removed.
|
||||
-keepnames class eu.darken.capod.pods.core.apple.aap.protocol.AapSetting
|
||||
-keepnames class * extends eu.darken.capod.pods.core.apple.aap.protocol.AapSetting
|
||||
-keepnames class eu.darken.capod.pods.core.apple.aap.protocol.AapCommand
|
||||
-keepnames class * extends eu.darken.capod.pods.core.apple.aap.protocol.AapCommand
|
||||
|
||||
# ViewModel1 derives its log tag from the subclass name (VM:OverviewViewModel).
|
||||
-keepnames class * extends eu.darken.capod.common.uix.ViewModel1
|
||||
|
||||
# Error dialogs and log summaries show exceptions by class name (LocalizedError, asLogSummary).
|
||||
-keepnames class * extends java.lang.Throwable
|
||||
Vendored
+3
-2
@@ -1,8 +1,9 @@
|
||||
-keep class eu.darken.capod.BuildConfig { *; }
|
||||
-dontobfuscate
|
||||
|
||||
# work-runtime 2.7.1 (pulled by Glance) uses Class.newInstance() reflection throughout.
|
||||
# R8 full mode strips no-arg constructors not reachable by static analysis.
|
||||
# Keep all constructors in the work package. Remove when Glance upgrades to work-runtime 2.10+.
|
||||
# See: https://issuetracker.google.com/issues/243257364
|
||||
-keep class androidx.work.** { <init>(...); }
|
||||
-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
|
||||
|
||||
@@ -14,17 +14,22 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.darken.capod.common.bluetooth.BluetoothDevice2
|
||||
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
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import eu.darken.capod.main.ui.widget.ComposeWidgetPreview
|
||||
import eu.darken.capod.main.ui.widget.WidgetConfigurationScreen
|
||||
import eu.darken.capod.main.ui.widget.WidgetConfigurationViewModel
|
||||
import eu.darken.capod.main.ui.widget.WidgetRenderState
|
||||
import eu.darken.capod.main.ui.widget.WidgetTheme
|
||||
import eu.darken.capod.main.ui.widget.withLocalizedPreviewEstimates
|
||||
import eu.darken.capod.pods.core.apple.PodModel
|
||||
import eu.darken.capod.profiles.ui.DeviceManagerScreen
|
||||
import eu.darken.capod.profiles.ui.DeviceManagerViewModel
|
||||
@@ -37,24 +42,58 @@ import eu.darken.capod.reaction.ui.popup.PopUpContent as PopUpCard
|
||||
internal const val DS = "spec:width=1080px,height=2400px,dpi=428"
|
||||
|
||||
@Composable
|
||||
internal fun DashboardContent() = PreviewWrapper {
|
||||
internal fun DashboardContent(showAap: Boolean = false) = PreviewWrapper {
|
||||
val devices = if (showAap) {
|
||||
listOf(
|
||||
MockPodDataProvider.dualPodMonitoredWithAap(),
|
||||
MockPodDataProvider.singlePodMonitoredWithAap(),
|
||||
MockPodDataProvider.unknownMonitored(),
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
MockPodDataProvider.dualPodMonitoredMixed(),
|
||||
MockPodDataProvider.singlePodMonitored(),
|
||||
MockPodDataProvider.unknownMonitored(),
|
||||
)
|
||||
}
|
||||
// Plausible estimates for the mock battery levels: runtime under each gauge, charge ETA in
|
||||
// the charging chip of whichever mock pod is charging.
|
||||
val estimates = if (showAap) {
|
||||
mapOf(
|
||||
"preview-dual-aap" to BatteryEstimate(
|
||||
left = BatteryEstimate.Pod(285, 0.17f, BatteryEstimate.Source.LIVE),
|
||||
right = BatteryEstimate.Pod(160, 0.17f, BatteryEstimate.Source.LIVE),
|
||||
),
|
||||
"preview-single-aap" to BatteryEstimate(
|
||||
headset = BatteryEstimate.Pod(480, 0.05f, BatteryEstimate.Source.LIVE, minutesUntilCharged = 45),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
mapOf(
|
||||
"preview-dual-mixed" to BatteryEstimate(
|
||||
left = BatteryEstimate.Pod(285, 0.17f, BatteryEstimate.Source.LIVE, minutesUntilCharged = 20),
|
||||
right = BatteryEstimate.Pod(160, 0.17f, BatteryEstimate.Source.LIVE),
|
||||
),
|
||||
"preview-single" to BatteryEstimate(
|
||||
headset = BatteryEstimate.Pod(1020, 0.05f, BatteryEstimate.Source.LIVE),
|
||||
),
|
||||
)
|
||||
}
|
||||
OverviewScreen(
|
||||
state = OverviewViewModel.State(
|
||||
now = MOCK_NOW,
|
||||
permissions = emptySet(),
|
||||
devices = listOf(
|
||||
MockPodDataProvider.dualPodMonitoredMixed(),
|
||||
MockPodDataProvider.singlePodMonitored(),
|
||||
MockPodDataProvider.unknownMonitored(),
|
||||
),
|
||||
isDebugMode = false,
|
||||
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),
|
||||
),
|
||||
upgradeInfo = MockPodDataProvider.gplayInfo(isPro = true),
|
||||
showUnmatchedDevices = false,
|
||||
batteryEstimates = estimates,
|
||||
),
|
||||
onRequestPermission = {},
|
||||
onBluetoothSettings = {},
|
||||
@@ -83,19 +122,38 @@ internal fun DeviceProfilesContent() = PreviewWrapper {
|
||||
|
||||
@Composable
|
||||
internal fun AddProfileContent() = PreviewWrapper {
|
||||
val pairedAirPods = BluetoothDevice2(
|
||||
address = "AA:BB:CC:DD:EE:FF",
|
||||
name = "AirPods Pro",
|
||||
seenFirstAt = MOCK_NOW,
|
||||
)
|
||||
val bondedItems = listOf(
|
||||
DeviceProfileCreationViewModel.BondedDeviceItem(
|
||||
device = pairedAirPods,
|
||||
claimedByProfile = null,
|
||||
),
|
||||
DeviceProfileCreationViewModel.BondedDeviceItem(
|
||||
device = BluetoothDevice2(
|
||||
address = "11:22:33:44:55:66",
|
||||
name = "Living Room TV",
|
||||
seenFirstAt = MOCK_NOW,
|
||||
),
|
||||
claimedByProfile = null,
|
||||
),
|
||||
)
|
||||
DeviceProfileCreationScreen(
|
||||
state = DeviceProfileCreationViewModel.State(
|
||||
isEditMode = false,
|
||||
name = "",
|
||||
name = "My AirPods Pro",
|
||||
nameError = null,
|
||||
selectedModel = null,
|
||||
selectedModel = PodModel.AIRPODS_PRO2,
|
||||
availableModels = PodModel.entries.filter { it != PodModel.UNKNOWN },
|
||||
identityKey = null,
|
||||
encryptionKey = null,
|
||||
selectedDevice = null,
|
||||
bondedDeviceItems = emptyList(),
|
||||
selectedDevice = pairedAirPods,
|
||||
bondedDeviceItems = bondedItems,
|
||||
minimumSignalQuality = 0.15f,
|
||||
canSave = false,
|
||||
canSave = true,
|
||||
),
|
||||
onBack = {},
|
||||
onSave = {},
|
||||
@@ -118,6 +176,7 @@ internal fun DeviceSettingsReactionsContent() = PreviewWrapper {
|
||||
now = MOCK_NOW,
|
||||
isPro = true,
|
||||
isNudgeAvailable = true,
|
||||
isClassicallyConnected = true,
|
||||
),
|
||||
onNavigateUp = {},
|
||||
)
|
||||
@@ -199,7 +258,7 @@ internal fun HomescreenWidgetContent() {
|
||||
bgColor = MaterialTheme.colorScheme.surface.toArgb(),
|
||||
textColor = MaterialTheme.colorScheme.onSurface.toArgb(),
|
||||
iconColor = MaterialTheme.colorScheme.onSurface.toArgb(),
|
||||
),
|
||||
).withLocalizedPreviewEstimates(LocalContext.current),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -211,7 +270,7 @@ private fun PreviewDashboardLight() = DashboardContent()
|
||||
|
||||
@Preview(name = "2 - Dashboard Dark", locale = "en", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES, showSystemUi = true)
|
||||
@Composable
|
||||
private fun PreviewDashboardDark() = DashboardContent()
|
||||
private fun PreviewDashboardDark() = DashboardContent(showAap = true)
|
||||
|
||||
@Preview(name = "3 - Case Pop-up", device = DS, showSystemUi = true)
|
||||
@Composable
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Skenk</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Ek het reeds geskenk</string>
|
||||
<string name="foss_upgrade_no_money_label">Ek spandeer al my geld op AirPods</string>
|
||||
<string name="upgrade_foss_preamble">CAPod FOSS is gratis en oopbron. As u dit nuttig vind, oorweeg dit om ontwikkeling te befonds om die projek aan die gang te hou.</string>
|
||||
<string name="upgrade_foss_sponsor_action">Borg ontwikkeling</string>
|
||||
<string name="upgrade_foss_sponsor_subtitle">Geen advertensies. Geen opsporing. Geen Google Play-binding.</string>
|
||||
<string name="upgrade_foss_sponsor_returned_early">Alreeds terug? U ondersteuning hou CAPod aan die lewe.</string>
|
||||
<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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?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="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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?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="upgrade_foss_preamble">برنامج كابود FOSS مجّاني ومفتوح المصدر. إذا وجدته مفيدًا، ففكّر في دعم تطويره للمساعدة في استمرار المشروع.</string>
|
||||
<string name="upgrade_foss_sponsor_action">ادعم التطوير</string>
|
||||
<string name="upgrade_foss_sponsor_subtitle">لا إعلانات. لا تتبع. لا قيود على Google Play.</string>
|
||||
<string name="upgrade_foss_sponsor_returned_early">عدت بسرعة؟ دعمك يبقي CAPod حياً.</string>
|
||||
<string name="upgrade_badge_label">البرمجيات الحرة (FOSS)</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">شكرًا لدعمك تطوير كابود!</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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">İanə ver</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Artıq ianə vermişəm</string>
|
||||
<string name="foss_upgrade_no_money_label">Bütün pulumu AirPods-lara xərcləyirəm</string>
|
||||
<string name="upgrade_foss_preamble">CAPod FOSS pulsuzdur və açıq mənbəlidir. Faydalı tapırsınızsa, layihənin davamına kömək etmək üçün inkişafı maliyyələşdirməyi düşünün.</string>
|
||||
<string name="upgrade_foss_sponsor_action">İnkişafı sponsor edin</string>
|
||||
<string name="upgrade_foss_sponsor_subtitle">Reklam yoxdur. İzləmə yoxdur. Google Play bağlılığı yoxdur.</string>
|
||||
<string name="upgrade_foss_sponsor_returned_early">Artıq geri qayıtdınız? Dəstəyiniz CAPod-u yaşadır.</string>
|
||||
<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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?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="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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?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="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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?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="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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Donacions</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Ja he donat</string>
|
||||
<string name="foss_upgrade_no_money_label">Gasto tots els meus diners en AirPods</string>
|
||||
<string name="upgrade_foss_preamble">El CAPod FOSS és gratuït i de codi obert. Si el trobeu útil, considereu patrocinar el desenvolupament per ajudar a mantenir el projecte en marxa.</string>
|
||||
<string name="upgrade_foss_sponsor_action">Patrocina el desenvolupament</string>
|
||||
<string name="upgrade_foss_sponsor_subtitle">Sense anuncis. Sense seguiment. Sense dependència del Google Play.</string>
|
||||
<string name="upgrade_foss_sponsor_returned_early">Ja heu tornat? El vostre suport manté viu el CAPod.</string>
|
||||
<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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Přispět</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Již jsem přispěl/a</string>
|
||||
<string name="foss_upgrade_no_money_label">Všechny peníze jsem utratil/a za AirPods</string>
|
||||
<string name="upgrade_foss_preamble">Aplikace CAPod FOSS je zdarma a open source. Pokud vám přijde užitečná, zvažte sponzorování vývoje, abyste pomohli udržet projekt v chodu.</string>
|
||||
<string name="upgrade_foss_sponsor_action">Sponzorovat vývoj</string>
|
||||
<string name="upgrade_foss_sponsor_subtitle">Žádné reklamy. Žádné sledování. Žádná závislost na Google Play.</string>
|
||||
<string name="upgrade_foss_sponsor_returned_early">Už jste zpět? Vaše podpora udržuje CAPod naživu.</string>
|
||||
<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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Doner</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Jeg har allerede doneret</string>
|
||||
<string name="foss_upgrade_no_money_label">Jeg bruger alle mine penge på AirPods</string>
|
||||
<string name="upgrade_foss_preamble">CAPod FOSS er gratis og open source. Hvis du finder det nüttigt, kan du overveje at sponsorere udviklingen for at hjælpe projektet med at fortsætte.</string>
|
||||
<string name="upgrade_foss_sponsor_action">Sponsorudvikling</string>
|
||||
<string name="upgrade_foss_sponsor_subtitle">Ingen annoncer. Ingen sporing. Ingen Google Play-lås.</string>
|
||||
<string name="upgrade_foss_sponsor_returned_early">Allerede tilbage? Din støtte holder CAPod i live.</string>
|
||||
<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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Spenden</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Ich habe schon gespendet</string>
|
||||
<string name="foss_upgrade_no_money_label">Hab alles für AirPods ausgegeben</string>
|
||||
<string name="upgrade_foss_preamble">CAPod FOSS ist kostenlos und Open Source. Wenn du es nützlich findest, erwäge, die Entwicklung zu sponsern, um das Projekt am Laufen zu halten.</string>
|
||||
<string name="upgrade_foss_sponsor_action">Fördere Entwicklung</string>
|
||||
<string name="upgrade_foss_sponsor_subtitle">Keine Werbung. Kein Tracking. Keine Google-Play-Bindung.</string>
|
||||
<string name="upgrade_foss_sponsor_returned_early">Schon zurück? Deine Unterstützung hält CAPod am Leben.</string>
|
||||
<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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?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="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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Donar</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Ya doné</string>
|
||||
<string name="foss_upgrade_no_money_label">Gasto todo mi dinero en AirPods</string>
|
||||
<string name="upgrade_foss_preamble">CAPod FOSS es gratuito y de código abierto. Si lo encontrás útil, considerá patrocinar el desarrollo para ayudar a mantener el proyecto en marcha.</string>
|
||||
<string name="upgrade_foss_sponsor_action">Sponsorear el desarrollo</string>
|
||||
<string name="upgrade_foss_sponsor_subtitle">Sin publicidad. Sin rastreo. Sin dependencia de Google Play.</string>
|
||||
<string name="upgrade_foss_sponsor_returned_early">¿Ya te vas? Tu apoyo mantiene CAPod vivo.</string>
|
||||
<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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Donar</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Ya doné</string>
|
||||
<string name="foss_upgrade_no_money_label">Gasto todo mi dinero en AirPods</string>
|
||||
<string name="upgrade_foss_preamble">CAPod FOSS es gratuito y de código abierto. Si lo encuentras útil, considera patrocinar el desarrollo para ayudar a mantener el proyecto.</string>
|
||||
<string name="upgrade_foss_sponsor_action">Patrocinar el desarrollo</string>
|
||||
<string name="upgrade_foss_sponsor_subtitle">Sin anuncios. Sin rastreo. Sin dependencia de Google Play.</string>
|
||||
<string name="upgrade_foss_sponsor_returned_early">¿Ya de vuelta? Tu apoyo mantiene CAPod con vida.</string>
|
||||
<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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Donar</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Ya he donado</string>
|
||||
<string name="foss_upgrade_no_money_label">Gasto todo mi dinero en AirPods</string>
|
||||
<string name="upgrade_foss_preamble">CAPod FOSS es gratuito y de código abierto. Si lo encuentras útil, considera patrocinar el desarrollo para ayudar a mantener el proyecto en marcha.</string>
|
||||
<string name="upgrade_foss_sponsor_action">Patrocinadores</string>
|
||||
<string name="upgrade_foss_sponsor_subtitle">Sin anuncios. Sin rastreo. Sin dependencia de Google Play.</string>
|
||||
<string name="upgrade_foss_sponsor_returned_early">¿Ya de vuelta? Tu apoyo mantiene CAPod vivo.</string>
|
||||
<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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Anneta</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Juba annetasin</string>
|
||||
<string name="foss_upgrade_no_money_label">Kulutasin kogu raha AirPodsidele</string>
|
||||
<string name="upgrade_foss_preamble">CAPod FOSS on tasuta ja avatud lähtekoodiga. Kui sellest on sulle kasu, kaalu arenduse rahastamist, et aidata projekti arendada.</string>
|
||||
<string name="upgrade_foss_sponsor_action">Rahasta arendamist</string>
|
||||
<string name="upgrade_foss_sponsor_subtitle">Reklaami pole. Jälgimist pole. Google Play lukustust pole.</string>
|
||||
<string name="upgrade_foss_sponsor_returned_early">Juba tagasi? Sinu toetus hoiab CAPodi elus.</string>
|
||||
<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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Eman dohaintza</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Dagoeneko eman dut dohaintza</string>
|
||||
<string name="foss_upgrade_no_money_label">Nire diru guztia AirPods-etan gastatzen dut</string>
|
||||
<string name="upgrade_foss_preamble">CAPod FOSS doakoa eta kode irekikoa da. Erabilgarria iruditzen bazaizu, kontuan hartu garapena babestu proiektua aurrera jarraitzeko.</string>
|
||||
<string name="upgrade_foss_sponsor_action">Garapena babestu</string>
|
||||
<string name="upgrade_foss_sponsor_subtitle">Iragarkirik ez. Jarraipenik ez. Google Play-ren lotura ez.</string>
|
||||
<string name="upgrade_foss_sponsor_returned_early">Dagoeneko itzuli? Zure laguntzak CAPod bizirik mantentzen du.</string>
|
||||
<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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?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">من تمام پولم را خرج ایرپاد میکنم</string>
|
||||
<string name="upgrade_foss_preamble">CAPod FOSS رایگان و متن-باز است. اگر آن را مفید یافتید، حمایت مالی از توسعه را در نظر بگیرید تا پروژه ادامه یابد.</string>
|
||||
<string name="upgrade_foss_sponsor_action">حامی توسعه دهنده</string>
|
||||
<string name="upgrade_foss_sponsor_subtitle">بدون تبلیغ. بدون ردیابی. بدون وابستگی به Google Play.</string>
|
||||
<string name="upgrade_foss_sponsor_returned_early">زود برگشتیدی؟ حمایت شما CAPod را زنده نگه میدارد.</string>
|
||||
<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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Lahjoita</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Olen jo lahjoittanut</string>
|
||||
<string name="foss_upgrade_no_money_label">Käytän kaikki rahani AirPodeihin</string>
|
||||
<string name="upgrade_foss_preamble">CAPod FOSS on ilmainen ja avoimen lähdekoodin ohjelma. Jos löydät sen hyödylliseksi, harkitse kehityksen tukemista projektin elässä pitämiseksi.</string>
|
||||
<string name="upgrade_foss_sponsor_action">Sponsoroi kehitystä</string>
|
||||
<string name="upgrade_foss_sponsor_subtitle">Ei mainoksia. Ei seurantaa. Ei Google Play -riippuvuutta.</string>
|
||||
<string name="upgrade_foss_sponsor_returned_early">Jo takaisin? Tukesi pitää CAPod-sovelluksen hengissä.</string>
|
||||
<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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Mag-donate</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Nakapag-donate na ako</string>
|
||||
<string name="foss_upgrade_no_money_label">Ginagastos ko lahat ng pera ko sa AirPods</string>
|
||||
<string name="upgrade_foss_preamble">Ang CAPod FOSS ay libre at open source. Kung nakita mong kapaki-pakinabang ito, isaalang-alang ang pag-sponsor ng development para mapanatiling aktibo ang proyekto.</string>
|
||||
<string name="upgrade_foss_sponsor_action">Mag-sponsor ng development</string>
|
||||
<string name="upgrade_foss_sponsor_subtitle">Walang ads. Walang tracking. Walang Google Play lock-in.</string>
|
||||
<string name="upgrade_foss_sponsor_returned_early">Bumalik na? Ang iyong suporta ay nagpapanatiling buhay ng CAPod.</string>
|
||||
<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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Faire un don</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">J’ai déjà fait un don</string>
|
||||
<string name="foss_upgrade_no_money_label">J’ai dépensé tout mon argent sur des AirPods</string>
|
||||
<string name="upgrade_foss_preamble">CAPod FOSS est gratuite et à code source ouvert. Si vous la trouvez utile, pensez à soutenir le développement pour contribuer à la pérennité du projet.</string>
|
||||
<string name="upgrade_foss_sponsor_action">Soutenir le développement</string>
|
||||
<string name="upgrade_foss_sponsor_subtitle">Pas de publicités. Pas de suivi à la trace. Pas de dépendance à Google Play.</string>
|
||||
<string name="upgrade_foss_sponsor_returned_early">Déjà de retour ? Votre soutien maintient CAPod en vie.</string>
|
||||
<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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Doar</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Xa doei</string>
|
||||
<string name="foss_upgrade_no_money_label">Gasto todos os meus cartos en AirPods</string>
|
||||
<string name="upgrade_foss_preamble">CAPod FOSS é gratuito e de código aberto. Se o atopas útil, considera patrocinar o desenvolvemento para axudar a manter o proxecto en marcha.</string>
|
||||
<string name="upgrade_foss_sponsor_action">Patrocinar desenvolvemento</string>
|
||||
<string name="upgrade_foss_sponsor_subtitle">Sen anuncios. Sen rastrexo. Sen dependencia de Google Play.</string>
|
||||
<string name="upgrade_foss_sponsor_returned_early">És rápido/a! O teu apoio mantén CAPod vivo.</string>
|
||||
<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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?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="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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Donirajte</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Već sam donirao</string>
|
||||
<string name="foss_upgrade_no_money_label">Sav svoj novac trošim na AirPods</string>
|
||||
<string name="upgrade_foss_preamble">CAPod FOSS je besplatan i otvorenog koda. Ako vam je koristan, razmislite o sponzoriranju razvoja kako bi projekt ostao živ.</string>
|
||||
<string name="upgrade_foss_sponsor_action">Podržite razvoj</string>
|
||||
<string name="upgrade_foss_sponsor_subtitle">Bez oglasa. Bez praćenja. Bez zaključanosti na Google Play.</string>
|
||||
<string name="upgrade_foss_sponsor_returned_early">Već se vraćate? Vaša podrška čuva CAPod na životu.</string>
|
||||
<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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Adományoz</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Már adományoztam</string>
|
||||
<string name="foss_upgrade_no_money_label">Minden pénzemet az AirPods-ra költöttem</string>
|
||||
<string name="upgrade_foss_preamble">A CAPod FOSS ingyenes és nyílt forráskódú. Ha hasznosnak találod, fontold meg a fejlesztés támogatását, hogy a projekt tovább élhessen.</string>
|
||||
<string name="upgrade_foss_sponsor_action">Szponzori fejlesztés</string>
|
||||
<string name="upgrade_foss_sponsor_subtitle">Nincs hirdetés. Nincs nyomkövetés. Nincs Google Play-függőség.</string>
|
||||
<string name="upgrade_foss_sponsor_returned_early">Már visszamész? A támogatásodnak köszönhetően él tovább a CAPod.</string>
|
||||
<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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?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="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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Menyumbang</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Saya telah berdonasi</string>
|
||||
<string name="foss_upgrade_no_money_label">Saya menghabiskan semua uang saya untuk AirPods</string>
|
||||
<string name="upgrade_foss_preamble">CAPod FOSS gratis dan sumber terbuka. Jika Anda merasa bermanfaat, pertimbangkan untuk mensponsori pengembangan guna menjaga proyek ini berjalan.</string>
|
||||
<string name="upgrade_foss_sponsor_action">Pengembangan sponsor</string>
|
||||
<string name="upgrade_foss_sponsor_subtitle">Tanpa iklan. Tanpa pelacakan. Tanpa ketergantungan Google Play.</string>
|
||||
<string name="upgrade_foss_sponsor_returned_early">Sudah kembali? Dukungan Anda menjaga CAPod tetap hidup.</string>
|
||||
<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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Gefa</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Ég hef þegar gefið</string>
|
||||
<string name="foss_upgrade_no_money_label">Ég eyði öllum peningunum mínum í AirPods</string>
|
||||
<string name="upgrade_foss_preamble">CAPod FOSS er ókeypis og opinn kóði. Ef þú finnur það gagnlegt, þakkaðu þér fyrir að stuðla að þróun til að hjálpa til við að halda verkefninu gangandi.</string>
|
||||
<string name="upgrade_foss_sponsor_action">Styrkja þróun</string>
|
||||
<string name="upgrade_foss_sponsor_subtitle">Engar auglysíngar. Engin rakning. Ekkert Google Play-lás.</string>
|
||||
<string name="upgrade_foss_sponsor_returned_early">Aftur þegar? Styðjað þít heldur CAPod lifandi.</string>
|
||||
<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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Dona</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Ho già donato</string>
|
||||
<string name="foss_upgrade_no_money_label">Spendo tutti i miei soldi per gli AirPods</string>
|
||||
<string name="upgrade_foss_preamble">CAPod FOSS è gratuito e open source. Se lo trovi utile, considera di sponsorizzare lo sviluppo per contribuire a mantenere il progetto in vita.</string>
|
||||
<string name="upgrade_foss_sponsor_action">Finanzia lo sviluppo</string>
|
||||
<string name="upgrade_foss_sponsor_subtitle">Nessuna pubblicità. Nessun tracciamento. Nessun vincolo con Google Play.</string>
|
||||
<string name="upgrade_foss_sponsor_returned_early">Già via? Il tuo supporto mantiene CAPod in vita.</string>
|
||||
<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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?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="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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?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="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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?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="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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?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="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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Beşdar bike</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Min berê bexş kir</string>
|
||||
<string name="foss_upgrade_no_money_label">Ez hemû pereyê xwe li AirPods xerc dikim</string>
|
||||
<string name="upgrade_foss_preamble">CAPod FOSS belaş û çavkaniya vekirî ye. Ger hûn wê kêrhastî dibînin, fikir bikin ku perkirezan bike da ku alîkariya berdewamî projeya bide.</string>
|
||||
<string name="upgrade_foss_sponsor_action">Pêşdebiriyê teref bike</string>
|
||||
<string name="upgrade_foss_sponsor_subtitle">Reklam tune. Śopandin tune. Girêdana Google Play tune.</string>
|
||||
<string name="upgrade_foss_sponsor_returned_early">Zirû vegeriya? Piştgiriya we CAPod sax dihêle.</string>
|
||||
<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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?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="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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?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="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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?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="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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?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="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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Aukoti</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Aš jau paaukojau</string>
|
||||
<string name="foss_upgrade_no_money_label">Visus savo pinigus išleidžiu „AirPods“ ausinėms</string>
|
||||
<string name="upgrade_foss_preamble">CAPod FOSS yra nemokama ir atviro kodo programa. Jei ji jums naudinga, pagalvokite apie rėmimą, kad projektą būtų galima tęsti.</string>
|
||||
<string name="upgrade_foss_sponsor_action">Remti plėtrą</string>
|
||||
<string name="upgrade_foss_sponsor_subtitle">Jokių reklamų. Jokio sekimo. Jokio „Google Play“ priklausomybės.</string>
|
||||
<string name="upgrade_foss_sponsor_returned_early">Jau grįžte? Jūsų parama palaiko CAPod gyvybę.</string>
|
||||
<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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Ziedot</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Es jau ziedoju</string>
|
||||
<string name="foss_upgrade_no_money_label">Es iztērēju visu savu naudu AirPods austiņām</string>
|
||||
<string name="upgrade_foss_preamble">CAPod FOSS ir bezmaksas un atkļējkoda. Ja tā ir noderba, apsveriet attīstības sponsēšanu, lai palīdzētu turpināt projektu.</string>
|
||||
<string name="upgrade_foss_sponsor_action">Sponsorēt attīstību</string>
|
||||
<string name="upgrade_foss_sponsor_subtitle">Bez reklamām. Bez seklōšanas. Bez Google Play saistībām.</string>
|
||||
<string name="upgrade_foss_sponsor_returned_early">Jau atpakaļ? Jūsu atbalsts uztur CAPod pie dzīvības.</string>
|
||||
<string name="upgrade_badge_label">FOSS</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_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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?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="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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?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="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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?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="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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?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="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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Sumbang</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Saya telah menyumbang</string>
|
||||
<string name="foss_upgrade_no_money_label">Saya belanjakan semua wang saya untuk AirPods</string>
|
||||
<string name="upgrade_foss_preamble">CAPod FOSS adalah percuma dan sumber terbuka. Jika anda mendapatinya berguna, pertimbangkan untuk menaja pembangunannya bagi membantu projek ini terus berjalan.</string>
|
||||
<string name="upgrade_foss_sponsor_action">Pembangunan penaja</string>
|
||||
<string name="upgrade_foss_sponsor_subtitle">Tiada iklan. Tiada penjejakan. Tiada ikatan Google Play.</string>
|
||||
<string name="upgrade_foss_sponsor_returned_early">Sudah kembali? Sokongan anda mengekalkan CAPod.</string>
|
||||
<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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?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 သည် အခမဲ့နှင့် open source ဖြစ်သည်။ အသုံးပြုခုပ်ဆည်းနိုင် အသားဖြစ်ပါက ပရာဗက်ဆက်လက်ရန် ဖွဲ့ဖြီးရေးကို ပံ့ပိုးပေးသည်။</string>
|
||||
<string name="upgrade_foss_sponsor_action">ဖွံ့ဖြိုးတိုးတက်မှုကို ကမကထပံ့ပိုးရန်</string>
|
||||
<string name="upgrade_foss_sponsor_subtitle">ကြောင်ညာမရှိ။ ခြေရာမခံ။ Google Play ချုပ်ချယ်မြုမရှိ။</string>
|
||||
<string name="upgrade_foss_sponsor_returned_early">ပြန်သွားပြီးလါ? သင်၏ ပံ့ပိုးမှု CAPod ကို ရှင်သန်ဆက်လက်ကေစေသည်။</string>
|
||||
<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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Doner</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Jeg har allerede donert</string>
|
||||
<string name="foss_upgrade_no_money_label">Jeg bruker alle pengene mine på AirPods</string>
|
||||
<string name="upgrade_foss_preamble">CAPod FOSS er gratis og åpen kildekode. Hvis du synes den er nyttig, kan du vurdere å sponse utviklingen for å bidra til at prosjektet går videre.</string>
|
||||
<string name="upgrade_foss_sponsor_action">Støtt utviklingen</string>
|
||||
<string name="upgrade_foss_sponsor_subtitle">Ingen reklame. Ingen sporing. Ingen Google Play-avhengighet.</string>
|
||||
<string name="upgrade_foss_sponsor_returned_early">Allerede tilbake? Din støtte holder CAPod i live.</string>
|
||||
<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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?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="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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Doneren</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Ik heb al gedoneerd</string>
|
||||
<string name="foss_upgrade_no_money_label">Ik besteed al mijn geld aan AirPods</string>
|
||||
<string name="upgrade_foss_preamble">CAPod FOSS is gratis en open source. Als je het nuttig vindt, overweeg dan om de ontwikkeling te sponsoren om het project gaande te houden.</string>
|
||||
<string name="upgrade_foss_sponsor_action">Sponsor ontwikkeling</string>
|
||||
<string name="upgrade_foss_sponsor_subtitle">Geen advertenties. Geen tracking. Geen Google Play-vergrendeling.</string>
|
||||
<string name="upgrade_foss_sponsor_returned_early">Alweer terug? Jouw steun houdt CAPod in leven.</string>
|
||||
<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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Doar</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Eu já doei</string>
|
||||
<string name="foss_upgrade_no_money_label">Eu gasto todo o meu dinheiro em AirPods</string>
|
||||
<string name="upgrade_foss_preamble">O CAPod FOSS é gratuito e de código aberto. Se você o achar útil, considere patrocinar o desenvolvimento para ajudar a manter o projeto ativo.</string>
|
||||
<string name="upgrade_foss_sponsor_action">Patrocinador do desenvolvimento</string>
|
||||
<string name="upgrade_foss_sponsor_subtitle">Sem anúncios. Sem rastreamento. Sem dependência do Google Play.</string>
|
||||
<string name="upgrade_foss_sponsor_returned_early">Já voltou? Seu apoio mantém o CAPod vivo.</string>
|
||||
<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>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="foss_upgrade_donate_label">Doar</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">Já doei</string>
|
||||
<string name="foss_upgrade_no_money_label">Gastei todo o meu dinheiro em AirPods</string>
|
||||
<string name="upgrade_foss_preamble">O CAPod FOSS é gratuito e de código aberto. Se o achar útil, considere patrocinar o desenvolvimento para ajudar a manter o projeto ativo.</string>
|
||||
<string name="upgrade_foss_sponsor_action">Patrocine o desenvolvimento</string>
|
||||
<string name="upgrade_foss_sponsor_subtitle">Sem anúncios. Sem rastreamento. Sem dependência do Google Play.</string>
|
||||
<string name="upgrade_foss_sponsor_returned_early">Já está a sair? O seu apoio mantém o CAPod ativo.</string>
|
||||
<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>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user