Compare commits
11 Commits
v2.16.0
...
b93f9759d2
| Author | SHA1 | Date | |
|---|---|---|---|
| b93f9759d2 | |||
| 9cccf0f513 | |||
| 048de67350 | |||
| fbb14f9334 | |||
|
|
db7094c54e | ||
| c70412c782 | |||
| ac0c43f930 | |||
| 314236ac0c | |||
| c6e83fc071 | |||
| 4c1bfc052e | |||
| bba536394f |
30
.forgejo/ISSUE_TEMPLATE/bug_report.md
Normal file
30
.forgejo/ISSUE_TEMPLATE/bug_report.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Something doesn't work the way it should
|
||||||
|
title: ""
|
||||||
|
labels:
|
||||||
|
- bug
|
||||||
|
---
|
||||||
|
|
||||||
|
### What happened
|
||||||
|
|
||||||
|
|
||||||
|
### What you expected
|
||||||
|
|
||||||
|
|
||||||
|
### Steps to reproduce
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
|
||||||
|
### Environment
|
||||||
|
- Calendula version: <!-- Settings → bottom of the screen -->
|
||||||
|
- Android version:
|
||||||
|
- Device:
|
||||||
|
- Installed from: <!-- official F-Droid / the self-hosted repo / built from source -->
|
||||||
|
- Affected calendar: <!-- Google, CalDAV (DAVx5, Nextcloud, …), on-device/local,
|
||||||
|
subscribed/WebCal, birthdays — provider behaviour differs
|
||||||
|
a lot per account type, so this often points straight at
|
||||||
|
the cause -->
|
||||||
|
- Time zone: <!-- only if the problem involves dates or all-day events -->
|
||||||
|
|
||||||
18
.forgejo/ISSUE_TEMPLATE/config.yml
Normal file
18
.forgejo/ISSUE_TEMPLATE/config.yml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Kept enabled so anything that doesn't fit the four templates still has a way
|
||||||
|
# in (the `ToDo` label exists for exactly those).
|
||||||
|
blank_issues_enabled: true
|
||||||
|
|
||||||
|
contact_links:
|
||||||
|
- name: Translate Calendula
|
||||||
|
url: https://weblate.dev.jeanlucmakiola.de/engage/calendula/
|
||||||
|
about: >-
|
||||||
|
Translations are managed on Weblate, not here — it owns every values-*
|
||||||
|
file, so a hand-edited translation gets overwritten on the next sync.
|
||||||
|
No coding needed: pick or request a language and translate in the browser.
|
||||||
|
|
||||||
|
- name: Contributing guide
|
||||||
|
url: https://codeberg.org/jlmakiola/calendula/src/branch/main/CONTRIBUTING.md
|
||||||
|
about: >-
|
||||||
|
Before opening a pull request: the issue-first workflow, which release
|
||||||
|
branch to target, how to build (there's a submodule), and the
|
||||||
|
architectural rules a change is reviewed against.
|
||||||
@@ -5,7 +5,7 @@ title: "Crash: "
|
|||||||
labels:
|
labels:
|
||||||
- bug
|
- bug
|
||||||
- crash
|
- crash
|
||||||
- priority:high
|
- priority/high
|
||||||
---
|
---
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
41
.forgejo/PULL_REQUEST_TEMPLATE.md
Normal file
41
.forgejo/PULL_REQUEST_TEMPLATE.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<!--
|
||||||
|
Thanks for contributing to Calendula!
|
||||||
|
|
||||||
|
Please skim CONTRIBUTING.md if you haven't:
|
||||||
|
https://codeberg.org/jlmakiola/calendula/src/branch/main/CONTRIBUTING.md
|
||||||
|
|
||||||
|
Two things it's easy to get wrong:
|
||||||
|
• Features need a discussed issue first — an undiscussed feature PR may be
|
||||||
|
closed unmerged even when the code is good.
|
||||||
|
• Target the release branch for your issue's milestone (milestone 2.18.0 →
|
||||||
|
release/v2.18.0), not main. If you targeted main, just say so below and it
|
||||||
|
will be retargeted.
|
||||||
|
-->
|
||||||
|
|
||||||
|
### What this changes
|
||||||
|
|
||||||
|
|
||||||
|
### Why
|
||||||
|
|
||||||
|
<!-- Closes #123 — link the issue this implements or fixes. -->
|
||||||
|
|
||||||
|
|
||||||
|
### How it was tested
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Which of these ran green, and anything you exercised by hand. On-device notes
|
||||||
|
are especially useful for UI changes.
|
||||||
|
|
||||||
|
./gradlew lint test assembleDebug
|
||||||
|
python3 scripts/check_translations.py
|
||||||
|
-->
|
||||||
|
|
||||||
|
|
||||||
|
### Checklist
|
||||||
|
|
||||||
|
- [ ] There's an issue for this, and (for a feature) it got a go-ahead
|
||||||
|
- [ ] Targeting the release branch for that issue's milestone — or `main`, noted above
|
||||||
|
- [ ] `./gradlew lint test assembleDebug` passes locally
|
||||||
|
- [ ] No `values-*/strings.xml` touched (Weblate owns those; new English strings in `values/` are fine)
|
||||||
|
- [ ] `CHANGELOG.md` updated under `## [Unreleased]`, if the change is user-visible
|
||||||
|
- [ ] No planning or design documents committed
|
||||||
@@ -72,9 +72,14 @@ jobs:
|
|||||||
distribution: 'zulu'
|
distribution: 'zulu'
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
|
|
||||||
|
# Fully qualified on purpose. Codeberg resolves bare `uses:` refs against
|
||||||
|
# data.forgejo.org, Forgejo's own action mirror — actions/checkout,
|
||||||
|
# setup-java and cache all exist there, but android-actions/setup-android
|
||||||
|
# does not, and the job dies with "repository not found". Gitea's instance
|
||||||
|
# defaults to GitHub, which is why this never surfaced before the split.
|
||||||
- name: Setup Android SDK
|
- name: Setup Android SDK
|
||||||
if: steps.scope.outputs.code == 'true'
|
if: steps.scope.outputs.code == 'true'
|
||||||
uses: android-actions/setup-android@v3
|
uses: https://github.com/android-actions/setup-android@v3
|
||||||
with:
|
with:
|
||||||
# Default ("tools platform-tools") drags in the Android Emulator
|
# Default ("tools platform-tools") drags in the Android Emulator
|
||||||
# (~300 MB) which the build never uses.
|
# (~300 MB) which the build never uses.
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Something doesn't work the way it should
|
|
||||||
title: ""
|
|
||||||
labels:
|
|
||||||
- bug
|
|
||||||
---
|
|
||||||
|
|
||||||
### What happened
|
|
||||||
|
|
||||||
|
|
||||||
### What you expected
|
|
||||||
|
|
||||||
|
|
||||||
### Steps to reproduce
|
|
||||||
1.
|
|
||||||
2.
|
|
||||||
3.
|
|
||||||
|
|
||||||
### Environment
|
|
||||||
- Calendula version: <!-- Settings → bottom of the screen -->
|
|
||||||
- Android version:
|
|
||||||
- Device:
|
|
||||||
@@ -27,6 +27,14 @@ jobs:
|
|||||||
# whether this push actually cuts a new release (no tag for it yet). Keeps the
|
# whether this push actually cuts a new release (no tag for it yet). Keeps the
|
||||||
# heavy job from running on every merge to main.
|
# heavy job from running on every merge to main.
|
||||||
detect:
|
detect:
|
||||||
|
# Gitea only. The workflow directory split already keeps this file invisible
|
||||||
|
# to Codeberg — Forgejo's lookup is first-match-wins, and .forgejo/workflows
|
||||||
|
# exists — but that only holds while .forgejo/ is non-empty. Move the last
|
||||||
|
# file out of it and Codeberg would fall back to .gitea/workflows and start
|
||||||
|
# running the release pipeline on the contributor-facing runner, with no
|
||||||
|
# secrets. repository_owner differs between the two forges regardless of
|
||||||
|
# URL, proxy or instance rename, so this closes it permanently.
|
||||||
|
if: github.repository_owner == 'makiolaj'
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
outputs:
|
outputs:
|
||||||
is_release: ${{ steps.v.outputs.is_release }}
|
is_release: ${{ steps.v.outputs.is_release }}
|
||||||
@@ -41,8 +49,16 @@ jobs:
|
|||||||
- name: Resolve version and whether it is a new release
|
- name: Resolve version and whether it is a new release
|
||||||
id: v
|
id: v
|
||||||
env:
|
env:
|
||||||
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
# Tags are read from Codeberg, which is canonical — deliberately NOT
|
||||||
API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
|
# from the Gitea API this workflow runs on. The Codeberg -> Gitea sync
|
||||||
|
# is a push mirror, i.e. `git push --mirror`, which deletes refs the
|
||||||
|
# source does not have. A tag minted here on Gitea is therefore wiped
|
||||||
|
# by the next sync (Codeberg does not have it yet) and only reappears
|
||||||
|
# once the tag push at the end of this workflow propagates back.
|
||||||
|
# Asking Gitea inside that window would report "no tag" for a release
|
||||||
|
# that already shipped, and cut it a second time.
|
||||||
|
# Public repo, so this read needs no token.
|
||||||
|
TAG_API: https://codeberg.org/api/v1/repos/jlmakiola/calendula
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
VERSION=$(grep -oP 'versionName\s*=\s*"\K[^"]+' app/build.gradle.kts)
|
VERSION=$(grep -oP 'versionName\s*=\s*"\K[^"]+' app/build.gradle.kts)
|
||||||
@@ -60,15 +76,28 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
# A tag for this version already existing means the release shipped on
|
# A tag for this version already existing means the release shipped on
|
||||||
# an earlier push; do nothing. Absent => this merge cuts the release.
|
# an earlier push; do nothing. Absent => this merge cuts the release.
|
||||||
STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
|
#
|
||||||
-H "Authorization: token $TOKEN" "$API/git/refs/tags/v$VERSION")
|
# Anything other than a clean 200/404 is treated as fatal rather than
|
||||||
if [ "$STATUS" = "200" ]; then
|
# as "no tag". A Codeberg outage or a network blip would otherwise
|
||||||
echo "Tag v$VERSION already exists — nothing to release."
|
# read as absent and re-cut a release that has already shipped —
|
||||||
echo "is_release=false" >> "$GITHUB_OUTPUT"
|
# republishing to F-Droid and Play. Failing here is recoverable; a
|
||||||
else
|
# duplicate release is not.
|
||||||
echo "No tag for v$VERSION yet — cutting the release."
|
STATUS=$(curl -s -o /dev/null -w '%{http_code}' "$TAG_API/git/refs/tags/v$VERSION" || echo 000)
|
||||||
echo "is_release=true" >> "$GITHUB_OUTPUT"
|
case "$STATUS" in
|
||||||
fi
|
200)
|
||||||
|
echo "Tag v$VERSION already exists on Codeberg — nothing to release."
|
||||||
|
echo "is_release=false" >> "$GITHUB_OUTPUT"
|
||||||
|
;;
|
||||||
|
404)
|
||||||
|
echo "No tag for v$VERSION on Codeberg yet — cutting the release."
|
||||||
|
echo "is_release=true" >> "$GITHUB_OUTPUT"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Codeberg tag lookup for v$VERSION returned HTTP $STATUS." >&2
|
||||||
|
echo "Refusing to guess: treating this as 'no tag' could re-cut a shipped release." >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
# Releases: build + sign + publish, then mint the tag and Gitea release.
|
# Releases: build + sign + publish, then mint the tag and Gitea release.
|
||||||
# Also runs on manual dispatch, where it skips the build and just re-signs and
|
# Also runs on manual dispatch, where it skips the build and just re-signs and
|
||||||
@@ -412,20 +441,31 @@ jobs:
|
|||||||
"prerelease": False,
|
"prerelease": False,
|
||||||
}))
|
}))
|
||||||
PY
|
PY
|
||||||
# Upsert (re-run safe). POST also creates the tag at target_commitish
|
# Create (or update) the release. Codeberg 500s on a POST/GET against a
|
||||||
# if the push mirror hasn't synced it yet.
|
# tag it has only just received — the release request outruns the
|
||||||
ID=$(curl -s -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG" | jq -r '.id // empty')
|
# indexing of the ref we pushed a moment ago — so a single attempt kept
|
||||||
if [ -n "$ID" ]; then
|
# failing and skipping the mirror even though the very same call
|
||||||
curl -s -o /dev/null -w "release PATCH HTTP %{http_code}\n" -X PATCH \
|
# succeeds seconds later. Retry with backoff, and PATCH in place if a
|
||||||
|
# release already exists (re-run safe). A 5xx body still exits curl 0,
|
||||||
|
# so the loop, not `set -e`, controls the flow.
|
||||||
|
ID=""
|
||||||
|
for attempt in 1 2 3 4 5 6; do
|
||||||
|
EXIST=$(curl -s -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG" | jq -r '.id // empty' 2>/dev/null || true)
|
||||||
|
if [ -n "$EXIST" ]; then
|
||||||
|
curl -s -o /dev/null -w "release PATCH HTTP %{http_code}\n" -X PATCH \
|
||||||
|
-H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
||||||
|
-d @cb-payload.json "$API/releases/$EXIST"
|
||||||
|
ID="$EXIST"; break
|
||||||
|
fi
|
||||||
|
CODE=$(curl -s -o cb-response.json -w "%{http_code}" -X POST \
|
||||||
-H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
-H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
||||||
-d @cb-payload.json "$API/releases/$ID"
|
-d @cb-payload.json "$API/releases")
|
||||||
else
|
echo "release POST attempt $attempt HTTP $CODE"
|
||||||
curl -s -o cb-response.json -w "release POST HTTP %{http_code}\n" -X POST \
|
|
||||||
-H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
|
||||||
-d @cb-payload.json "$API/releases"
|
|
||||||
ID=$(jq -r '.id // empty' cb-response.json 2>/dev/null || true)
|
ID=$(jq -r '.id // empty' cb-response.json 2>/dev/null || true)
|
||||||
fi
|
[ -n "$ID" ] && break
|
||||||
if [ -z "$ID" ]; then echo "Could not resolve Codeberg release id." >&2; exit 1; fi
|
sleep $((attempt * 10))
|
||||||
|
done
|
||||||
|
if [ -z "$ID" ]; then echo "Could not resolve Codeberg release id after retries." >&2; exit 1; fi
|
||||||
|
|
||||||
# Attach APK + checksum, replacing any prior asset of the same name.
|
# Attach APK + checksum, replacing any prior asset of the same name.
|
||||||
for A in "$ASSET_APK" "$ASSET_SUM"; do
|
for A in "$ASSET_APK" "$ASSET_SUM"; do
|
||||||
|
|||||||
@@ -29,14 +29,30 @@ jobs:
|
|||||||
- name: Run Renovate
|
- name: Run Renovate
|
||||||
run: renovate
|
run: renovate
|
||||||
env:
|
env:
|
||||||
# Self-hosted Gitea, not github.com.
|
# Renovate targets Codeberg (canonical) while still RUNNING on the
|
||||||
RENOVATE_PLATFORM: gitea
|
# Gitea runner. Moving the job to Codeberg would put a repo-write
|
||||||
RENOVATE_ENDPOINT: https://gitea.jeanlucmakiola.de/api/v1
|
# token on the contributor-facing runner, which is exactly what the
|
||||||
# Bot-account token (Gitea secret). Needs repo read/write + PR scope.
|
# .forgejo/ vs .gitea/ split exists to prevent — so the token stays
|
||||||
|
# where the other secrets live and only the API calls cross over.
|
||||||
|
#
|
||||||
|
# Platform is `forgejo`, not `gitea`: Codeberg runs Forgejo, and the
|
||||||
|
# pinned image ships a distinct forgejo platform module.
|
||||||
|
RENOVATE_PLATFORM: forgejo
|
||||||
|
RENOVATE_ENDPOINT: https://codeberg.org/api/v1
|
||||||
|
# Codeberg bot-account token (Gitea secret). Needs repo read/write +
|
||||||
|
# PR scope on jlmakiola/calendula.
|
||||||
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
|
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
|
||||||
# Scope to this repo only — no org-wide autodiscovery.
|
# Scope to this repo only — no org-wide autodiscovery.
|
||||||
RENOVATE_AUTODISCOVER: 'false'
|
RENOVATE_AUTODISCOVER: 'false'
|
||||||
RENOVATE_REPOSITORIES: '["makiolaj/calendula"]'
|
RENOVATE_REPOSITORIES: '["jlmakiola/calendula"]'
|
||||||
# Commits/PRs authored as the bot, not a real maintainer.
|
# Commits/PRs authored as the bot, not a real maintainer. This address
|
||||||
|
# must be a verified email on the Codeberg bot account, otherwise the
|
||||||
|
# commits show up unattributed there.
|
||||||
RENOVATE_GIT_AUTHOR: 'Renovate Bot <renovate@jeanlucmakiola.de>'
|
RENOVATE_GIT_AUTHOR: 'Renovate Bot <renovate@jeanlucmakiola.de>'
|
||||||
|
# Read-only github.com PAT (no scopes needed). Unaffected by the forge
|
||||||
|
# move — nearly every dependency is *released* on GitHub, and without
|
||||||
|
# this,
|
||||||
|
# changelog/release-note lookups hit the 60/h anonymous rate limit
|
||||||
|
# and PRs arrive with an empty "Release Notes" section.
|
||||||
|
RENOVATE_GITHUB_COM_TOKEN: ${{ secrets.GITHUB_COM_TOKEN }}
|
||||||
LOG_LEVEL: info
|
LOG_LEVEL: info
|
||||||
|
|||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -50,6 +50,11 @@ google-services.json
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
|
# Editor swap/backup files
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
# F-Droid local artifacts (the pipeline generates them in CI)
|
# F-Droid local artifacts (the pipeline generates them in CI)
|
||||||
/fdroid/
|
/fdroid/
|
||||||
|
|
||||||
|
|||||||
2
.gitmodules
vendored
2
.gitmodules
vendored
@@ -1,3 +1,3 @@
|
|||||||
[submodule "floret-kit"]
|
[submodule "floret-kit"]
|
||||||
path = floret-kit
|
path = floret-kit
|
||||||
url = https://gitea.jeanlucmakiola.de/makiolaj/floret-kit.git
|
url = https://codeberg.org/jlmakiola/floret-kit.git
|
||||||
|
|||||||
@@ -40,5 +40,9 @@ shows a stylized "1" on a slate squircle.
|
|||||||
|
|
||||||
## Source
|
## Source
|
||||||
|
|
||||||
Hosted on self-hosted Gitea, released through self-hosted F-Droid repo on
|
**Codeberg (`jlmakiola/calendula`) is canonical** — git, issues, PRs, tags and
|
||||||
Hetzner. Same infrastructure as `HouseHoldKeaper`.
|
releases, plus contributor CI. The self-hosted Gitea instance is build
|
||||||
|
infrastructure only: it holds the signing key, runs the release pipeline, and
|
||||||
|
publishes the self-hosted F-Droid repo on Hetzner. Codeberg push-mirrors `main`
|
||||||
|
and tags to Gitea, where a bumped `versionName` triggers the release. Also
|
||||||
|
published to the official F-Droid repo. See `docs/RELEASING.md`.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Calendula — Requirements
|
# Calendula — Requirements
|
||||||
|
|
||||||
See full design spec: `docs/superpowers/specs/2026-06-08-calendar-app-design.md`
|
Architecture reference: [`../docs/ARCHITECTURE.md`](../docs/ARCHITECTURE.md)
|
||||||
|
|
||||||
## V1 Scope (Variant "B") — shipped in full (v1.0.0, 2026-06-11)
|
## V1 Scope (Variant "B") — shipped in full (v1.0.0, 2026-06-11)
|
||||||
|
|
||||||
|
|||||||
@@ -55,9 +55,8 @@ after v0.6 (full event read) plus the onboarding-screen polish pass.
|
|||||||
|
|
||||||
## v2.0 — Write Support (complete, shipped 2026-06-11)
|
## v2.0 — Write Support (complete, shipped 2026-06-11)
|
||||||
|
|
||||||
Delivered in four releasable slices (plan:
|
Delivered in four releasable slices. The V1 spec is a guide here, not a
|
||||||
`docs/superpowers/plans/2026-06-11-03-write-support.md`). The V1 spec is a
|
contract — scope per slice is decided as we go.
|
||||||
guide here, not a contract — scope per slice is decided as we go.
|
|
||||||
|
|
||||||
| Version | Milestone | Status |
|
| Version | Milestone | Status |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -244,8 +243,7 @@ pass on the existing controls; new toggles ride in with their own features.
|
|||||||
sequential branches: `feat/ics-export` (write side + UID-on-create precursor)
|
sequential branches: `feat/ics-export` (write side + UID-on-create precursor)
|
||||||
then `feat/ics-import` (parser, restore, dedup by UID). Import is
|
then `feat/ics-import` (parser, restore, dedup by UID). Import is
|
||||||
liberal-in/strict-out: skip-and-report foreign `VTIMEZONE` / `RECURRENCE-ID`
|
liberal-in/strict-out: skip-and-report foreign `VTIMEZONE` / `RECURRENCE-ID`
|
||||||
/ guest lists it can't model. Plans:
|
/ guest lists it can't model.
|
||||||
`docs/superpowers/plans/2026-06-18-05-ics-export.md` + `…-06-ics-import.md`.
|
|
||||||
11. **Snooze / dismiss notification actions** *(merged into release/v2.8.0)* —
|
11. **Snooze / dismiss notification actions** *(merged into release/v2.8.0)* —
|
||||||
followed the `.ics` work; inherits v2.6's deferred exact-alarm/WorkManager
|
followed the `.ics` work; inherits v2.6's deferred exact-alarm/WorkManager
|
||||||
decision (snooze must re-fire an alarm).
|
decision (snooze must re-fire an alarm).
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ release-cut.
|
|||||||
|
|
||||||
## Progress
|
## Progress
|
||||||
|
|
||||||
- [x] Design spec written and committed (`docs/superpowers/specs/2026-06-08-calendar-app-design.md`)
|
- [x] Design spec written and committed (2026-06-08)
|
||||||
- [x] V1 design decisions resolved (App name "Calendula", icon, seed color)
|
- [x] V1 design decisions resolved (App name "Calendula", icon, seed color)
|
||||||
- [x] Plan 01 written and executed — foundation lands (theme, icon, i18n, Hilt, DataStore, CI green)
|
- [x] Plan 01 written and executed — foundation lands (theme, icon, i18n, Hilt, DataStore, CI green)
|
||||||
- [x] Plan 02 written and executed — data layer + permission flow + debug screen
|
- [x] Plan 02 written and executed — data layer + permission flow + debug screen
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Calendula's source code now lives on **Codeberg**, where its issues already
|
||||||
|
were. The **Source code** and **License** links in Settings → About point
|
||||||
|
there, so reporting a bug and reading the code no longer land on two different
|
||||||
|
sites. Nothing about the app itself changes, and the F-Droid repository is
|
||||||
|
unaffected.
|
||||||
|
|
||||||
## [2.16.0] — 2026-07-24
|
## [2.16.0] — 2026-07-24
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
192
CONTRIBUTING.md
Normal file
192
CONTRIBUTING.md
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
# Contributing to Calendula
|
||||||
|
|
||||||
|
Calendula is a Material 3 Expressive calendar app that lives strictly on top of
|
||||||
|
Android's `CalendarContract` — no app database, no sync stack, no network access.
|
||||||
|
That constraint shapes most review comments, so
|
||||||
|
[`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) is worth skimming before you write
|
||||||
|
code. This file is the practical how.
|
||||||
|
|
||||||
|
**[Codeberg](https://codeberg.org/jlmakiola/calendula) is the canonical home** —
|
||||||
|
issues, pull requests, releases. The self-hosted Gitea instance referenced in the
|
||||||
|
release docs is build infrastructure only; there is nothing to contribute there.
|
||||||
|
Be decent to the people you meet in the tracker.
|
||||||
|
|
||||||
|
## Start with an issue
|
||||||
|
|
||||||
|
| You want to | Do this |
|
||||||
|
|---|---|
|
||||||
|
| Add a feature | **Open an issue first** and wait for a go-ahead |
|
||||||
|
| Fix a bug | Open an issue, then a pull request |
|
||||||
|
| Fix a typo, a comment, or docs | Just open the pull request |
|
||||||
|
| Add or fix a translation | **Don't** — [use Weblate](#translations) |
|
||||||
|
|
||||||
|
Features get an opinion before they get code: whether Calendula should do a
|
||||||
|
thing at all is the one decision a patch can't make. A feature PR that arrives
|
||||||
|
without a discussed issue may be closed unmerged even when the code is good —
|
||||||
|
please don't spend a weekend on one first.
|
||||||
|
|
||||||
|
Bugs are more straightforward, but still start with an issue: it's what carries
|
||||||
|
the milestone and gives the changelog something to link.
|
||||||
|
|
||||||
|
Issue templates cover bug, crash, feature and question. For a crash, let the app
|
||||||
|
do the work — **Settings → Report a problem**, or the prompt shown after a crash,
|
||||||
|
captures the stack trace and prefills the form. The report contains app, Android
|
||||||
|
and device versions plus the trace; no calendar content, no personal data.
|
||||||
|
|
||||||
|
## Which branch to target
|
||||||
|
|
||||||
|
Calendula releases by merging a version bump into `main`, so `main` is a release
|
||||||
|
trigger rather than a staging area. Work is assembled on release branches first.
|
||||||
|
|
||||||
|
Once your issue has a milestone, that milestone names your branch:
|
||||||
|
|
||||||
|
| Milestone | Target branch |
|
||||||
|
|---|---|
|
||||||
|
| `2.18.0` | `release/v2.18.0` |
|
||||||
|
|
||||||
|
Every milestone has a matching branch. If it's somehow missing, target `main` and
|
||||||
|
mention it in the PR — it will be retargeted. Don't pick an older release branch:
|
||||||
|
they're kept after shipping, so the newest one isn't necessarily yours.
|
||||||
|
|
||||||
|
## Translations
|
||||||
|
|
||||||
|
**Never edit a `values-*/strings.xml` file in a pull request** — German included.
|
||||||
|
Translations are owned by a self-hosted Weblate that writes to this repository
|
||||||
|
directly, and a hand-edit is overwritten on the next sync.
|
||||||
|
|
||||||
|
→ **[Translate Calendula on Weblate](https://weblate.dev.jeanlucmakiola.de/engage/calendula/)**
|
||||||
|
|
||||||
|
Adding a *new* English string to `values/strings.xml` is normal PR work; Weblate
|
||||||
|
picks it up and offers it to translators. Partial translations are expected and
|
||||||
|
fine — missing keys are informational. Stale and orphaned keys are not, so run
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 scripts/check_translations.py
|
||||||
|
```
|
||||||
|
|
||||||
|
before pushing. It reports those more clearly than lint's `MissingTranslation`
|
||||||
|
does.
|
||||||
|
|
||||||
|
## Build & test
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone --recurse-submodules https://codeberg.org/jlmakiola/calendula.git
|
||||||
|
```
|
||||||
|
|
||||||
|
The `floret-kit` submodule is a composite build compiled from source. An
|
||||||
|
existing clone needs `git submodule update --init --recursive`, or nothing
|
||||||
|
resolves.
|
||||||
|
|
||||||
|
- **JDK 17** — not newer; the Android Gradle Plugin requires exactly 17. Set
|
||||||
|
`JAVA_HOME` if your default differs.
|
||||||
|
- **Android SDK** — platform 37 (`compileSdk`) and build-tools 36.0.0, located
|
||||||
|
via `ANDROID_HOME` or a gitignored `local.properties` with `sdk.dir`. If you
|
||||||
|
go the `local.properties` route the included build needs its own copy at
|
||||||
|
`floret-kit/local.properties`; `ANDROID_HOME` covers both at once and is the
|
||||||
|
easier path.
|
||||||
|
|
||||||
|
The Gradle wrapper is checked in, so no system Gradle is needed.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./gradlew lint test assembleDebug # roughly what CI runs
|
||||||
|
```
|
||||||
|
|
||||||
|
A single test class, or a pattern:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./gradlew testDebugUnitTest --tests "de.jeanlucmakiola.calendula.domain.SimpleRecurrenceTest"
|
||||||
|
./gradlew testDebugUnitTest --tests "*SimpleRecurrence*"
|
||||||
|
```
|
||||||
|
|
||||||
|
CI reports one `CI` check per pull request: `lintDebug`, `testDebugUnitTest`,
|
||||||
|
`assembleDebug`, and a Trivy scan. Pull requests touching only docs, F-Droid
|
||||||
|
metadata or the licence skip the Android build and go green quickly. More detail
|
||||||
|
in [`docs/BUILDING.md`](docs/BUILDING.md).
|
||||||
|
|
||||||
|
## The rules
|
||||||
|
|
||||||
|
These are the ones that turn into review comments.
|
||||||
|
|
||||||
|
1. **No network.** Calendula holds no `INTERNET` permission, and that's a
|
||||||
|
feature rather than an oversight. Anything that would need one is a product
|
||||||
|
decision before it's a patch — the crash reporter deliberately opens a
|
||||||
|
prefilled web issue instead of posting anything itself.
|
||||||
|
2. **The provider is the only database.** No Room, no cache, no local mirror of
|
||||||
|
events. `CalendarContract` is the single source of truth, which is also why
|
||||||
|
externally synced changes work for free.
|
||||||
|
3. **Don't patch UI state after a write.** A `ContentObserver` re-queries and
|
||||||
|
views recompose from fresh provider state. Hand-patching a list after saving
|
||||||
|
appears to work, then quietly diverges from what the provider actually stored.
|
||||||
|
4. **`domain/` has no Android imports.** Models, validation, recurrence
|
||||||
|
rendering, conflict snapshots and the `.ics` codec stay pure Kotlin so they
|
||||||
|
remain JVM-testable.
|
||||||
|
5. **Tests run on the JVM.** JUnit 5 + Truth + Turbine. The seams exist for you:
|
||||||
|
fake the data source (`FakeCalendarDataSource`), and feed mappers plain maps
|
||||||
|
through `ColumnReader` instead of cursors. Instrumented tests are a last
|
||||||
|
resort, not a default.
|
||||||
|
6. **Read before touching the subtle pipelines.** Recurring writes (UNTIL vs
|
||||||
|
DURATION, exception URIs, series splits), save-conflict detection and reminder
|
||||||
|
delivery (post-before-mark) follow provider-driven rules that are documented
|
||||||
|
in [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) and are not guessable from
|
||||||
|
the code alone.
|
||||||
|
7. **Don't break reproducible builds.** `vcsInfo`, `dependenciesInfo` and the AGP
|
||||||
|
metadata block are disabled on purpose so the official F-Droid repo can verify
|
||||||
|
our binary against a from-source rebuild.
|
||||||
|
`scripts/check_reproducible_release.sh` runs on every pull request, including
|
||||||
|
docs-only ones.
|
||||||
|
|
||||||
|
## UI conventions
|
||||||
|
|
||||||
|
Material 3 Expressive throughout, built from the system's own tokens and
|
||||||
|
components — colour-scheme tokens rather than hardcoded colours, `ListItem` for
|
||||||
|
settings rows.
|
||||||
|
|
||||||
|
**Selection pickers are full-screen.** Every browse-style "choose one" surface
|
||||||
|
uses floret-kit's `FullScreenPicker` / `OptionPicker`; one that needs a commit or
|
||||||
|
extra action passes it through the picker's `actions` slot. The exception is the
|
||||||
|
recurring-scope chooser (*this / this and following / all*), which stays a
|
||||||
|
compact dialog — a two- or three-option decision reads better as a popup than as
|
||||||
|
a nearly empty screen. `AlertDialog` is for plain confirmations only, and radio-
|
||||||
|
or text-list dialogs aren't used at all.
|
||||||
|
|
||||||
|
Shared UI machinery lives in the `floret-kit` submodule and has
|
||||||
|
[its own contributing guide](https://codeberg.org/jlmakiola/floret-kit/src/branch/main/CONTRIBUTING.md);
|
||||||
|
changing it means a pull request against that repository plus a submodule bump
|
||||||
|
here.
|
||||||
|
|
||||||
|
## Commits & pull requests
|
||||||
|
|
||||||
|
Conventional commits, scoped to the area you touched:
|
||||||
|
|
||||||
|
```
|
||||||
|
fix(calendars): keep an event's own calendar when it is switched off
|
||||||
|
feat(month): pull-to-expand the split view (#38)
|
||||||
|
docs(architecture): record what the second review pass changed
|
||||||
|
```
|
||||||
|
|
||||||
|
Types in use: `feat` `fix` `docs` `refactor` `style` `chore` `ci` `build`
|
||||||
|
`revert`. Reference the issue in the subject or the body. Keep commits small —
|
||||||
|
small commits revert cleanly, which matters more here than a tidy history.
|
||||||
|
|
||||||
|
If your change is user-visible, add an entry under `## [Unreleased]` in
|
||||||
|
[`CHANGELOG.md`](CHANGELOG.md). Match the surrounding voice: entries describe
|
||||||
|
what changed *for the person using the app*, and why, not what changed in the
|
||||||
|
code. Link the issue and add its reference at the bottom of the file. It may get
|
||||||
|
reworded when the release is cut, so don't agonise over it.
|
||||||
|
|
||||||
|
Please don't commit planning or design documents. Code, tests, architecture notes
|
||||||
|
and the changelog land; the reasoning belongs in the commit message and the
|
||||||
|
issue.
|
||||||
|
|
||||||
|
## Not going to happen
|
||||||
|
|
||||||
|
Saving you the effort:
|
||||||
|
|
||||||
|
- **Drag-and-drop event rescheduling.** Considered repeatedly, settled as a no.
|
||||||
|
- **Anything requiring `INTERNET`**, including a sync stack of our own.
|
||||||
|
- **An app-side event database.**
|
||||||
|
|
||||||
|
## Licence
|
||||||
|
|
||||||
|
Calendula is [MIT](LICENSE). By contributing you agree your changes ship under
|
||||||
|
the same licence.
|
||||||
18
README.md
18
README.md
@@ -8,7 +8,7 @@
|
|||||||
Reads, writes, and reminds — on top of the system calendar, with zero network access.</p>
|
Reads, writes, and reminds — on top of the system calendar, with zero network access.</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a href="https://gitea.jeanlucmakiola.de/makiolaj/calendula/actions"><img src="https://gitea.jeanlucmakiola.de/makiolaj/calendula/actions/workflows/ci.yaml/badge.svg?branch=main" alt="CI"></a>
|
<a href="https://codeberg.org/jlmakiola/calendula/actions"><img src="https://codeberg.org/jlmakiola/calendula/actions/workflows/ci.yaml/badge.svg?branch=main" alt="CI"></a>
|
||||||
<img src="https://img.shields.io/badge/Android-10%2B-3DDC84?logo=android&logoColor=white" alt="Android 10+">
|
<img src="https://img.shields.io/badge/Android-10%2B-3DDC84?logo=android&logoColor=white" alt="Android 10+">
|
||||||
<img src="https://img.shields.io/badge/Kotlin-Compose-7F52FF?logo=kotlin&logoColor=white" alt="Kotlin + Compose">
|
<img src="https://img.shields.io/badge/Kotlin-Compose-7F52FF?logo=kotlin&logoColor=white" alt="Kotlin + Compose">
|
||||||
<img src="https://img.shields.io/badge/Material%203-Expressive-4285F4" alt="Material 3 Expressive">
|
<img src="https://img.shields.io/badge/Material%203-Expressive-4285F4" alt="Material 3 Expressive">
|
||||||
@@ -105,10 +105,26 @@ without reinstalling. Or build from source — see below.
|
|||||||
|
|
||||||
## 📚 Documentation
|
## 📚 Documentation
|
||||||
|
|
||||||
|
- **[Contributing](CONTRIBUTING.md)** — how to report, propose, and patch
|
||||||
- **[Building from source](docs/BUILDING.md)** — requirements and Gradle tasks
|
- **[Building from source](docs/BUILDING.md)** — requirements and Gradle tasks
|
||||||
- **[Architecture](docs/ARCHITECTURE.md)** — the layered design and key pipelines
|
- **[Architecture](docs/ARCHITECTURE.md)** — the layered design and key pipelines
|
||||||
- **[Roadmap](.planning/ROADMAP.md)** — what's shipped and what's next
|
- **[Roadmap](.planning/ROADMAP.md)** — what's shipped and what's next
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
Bug reports, ideas, and patches are all welcome on
|
||||||
|
**[Codeberg](https://codeberg.org/jlmakiola/calendula/issues)**.
|
||||||
|
|
||||||
|
The short version: **start with an issue.** Features get a yes-or-no before they
|
||||||
|
get code, and both features and bugs are assigned a milestone whose
|
||||||
|
`release/vX.Y.Z` branch your pull request then targets. Typo and docs fixes can
|
||||||
|
skip straight to a pull request. Translations don't go through pull requests at
|
||||||
|
all — [Weblate owns them](#-translations).
|
||||||
|
|
||||||
|
Read **[CONTRIBUTING.md](CONTRIBUTING.md)** before writing code: it covers the
|
||||||
|
workflow, the build (note the `floret-kit` submodule), and the architectural
|
||||||
|
rules a change is reviewed against.
|
||||||
|
|
||||||
## 🌍 Translations
|
## 🌍 Translations
|
||||||
|
|
||||||
Calendula ships in German and English, and you're warmly invited to add your
|
Calendula ships in German and English, and you're warmly invited to add your
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ import androidx.compose.material.icons.filled.Keyboard
|
|||||||
import androidx.compose.material.icons.filled.Language
|
import androidx.compose.material.icons.filled.Language
|
||||||
import androidx.compose.material.icons.filled.Notifications
|
import androidx.compose.material.icons.filled.Notifications
|
||||||
import androidx.compose.material.icons.filled.Palette
|
import androidx.compose.material.icons.filled.Palette
|
||||||
|
import androidx.compose.material.icons.filled.PrivacyTip
|
||||||
import androidx.compose.material.icons.filled.Translate
|
import androidx.compose.material.icons.filled.Translate
|
||||||
import androidx.compose.material.icons.filled.Tune
|
import androidx.compose.material.icons.filled.Tune
|
||||||
import androidx.compose.material.icons.filled.UploadFile
|
import androidx.compose.material.icons.filled.UploadFile
|
||||||
@@ -408,7 +409,9 @@ private fun LanguageRow(position: Position) {
|
|||||||
@Composable
|
@Composable
|
||||||
private fun AboutCard() {
|
private fun AboutCard() {
|
||||||
// The card layout lives in floret-kit (components.AboutCard); Calendula
|
// The card layout lives in floret-kit (components.AboutCard); Calendula
|
||||||
// supplies its own logo, author and the source / licence / support links.
|
// supplies its own logo, author and the source / licence / privacy / support
|
||||||
|
// links. The privacy policy has to be reachable from inside the app, not just
|
||||||
|
// from the store listing, because Calendula touches calendar and contact data.
|
||||||
AboutCard(
|
AboutCard(
|
||||||
logo = { AppLogo() },
|
logo = { AppLogo() },
|
||||||
appName = stringResource(R.string.app_name),
|
appName = stringResource(R.string.app_name),
|
||||||
@@ -424,6 +427,11 @@ private fun AboutCard() {
|
|||||||
label = stringResource(R.string.settings_license),
|
label = stringResource(R.string.settings_license),
|
||||||
url = stringResource(R.string.about_license_url),
|
url = stringResource(R.string.about_license_url),
|
||||||
),
|
),
|
||||||
|
AboutLink(
|
||||||
|
icon = Icons.Default.PrivacyTip,
|
||||||
|
label = stringResource(R.string.settings_about_privacy),
|
||||||
|
url = stringResource(R.string.about_privacy_url),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
highlightLink = AboutLink(
|
highlightLink = AboutLink(
|
||||||
icon = Icons.Default.Favorite,
|
icon = Icons.Default.Favorite,
|
||||||
|
|||||||
@@ -53,11 +53,11 @@
|
|||||||
<string name="event_share_failed">تعذّر مشاركة هذا الحدث.</string>
|
<string name="event_share_failed">تعذّر مشاركة هذا الحدث.</string>
|
||||||
<string name="event_delete_title">حذف الحدث؟</string>
|
<string name="event_delete_title">حذف الحدث؟</string>
|
||||||
<string name="event_delete_body">الحدث أُزيل من تقويمك ومن كل جهاز تتم مزامنته معه.</string>
|
<string name="event_delete_body">الحدث أُزيل من تقويمك ومن كل جهاز تتم مزامنته معه.</string>
|
||||||
<string name="event_delete_recurring_title">حذف الحدث المتكرر</string>
|
<string name="event_delete_recurring_title">حذف الحدث المتكرّر</string>
|
||||||
<string name="event_delete_option_occurrence">فقط هذا الحدث</string>
|
<string name="event_delete_option_occurrence">فقط هذا الحدث</string>
|
||||||
<string name="event_delete_option_following">هذا وجميع الأحداث اللاحقة</string>
|
<string name="event_delete_option_following">هذا وجميع الأحداث اللاحقة</string>
|
||||||
<string name="event_delete_option_series">جميع الأحداث في السلسلة</string>
|
<string name="event_delete_option_series">جميع الأحداث في السلسلة</string>
|
||||||
<string name="event_edit_recurring_title">تعديل الحدث المتكرر</string>
|
<string name="event_edit_recurring_title">تعديل الحدث المتكرّر</string>
|
||||||
<string name="event_delete_failed">تعذّر حذف الحدث</string>
|
<string name="event_delete_failed">تعذّر حذف الحدث</string>
|
||||||
<string name="event_delete_write_denied">Calendula يحتاج إلى صلاحية للكتابة لحذف الأحداث</string>
|
<string name="event_delete_write_denied">Calendula يحتاج إلى صلاحية للكتابة لحذف الأحداث</string>
|
||||||
<string name="dialog_cancel">إلغاء</string>
|
<string name="dialog_cancel">إلغاء</string>
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
<string name="event_edit_conflict_discard_hint">الحدث يبقى كما هو الآن</string>
|
<string name="event_edit_conflict_discard_hint">الحدث يبقى كما هو الآن</string>
|
||||||
<string name="import_reminder_prompt_title">هل تريد تطبيق تذكيرك الافتراضي؟</string>
|
<string name="import_reminder_prompt_title">هل تريد تطبيق تذكيرك الافتراضي؟</string>
|
||||||
<string name="event_edit_gone_body">هذا الحدث حُذف في هذه الأثناء، على سبيل المثال على جهاز آخر. لم يعد من الممكن حفظ تغييراتك.</string>
|
<string name="event_edit_gone_body">هذا الحدث حُذف في هذه الأثناء، على سبيل المثال على جهاز آخر. لم يعد من الممكن حفظ تغييراتك.</string>
|
||||||
<string name="event_edit_more_fields">المزيد من الخيارات</string>
|
<string name="event_edit_more_fields">المزيد من الخانات</string>
|
||||||
<string name="event_access_public">علني</string>
|
<string name="event_access_public">علني</string>
|
||||||
<string name="event_access_default">الافتراضي</string>
|
<string name="event_access_default">الافتراضي</string>
|
||||||
<string name="event_availability_busy">مشغول</string>
|
<string name="event_availability_busy">مشغول</string>
|
||||||
@@ -153,7 +153,7 @@
|
|||||||
<string name="recurrence_with_until">%1$s حتى %2$s</string>
|
<string name="recurrence_with_until">%1$s حتى %2$s</string>
|
||||||
<string name="recurrence_with_count">%1$s، %2$d مرّات</string>
|
<string name="recurrence_with_count">%1$s، %2$d مرّات</string>
|
||||||
<string name="import_reminder_prompt_apply">طَبِّق الافتراضي</string>
|
<string name="import_reminder_prompt_apply">طَبِّق الافتراضي</string>
|
||||||
<string name="event_detail_recurring">حدث مُتكرر</string>
|
<string name="event_detail_recurring">حدث متكرّر</string>
|
||||||
<string name="recurrence_on_days">%1$s في %2$s</string>
|
<string name="recurrence_on_days">%1$s في %2$s</string>
|
||||||
<string name="event_attendee_unknown">—</string>
|
<string name="event_attendee_unknown">—</string>
|
||||||
<string name="event_attendee_accepted">تم القُبول</string>
|
<string name="event_attendee_accepted">تم القُبول</string>
|
||||||
@@ -166,7 +166,7 @@
|
|||||||
<string name="event_status_cancelled">ملغي</string>
|
<string name="event_status_cancelled">ملغي</string>
|
||||||
<string name="event_availability_free">متاح</string>
|
<string name="event_availability_free">متاح</string>
|
||||||
<string name="event_access_private">خاص</string>
|
<string name="event_access_private">خاص</string>
|
||||||
<string name="event_access_confidential">سري</string>
|
<string name="event_access_confidential">سرّي</string>
|
||||||
<string name="event_attendee_organizer">المُنظِّم</string>
|
<string name="event_attendee_organizer">المُنظِّم</string>
|
||||||
<string name="event_attendee_optional">اختياري</string>
|
<string name="event_attendee_optional">اختياري</string>
|
||||||
<string name="reminder_default">التذكير الافتراضي</string>
|
<string name="reminder_default">التذكير الافتراضي</string>
|
||||||
@@ -180,7 +180,7 @@
|
|||||||
<string name="reminder_onboarding_body">Android لا يعرض تذكيرات الأحداث من نفسه — بل يجب أن يقوم تطبيق تقويم بذلك. دع Calendula يتولى هذه المهمة.</string>
|
<string name="reminder_onboarding_body">Android لا يعرض تذكيرات الأحداث من نفسه — بل يجب أن يقوم تطبيق تقويم بذلك. دع Calendula يتولى هذه المهمة.</string>
|
||||||
<string name="reminder_benefit_delivery_body">كل تذكير لأحداثك يصِل كإشعار، في الوقت المحدد تمامًا.</string>
|
<string name="reminder_benefit_delivery_body">كل تذكير لأحداثك يصِل كإشعار، في الوقت المحدد تمامًا.</string>
|
||||||
<string name="reminder_benefit_duplicates_title">هل تستخدم تطبيق تقويم ثانٍ؟</string>
|
<string name="reminder_benefit_duplicates_title">هل تستخدم تطبيق تقويم ثانٍ؟</string>
|
||||||
<string name="reminder_benefit_duplicates_body">إذا كان تطبيق آخر أيضًا ينشر تذكيرات، فستراهم مرتين — قم بإيقاف تشغيلهم هناك أو هنا.</string>
|
<string name="reminder_benefit_duplicates_body">إذا تطبيق آخر أيضًا ينشر تذكيرات، فستراهم مرتين — قم بإيقاف تشغيلهم هناك أو هنا.</string>
|
||||||
<string name="reminder_benefit_reversible_title">يمكنك تغييره في أي وقت</string>
|
<string name="reminder_benefit_reversible_title">يمكنك تغييره في أي وقت</string>
|
||||||
<string name="reminder_onboarding_enable_button">شَغِّل التذكيرات</string>
|
<string name="reminder_onboarding_enable_button">شَغِّل التذكيرات</string>
|
||||||
<string name="reminder_onboarding_skip_button">ليس الآن</string>
|
<string name="reminder_onboarding_skip_button">ليس الآن</string>
|
||||||
@@ -200,7 +200,7 @@
|
|||||||
<string name="search_hint">البحث عن الأحداث</string>
|
<string name="search_hint">البحث عن الأحداث</string>
|
||||||
<string name="search_clear">مسح</string>
|
<string name="search_clear">مسح</string>
|
||||||
<string name="agenda_no_more_today">لا مزيد من الأحداث اليوم</string>
|
<string name="agenda_no_more_today">لا مزيد من الأحداث اليوم</string>
|
||||||
<string name="search_empty">لا توجد أحداث مطابقة ”%1$s“.</string>
|
<string name="search_empty">لا أحداث مطابقة ”%1$s“.</string>
|
||||||
<string name="search_idle_hint">ابحث عن أحداثك بالعنوان، الموقع أو الملاحظات.</string>
|
<string name="search_idle_hint">ابحث عن أحداثك بالعنوان، الموقع أو الملاحظات.</string>
|
||||||
<string name="search_back">الرجوع</string>
|
<string name="search_back">الرجوع</string>
|
||||||
<string name="search_action">البحث</string>
|
<string name="search_action">البحث</string>
|
||||||
@@ -217,7 +217,7 @@
|
|||||||
<string name="back">رجوع</string>
|
<string name="back">رجوع</string>
|
||||||
<string name="settings_section_appearance">المظهر</string>
|
<string name="settings_section_appearance">المظهر</string>
|
||||||
<string name="settings_dynamic_color">اللون الديناميكي</string>
|
<string name="settings_dynamic_color">اللون الديناميكي</string>
|
||||||
<string name="settings_dynamic_color_unavailable">يتطلب Android 12 أو أحدث</string>
|
<string name="settings_dynamic_color_unavailable">يتطلب أندرويد ١٢ أو أحدث</string>
|
||||||
<string name="settings_default_view">طريقة العرض الافتراضية</string>
|
<string name="settings_default_view">طريقة العرض الافتراضية</string>
|
||||||
<string name="settings_soften_colors">ألوان تقويم ناعمة</string>
|
<string name="settings_soften_colors">ألوان تقويم ناعمة</string>
|
||||||
<string name="settings_soften_colors_summary">خفف ألوان التقويم والأحداث لتتناسب مع الثيم. قم بإيقافه لإظهار الألوان الخام من مصدر التقويم.</string>
|
<string name="settings_soften_colors_summary">خفف ألوان التقويم والأحداث لتتناسب مع الثيم. قم بإيقافه لإظهار الألوان الخام من مصدر التقويم.</string>
|
||||||
@@ -227,7 +227,7 @@
|
|||||||
<string name="settings_font_choose_file">اختر ملفًا…</string>
|
<string name="settings_font_choose_file">اختر ملفًا…</string>
|
||||||
<string name="settings_font_custom_selected">خط مخصص</string>
|
<string name="settings_font_custom_selected">خط مخصص</string>
|
||||||
<string name="settings_font_import_failed">تعذّر قراءة هذا الملف كخط</string>
|
<string name="settings_font_import_failed">تعذّر قراءة هذا الملف كخط</string>
|
||||||
<string name="settings_week_start">يبدأ الأسبوع في</string>
|
<string name="settings_week_start">الأسبوع يبدأ في</string>
|
||||||
<string name="settings_week_start_auto">تلقائي</string>
|
<string name="settings_week_start_auto">تلقائي</string>
|
||||||
<string name="settings_time_format_auto">تلقائي</string>
|
<string name="settings_time_format_auto">تلقائي</string>
|
||||||
<string name="settings_time_format">تنسيق الوقت</string>
|
<string name="settings_time_format">تنسيق الوقت</string>
|
||||||
@@ -243,4 +243,58 @@
|
|||||||
<string name="settings_past_events_show">إظهار</string>
|
<string name="settings_past_events_show">إظهار</string>
|
||||||
<string name="settings_past_events_hide">إخفاء</string>
|
<string name="settings_past_events_hide">إخفاء</string>
|
||||||
<string name="settings_agenda_header">جدول</string>
|
<string name="settings_agenda_header">جدول</string>
|
||||||
|
<string name="event_edit_timezone_device">المنطقة الزمنية للجهاز</string>
|
||||||
|
<string name="event_edit_timezone_search">البحث عن المناطق الزمنية</string>
|
||||||
|
<string name="event_edit_timezone_all">جميع المناطق الزمنية</string>
|
||||||
|
<string name="event_edit_timezone_none">لا منطقة زمنية تطابق \"%1$s\"</string>
|
||||||
|
<string name="event_edit_timezone_local_time">%1$s توقيتك</string>
|
||||||
|
<string name="event_edit_timezone_recent">الأحدث</string>
|
||||||
|
<string name="event_edit_recurrence_incomplete">أدخل رقمًا من ١ إلى ٩٩٩</string>
|
||||||
|
<plurals name="duration_minutes">
|
||||||
|
<item quantity="zero">(%d) لا دقائق</item>
|
||||||
|
<item quantity="one">(%d) دقيقة واحده</item>
|
||||||
|
<item quantity="two">%d دقيقتان</item>
|
||||||
|
<item quantity="few">%d دقائق</item>
|
||||||
|
<item quantity="many">%d دقيقة</item>
|
||||||
|
<item quantity="other">%d دقيقة</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="duration_hours">
|
||||||
|
<item quantity="zero">(%d) لا ساعات</item>
|
||||||
|
<item quantity="one">(%d) ساعة واحده</item>
|
||||||
|
<item quantity="two">%d ساعتان</item>
|
||||||
|
<item quantity="few">%d ساعات</item>
|
||||||
|
<item quantity="many">%d ساعة</item>
|
||||||
|
<item quantity="other">%d ساعة</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="duration_days">
|
||||||
|
<item quantity="zero">(%d) لا أيام</item>
|
||||||
|
<item quantity="one">(%d) يوم واحد</item>
|
||||||
|
<item quantity="two">%d يومان</item>
|
||||||
|
<item quantity="few">%d أيام</item>
|
||||||
|
<item quantity="many">%d يوم</item>
|
||||||
|
<item quantity="other">%d يوم</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="duration_weeks">
|
||||||
|
<item quantity="zero">(%d) لا أسابيع</item>
|
||||||
|
<item quantity="one">(%d) أسبوع واحد</item>
|
||||||
|
<item quantity="two">%d أسبوعان</item>
|
||||||
|
<item quantity="few">%d أسابيع</item>
|
||||||
|
<item quantity="many">%d أسبوع</item>
|
||||||
|
<item quantity="other">%d أسبوع</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="agenda_span_starts">يبدأ %1$s</string>
|
||||||
|
<string name="agenda_span_ends">ينتهي %1$s</string>
|
||||||
|
<string name="today_jump_action">الانتقال إلى اليوم</string>
|
||||||
|
<string name="widget_refresh">تحديث</string>
|
||||||
|
<string name="settings_app_name">اسم التطبيق</string>
|
||||||
|
<string name="settings_dim_completed">تعتيم الأحداث المكتملة</string>
|
||||||
|
<string name="settings_dim_completed_summary">تعتيم الأحداث التي انتهت بالفعل في عرض الشهر والأسبوع</string>
|
||||||
|
<string name="settings_today_toolbar">زر اليوم في شريط الأدوات</string>
|
||||||
|
<string name="settings_today_toolbar_summary">إظهار زر الانتقال إلى اليوم في شريط الأدوات بدلاً من زر عائم</string>
|
||||||
|
<string name="settings_app_name_summary">اعرض Calendula كـ \"Calendar\" في مشغّل التطبيقات الخاص بك. فقط الاسم في المشغّل يتغير؛ وقد ينتقل الرمز إلى مكان جديد بعد التبديل.</string>
|
||||||
|
<string name="settings_past_events_dim">تعتيم</string>
|
||||||
|
<string name="settings_past_events">الأحداث السابقة</string>
|
||||||
|
<string name="settings_agenda_range">مدى الجدول</string>
|
||||||
|
<string name="agenda_range_custom">مخصص…</string>
|
||||||
|
<string name="agenda_range_custom_hint">أيام</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -354,7 +354,7 @@
|
|||||||
<string name="settings_translate">Pomóż w tłumaczeniu</string>
|
<string name="settings_translate">Pomóż w tłumaczeniu</string>
|
||||||
<string name="settings_translate_hint">Dodaj lub ulepsz tłumaczenie w Weblate</string>
|
<string name="settings_translate_hint">Dodaj lub ulepsz tłumaczenie w Weblate</string>
|
||||||
<string name="settings_appearance_subtitle">Motyw, domyślny widok, pierwszy dzień tygodnia</string>
|
<string name="settings_appearance_subtitle">Motyw, domyślny widok, pierwszy dzień tygodnia</string>
|
||||||
<string name="settings_views_subtitle">Kolejność przycisku szybkiego przełączania oraz menu</string>
|
<string name="settings_views_subtitle">Układ widoku miesiąca, przycisk szybkiego przełączania, kolejność menu</string>
|
||||||
<string name="settings_event_form_subtitle">Domyślne pola dla nowych wydarzeń</string>
|
<string name="settings_event_form_subtitle">Domyślne pola dla nowych wydarzeń</string>
|
||||||
<string name="settings_notifications_subtitle">Przypomnienia o wydarzeniach</string>
|
<string name="settings_notifications_subtitle">Przypomnienia o wydarzeniach</string>
|
||||||
<string name="settings_special_dates_subtitle">Urodziny i rocznice kontaktów</string>
|
<string name="settings_special_dates_subtitle">Urodziny i rocznice kontaktów</string>
|
||||||
@@ -517,4 +517,32 @@
|
|||||||
<string name="settings_agenda_show_today_hint">Zachowaj dzisiejszy dzień na górze agendy i widżetu, nawet gdy nie ma już na dziś żadnych zadań.</string>
|
<string name="settings_agenda_show_today_hint">Zachowaj dzisiejszy dzień na górze agendy i widżetu, nawet gdy nie ma już na dziś żadnych zadań.</string>
|
||||||
<string name="special_dates_calendar_birthday">Urodziny</string>
|
<string name="special_dates_calendar_birthday">Urodziny</string>
|
||||||
<string name="special_dates_calendar_anniversary">Rocznice</string>
|
<string name="special_dates_calendar_anniversary">Rocznice</string>
|
||||||
|
<string name="event_edit_timezone_device">Strefa czasowa urządzenia</string>
|
||||||
|
<string name="event_edit_timezone_device_summary">Podąża za Twoją lokalizacją</string>
|
||||||
|
<string name="event_edit_timezone_search">Znajdź strefę czasową</string>
|
||||||
|
<string name="event_edit_timezone_recent">Ostatnie</string>
|
||||||
|
<string name="event_edit_timezone_all">Wszystkie strefy czasowe</string>
|
||||||
|
<string name="event_edit_timezone_none">Żadna strefa czasowa nie pasuje do „%1$s”</string>
|
||||||
|
<string name="event_edit_timezone_local_time">%1$s Twojego czasu</string>
|
||||||
|
<string name="event_edit_recurrence_incomplete">Wpisz liczbę od 1 do 999</string>
|
||||||
|
<string name="agenda_span_starts">Początek o %1$s</string>
|
||||||
|
<string name="agenda_span_ends">Koniec o %1$s</string>
|
||||||
|
<string name="today_jump_action">Dzisiaj</string>
|
||||||
|
<string name="settings_today_toolbar">Przycisk „Dzisiaj” na pasku narzędzi</string>
|
||||||
|
<string name="settings_today_toolbar_summary">Pokaż przycisk skoku do dzisiaj na pasku narzędzi zamiast przycisku pływającego</string>
|
||||||
|
<string name="settings_app_name">Nazwa aplikacji</string>
|
||||||
|
<string name="settings_app_name_summary">Wyświetlaj Calendula jako „Kalendarz” w menu aplikacji. Zmieni się tylko nazwa w menu; po przełączeniu ikona może pojawić się w innym miejscu.</string>
|
||||||
|
<string name="settings_month_header">Widok miesiąca</string>
|
||||||
|
<string name="settings_month_view_style">Styl widoku miesiąca</string>
|
||||||
|
<string name="month_style_paged">Strony</string>
|
||||||
|
<string name="month_style_paged_summary">Jeden miesiąc wypełnia ekran. Przesuń w lewo lub w prawo, aby zmienić miesiąc.</string>
|
||||||
|
<string name="month_style_continuous">Przewijanie miesięcy</string>
|
||||||
|
<string name="month_style_continuous_summary">Każdy miesiąc znajduje się pod własnym nagłówkiem, oddzielony od następnego niewielkim odstępem.</string>
|
||||||
|
<string name="month_style_dense">Tygodnie bez przerw</string>
|
||||||
|
<string name="month_style_dense_summary">Tygodnie są ułożone jeden po drugim, każdy miesiąc płynnie łączy się z następnym, bez odstępu pomiędzy.</string>
|
||||||
|
<string name="month_style_split">Podzielony</string>
|
||||||
|
<string name="month_style_split_summary">Zwięzła siatka wyróżnia dni z wydarzeniami a lista dla dnia wybranego dotknięciem wyświetla się poniżej.</string>
|
||||||
|
<string name="month_split_no_events">Brak planów</string>
|
||||||
|
<string name="month_split_expand">Pokaż cały miesiąc</string>
|
||||||
|
<string name="month_split_collapse">Pokaż wydarzenia dnia</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -462,6 +462,7 @@
|
|||||||
<string name="settings_license_value">MIT</string>
|
<string name="settings_license_value">MIT</string>
|
||||||
<string name="settings_about_author">by Jean-Luc Makiola</string>
|
<string name="settings_about_author">by Jean-Luc Makiola</string>
|
||||||
<string name="settings_about_source">Source</string>
|
<string name="settings_about_source">Source</string>
|
||||||
|
<string name="settings_about_privacy">Privacy policy</string>
|
||||||
<string name="settings_about_support">Support development</string>
|
<string name="settings_about_support">Support development</string>
|
||||||
<string name="settings_about_version">Version %1$s</string>
|
<string name="settings_about_version">Version %1$s</string>
|
||||||
<string name="settings_about_logo_desc">Calendula app icon</string>
|
<string name="settings_about_logo_desc">Calendula app icon</string>
|
||||||
@@ -570,8 +571,9 @@
|
|||||||
<string name="settings_qs_tile">Add Quick Settings tile</string>
|
<string name="settings_qs_tile">Add Quick Settings tile</string>
|
||||||
<string name="settings_qs_tile_hint">Add a “New event” tile to the Quick Settings panel.</string>
|
<string name="settings_qs_tile_hint">Add a “New event” tile to the Quick Settings panel.</string>
|
||||||
|
|
||||||
<string name="about_source_url" translatable="false">https://gitea.jeanlucmakiola.de/makiolaj/calendula</string>
|
<string name="about_source_url" translatable="false">https://codeberg.org/jlmakiola/calendula</string>
|
||||||
<string name="about_license_url" translatable="false">https://gitea.jeanlucmakiola.de/makiolaj/calendula/src/branch/main/LICENSE</string>
|
<string name="about_license_url" translatable="false">https://codeberg.org/jlmakiola/calendula/src/branch/main/LICENSE</string>
|
||||||
|
<string name="about_privacy_url" translatable="false">https://jeanlucmakiola.de/calendula/privacy</string>
|
||||||
<string name="about_support_url" translatable="false">https://ko-fi.com/jeanlucmakiola</string>
|
<string name="about_support_url" translatable="false">https://ko-fi.com/jeanlucmakiola</string>
|
||||||
<string name="about_translate_url" translatable="false">https://weblate.dev.jeanlucmakiola.de/engage/calendula/</string>
|
<string name="about_translate_url" translatable="false">https://weblate.dev.jeanlucmakiola.de/engage/calendula/</string>
|
||||||
|
|
||||||
|
|||||||
@@ -62,9 +62,15 @@ flowchart TD
|
|||||||
- **`data/prefs/`** — DataStore-backed settings (theme, week start, form
|
- **`data/prefs/`** — DataStore-backed settings (theme, week start, form
|
||||||
field defaults, reminders toggle) and small state (last-used calendar).
|
field defaults, reminders toggle) and small state (last-used calendar).
|
||||||
- **`ui/`** — one package per screen, each with Screen + ViewModel +
|
- **`ui/`** — one package per screen, each with Screen + ViewModel +
|
||||||
UiState. Shared pieces in `ui/common/` (OptionCard — the app's only
|
UiState. Shared pieces in `ui/common/` (recurrence humanizer, FAB column,
|
||||||
sanctioned selection-dialog style —, recurrence humanizer, FAB column,
|
drawer, transitions). Selection pickers are full-screen and come from
|
||||||
drawer, transitions).
|
floret-kit (`FullScreenPicker` / `OptionPicker`); `AlertDialog` is reserved
|
||||||
|
for plain confirmations and the compact recurring-scope choosers.
|
||||||
|
- **`floret-kit/`** — the shared Material 3 Expressive kit for the Floret app
|
||||||
|
family, wired in as a git submodule *and* a Gradle composite build
|
||||||
|
(`includeBuild`), so it is compiled from source rather than resolved as a
|
||||||
|
dependency. Pickers, crash plumbing, and locale/time helpers live there;
|
||||||
|
changing them is a pull request against that repository plus a submodule bump.
|
||||||
|
|
||||||
## Navigation
|
## Navigation
|
||||||
|
|
||||||
@@ -81,7 +87,7 @@ exactly like an event tap.
|
|||||||
## Recurring writes
|
## Recurring writes
|
||||||
|
|
||||||
The provider's invariants drive the design (learned the hard way, verified
|
The provider's invariants drive the design (learned the hard way, verified
|
||||||
on-device — see plan 03):
|
on-device):
|
||||||
|
|
||||||
- Recurring rows carry `RRULE` + `DURATION` (no `DTEND`); one-off rows
|
- Recurring rows carry `RRULE` + `DURATION` (no `DTEND`); one-off rows
|
||||||
carry `DTEND`.
|
carry `DTEND`.
|
||||||
@@ -167,6 +173,7 @@ JUnit 5 + Truth + Turbine on the JVM. The seams that make it work:
|
|||||||
`CalendarDataSource` is faked (`FakeCalendarDataSource` records writes),
|
`CalendarDataSource` is faked (`FakeCalendarDataSource` records writes),
|
||||||
mappers parse `ColumnReader`/plain maps instead of cursors, domain logic
|
mappers parse `ColumnReader`/plain maps instead of cursors, domain logic
|
||||||
(recurrence, validation, snapshots, write-value building) is pure. CI
|
(recurrence, validation, snapshots, write-value building) is pure. CI
|
||||||
(Gitea Actions) runs `lint test assembleDebug` once per pull request; merging a
|
(Forgejo Actions on Codeberg) runs `lint test assembleDebug` once per pull
|
||||||
|
request; merging a
|
||||||
bumped `versionName` to `main` builds, signs, and publishes to the self-hosted
|
bumped `versionName` to `main` builds, signs, and publishes to the self-hosted
|
||||||
F-Droid repo and then mints the `vX.Y.Z` tag + release. See docs/RELEASING.md.
|
F-Droid repo and then mints the `vX.Y.Z` tag + release. See docs/RELEASING.md.
|
||||||
|
|||||||
@@ -1,12 +1,35 @@
|
|||||||
# Building from source
|
# Building from source
|
||||||
|
|
||||||
Calendula builds with the standard Android Gradle toolchain — no extra setup
|
Calendula builds with the standard Android Gradle toolchain — no extra setup
|
||||||
beyond the SDK and a JDK.
|
beyond the SDK, a JDK, and the submodule.
|
||||||
|
|
||||||
|
## Clone
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone --recurse-submodules https://codeberg.org/jlmakiola/calendula.git
|
||||||
|
```
|
||||||
|
|
||||||
|
Calendula depends on **[floret-kit](https://codeberg.org/jlmakiola/floret-kit)**,
|
||||||
|
the shared Material 3 Expressive kit, as a git submodule wired in as a Gradle
|
||||||
|
composite build (`includeBuild("floret-kit")` in `settings.gradle.kts`) — it is
|
||||||
|
compiled from source, not resolved from a repository. A clone without the
|
||||||
|
submodule fails to configure. For an existing clone:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git submodule update --init --recursive
|
||||||
|
```
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- **Android SDK 36+**
|
- **JDK 17** — not newer; the Android Gradle Plugin requires exactly 17. If your
|
||||||
- **JDK 17** — if your default JDK is not 17, set `JAVA_HOME` explicitly.
|
default JDK differs, set `JAVA_HOME` explicitly.
|
||||||
|
- **Android SDK** — platform **37** (`compileSdk`) and **build-tools 36.0.0**.
|
||||||
|
`minSdk` is 29, `targetSdk` 36.
|
||||||
|
|
||||||
|
The SDK is located via `ANDROID_HOME` (or `ANDROID_SDK_ROOT`), or via a
|
||||||
|
gitignored `local.properties` with `sdk.dir`. If you use `local.properties`, note
|
||||||
|
that the composite build needs **its own** copy at `floret-kit/local.properties`;
|
||||||
|
setting `ANDROID_HOME` covers both builds at once and is the simpler route.
|
||||||
|
|
||||||
The Gradle wrapper is checked in, so you don't need a system Gradle.
|
The Gradle wrapper is checked in, so you don't need a system Gradle.
|
||||||
|
|
||||||
@@ -14,11 +37,44 @@ The Gradle wrapper is checked in, so you don't need a system Gradle.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
./gradlew assembleDebug # debug APK → app/build/outputs/apk/debug/
|
./gradlew assembleDebug # debug APK → app/build/outputs/apk/debug/
|
||||||
./gradlew test # JVM unit tests
|
./gradlew test # JVM unit tests (JUnit 5 + Truth + Turbine)
|
||||||
./gradlew lint # Android lint
|
./gradlew lint # Android lint
|
||||||
```
|
```
|
||||||
|
|
||||||
|
A single test class, or a pattern:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew testDebugUnitTest --tests "de.jeanlucmakiola.calendula.domain.SimpleRecurrenceTest"
|
||||||
|
./gradlew testDebugUnitTest --tests "*SimpleRecurrence*"
|
||||||
|
```
|
||||||
|
|
||||||
|
Translation-key invariants (stale and orphaned keys are fatal; missing ones are
|
||||||
|
not) are checked outside Gradle:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 scripts/check_translations.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## What CI runs
|
||||||
|
|
||||||
|
`.forgejo/workflows/ci.yaml` reports a single `CI` check per pull request on
|
||||||
|
Codeberg: the reproducible-release guard, then `lintDebug`,
|
||||||
|
`testDebugUnitTest`, `assembleDebug` and a Trivy filesystem scan. Pull requests
|
||||||
|
that touch only docs, F-Droid metadata or the licence skip the Android build and
|
||||||
|
still report green.
|
||||||
|
|
||||||
## Release builds
|
## Release builds
|
||||||
|
|
||||||
Release signing and the publishing pipeline are documented separately in
|
Release signing and the publishing pipeline are documented separately in
|
||||||
[RELEASING.md](RELEASING.md).
|
[RELEASING.md](RELEASING.md). To smoke-test a release candidate locally, the
|
||||||
|
`releaseTest` build type is an R8-shrunk twin of `release`, debug-signed with its
|
||||||
|
own `applicationId` suffix so it installs alongside the real app:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scripts/verify-release.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
See [`../CONTRIBUTING.md`](../CONTRIBUTING.md) for the issue-first workflow,
|
||||||
|
which branch to target, and the architectural rules a change is reviewed against.
|
||||||
|
|||||||
@@ -4,19 +4,20 @@ Where to look for what:
|
|||||||
|
|
||||||
| Document | What it is |
|
| Document | What it is |
|
||||||
|---|---|
|
|---|---|
|
||||||
|
| [`../CONTRIBUTING.md`](../CONTRIBUTING.md) | How to contribute: issue-first workflow, which branch to target, translations, the rules a change is reviewed against |
|
||||||
|
| [`BUILDING.md`](BUILDING.md) | Building from source: submodule, JDK/SDK requirements, Gradle tasks, what CI runs |
|
||||||
| [`ARCHITECTURE.md`](ARCHITECTURE.md) | Orientation tour: principles, layers, navigation, recurring-write / conflict / reminder pipelines, testing |
|
| [`ARCHITECTURE.md`](ARCHITECTURE.md) | Orientation tour: principles, layers, navigation, recurring-write / conflict / reminder pipelines, testing |
|
||||||
|
| [`RELEASING.md`](RELEASING.md) | Release process: versioning, the merge-driven pipeline, the two-forge split, secrets, key custody |
|
||||||
| [`../CHANGELOG.md`](../CHANGELOG.md) | Release history (Keep a Changelog, SemVer) |
|
| [`../CHANGELOG.md`](../CHANGELOG.md) | Release history (Keep a Changelog, SemVer) |
|
||||||
| [`../.planning/ROADMAP.md`](../.planning/ROADMAP.md) | Living roadmap: shipped milestones, current scope, idea backlog |
|
| [`../.planning/ROADMAP.md`](../.planning/ROADMAP.md) | Living roadmap: shipped milestones, current scope, idea backlog |
|
||||||
| [`../.planning/PROJECT.md`](../.planning/PROJECT.md) | What the project is, stack, naming, infrastructure |
|
| [`../.planning/PROJECT.md`](../.planning/PROJECT.md) | What the project is, stack, naming, infrastructure |
|
||||||
| [`../.planning/REQUIREMENTS.md`](../.planning/REQUIREMENTS.md) | Requirement checklist per milestone |
|
| [`../.planning/REQUIREMENTS.md`](../.planning/REQUIREMENTS.md) | Requirement checklist per milestone |
|
||||||
| [`../.planning/STATE.md`](../.planning/STATE.md) | Snapshot of where development currently stands |
|
| [`../.planning/STATE.md`](../.planning/STATE.md) | Snapshot of where development currently stands |
|
||||||
| [`superpowers/specs/`](superpowers/specs/) | The original design spec (2026-06-08) — historical record, not updated |
|
| [`design/`](design/) | Per-feature design notes kept for features whose provider behaviour is worth recording |
|
||||||
| [`superpowers/plans/`](superpowers/plans/) | Per-milestone implementation plans with task checklists — historical record of how each slice was built, including provider lessons learned |
|
|
||||||
| [`../fastlane/metadata/android/`](../fastlane/metadata/android/) | Store metadata (single source of truth): descriptions, title, icon, screenshots (DE + EN). Harvested directly by the official F-Droid repo; transformed into the self-hosted repo layout at release time by [`../scripts/fastlane_to_fdroid_localized.sh`](../scripts/fastlane_to_fdroid_localized.sh) |
|
| [`../fastlane/metadata/android/`](../fastlane/metadata/android/) | Store metadata (single source of truth): descriptions, title, icon, screenshots (DE + EN). Harvested directly by the official F-Droid repo; transformed into the self-hosted repo layout at release time by [`../scripts/fastlane_to_fdroid_localized.sh`](../scripts/fastlane_to_fdroid_localized.sh) |
|
||||||
| [`../fdroid-metadata/`](../fdroid-metadata/) | App-level F-Droid control file (`*.yml`: Categories, License, links) for the self-hosted repo's `fdroid update` |
|
| [`../fdroid-metadata/`](../fdroid-metadata/) | App-level F-Droid control file (`*.yml`: Categories, License, links) for the self-hosted repo's `fdroid update` |
|
||||||
| [`fdroid-official/`](fdroid-official/) | Draft recipe + notes for publishing to the **official** F-Droid repo (reproducible build + developer-signed binary) |
|
| [`fdroid-official/`](fdroid-official/) | Recipe + notes for publishing to the **official** F-Droid repo (reproducible build + developer-signed binary) |
|
||||||
|
|
||||||
Conventions: plans and specs under `superpowers/` are point-in-time
|
Conventions: the `.planning/` files are living documents and should stay current.
|
||||||
artifacts of the agentic workflow that built each milestone — they get
|
`ARCHITECTURE.md` is the authoritative orientation tour — it is updated with the
|
||||||
status updates but are never rewritten. The `.planning/` files are living
|
code, and is the right place for a lesson learned about the calendar provider.
|
||||||
documents and should stay current.
|
|
||||||
|
|||||||
@@ -73,7 +73,8 @@ Published version codes so far: `v0.1.0`→100 … `v1.0.0`→10000 … `v2.0.0`
|
|||||||
CI and release are split so a change is built once on its PR and only does
|
CI and release are split so a change is built once on its PR and only does
|
||||||
release work when a merge actually cuts a release:
|
release work when a merge actually cuts a release:
|
||||||
|
|
||||||
- **`ci.yaml`** (on `pull_request`) — lint + unit tests + a debug assemble (and
|
- **`ci.yaml`** (`.forgejo/workflows/`, on `pull_request`, **Codeberg**) — lint +
|
||||||
|
unit tests + a debug assemble (and
|
||||||
a Trivy scan), once per PR. Docs/metadata-only PRs skip the Android build but
|
a Trivy scan), once per PR. Docs/metadata-only PRs skip the Android build but
|
||||||
still report a green `CI` check.
|
still report a green `CI` check.
|
||||||
- **`release.yaml`** (on push to `main`, plus `workflow_dispatch`) — a cheap
|
- **`release.yaml`** (on push to `main`, plus `workflow_dispatch`) — a cheap
|
||||||
@@ -91,9 +92,9 @@ release work when a merge actually cuts a release:
|
|||||||
|
|
||||||
Alongside F-Droid, each release is mirrored to the Codeberg repo
|
Alongside F-Droid, each release is mirrored to the Codeberg repo
|
||||||
(`jlmakiola/calendula`) as a plain download for users who don't want F-Droid.
|
(`jlmakiola/calendula`) as a plain download for users who don't want F-Droid.
|
||||||
Gitea already **push-mirrors** branches and tags to Codeberg, but releases
|
Codeberg **push-mirrors** branches and tags to Gitea, but releases aren't git
|
||||||
aren't git objects and don't sync, so the pipeline creates the release over the
|
objects and don't sync in either direction, so the pipeline creates the release
|
||||||
Codeberg API and attaches `calendula_v<version>.apk` + its `.sha256`. It's the
|
over the Codeberg API and attaches `calendula_v<version>.apk` + its `.sha256`. It's the
|
||||||
same APK the F-Droid repo serves (same **app key**), so it adds no trust
|
same APK the F-Droid repo serves (same **app key**), so it adds no trust
|
||||||
surface. The step is best-effort: a Codeberg outage never fails an
|
surface. The step is best-effort: a Codeberg outage never fails an
|
||||||
already-published F-Droid release, and it skips cleanly if `CODEBERG_RELEASE_TOKEN` is
|
already-published F-Droid release, and it skips cleanly if `CODEBERG_RELEASE_TOKEN` is
|
||||||
@@ -109,6 +110,39 @@ build, the version bump, and tag/release creation, and just re-signs the
|
|||||||
existing F-Droid index with the configured repo key and re-uploads. Use this
|
existing F-Droid index with the configured repo key and re-uploads. Use this
|
||||||
for key rotation or repo recovery without publishing a new app version.
|
for key rotation or repo recovery without publishing a new app version.
|
||||||
|
|
||||||
|
## Two forges, one repo
|
||||||
|
|
||||||
|
**Codeberg (`jlmakiola/calendula`) is canonical** — git, issues, PRs, tags and
|
||||||
|
releases. The self-hosted Gitea instance is build infrastructure: it holds the
|
||||||
|
signing key, publishes the F-Droid repo, and runs the release pipeline. Codeberg
|
||||||
|
push-mirrors `main` and tags to Gitea, and a bumped `versionName` arriving there
|
||||||
|
triggers `release.yaml` exactly as before.
|
||||||
|
|
||||||
|
Workflows are separated by **directory**, not by conditionals. Forgejo looks in
|
||||||
|
`.forgejo/workflows` → `.gitea/workflows` → `.github/workflows` and stops at the
|
||||||
|
first that exists; Gitea doesn't know `.forgejo/` at all:
|
||||||
|
|
||||||
|
| Directory | Runs on | Contains | Secrets |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `.forgejo/workflows/` | Codeberg | `ci.yaml`, `translations.yaml` | **none** |
|
||||||
|
| `.gitea/workflows/` | Gitea | `release.yaml`, `renovate.yml` | signing key, F-Droid, Play, bot tokens |
|
||||||
|
|
||||||
|
The line is drawn at **secrets, not at CI-vs-release**. That's what makes fork
|
||||||
|
PRs safe: everything a contributor can trigger lives in `.forgejo/` and can
|
||||||
|
reference no secret. Renovate stays on the Gitea runner *even though it opens
|
||||||
|
PRs on Codeberg* — it talks to Codeberg's API rather than moving its token onto
|
||||||
|
the contributor-facing runner.
|
||||||
|
|
||||||
|
Two consequences worth remembering:
|
||||||
|
|
||||||
|
- **`detect` reads tags from Codeberg**, not from the Gitea instance it runs on.
|
||||||
|
Push mirroring is `git push --mirror`, so a tag minted on Gitea is deleted by
|
||||||
|
the next sync until the Codeberg tag push propagates back. Asking Gitea inside
|
||||||
|
that window would re-cut a shipped release.
|
||||||
|
- **Any ref that exists only on Gitea gets deleted** by the mirror. That's
|
||||||
|
correct under Codeberg-canonical, but don't debug a "vanished" branch without
|
||||||
|
remembering it.
|
||||||
|
|
||||||
## Secrets (Gitea → repo Settings → Actions → Secrets)
|
## Secrets (Gitea → repo Settings → Actions → Secrets)
|
||||||
|
|
||||||
| Secret | Purpose |
|
| Secret | Purpose |
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ Categories:
|
|||||||
- Calendar & Agenda
|
- Calendar & Agenda
|
||||||
License: MIT
|
License: MIT
|
||||||
AuthorName: Jean-Luc Makiola
|
AuthorName: Jean-Luc Makiola
|
||||||
SourceCode: https://gitea.jeanlucmakiola.de/makiolaj/calendula
|
SourceCode: https://codeberg.org/jlmakiola/calendula
|
||||||
IssueTracker: https://gitea.jeanlucmakiola.de/makiolaj/calendula/issues
|
IssueTracker: https://codeberg.org/jlmakiola/calendula/issues
|
||||||
Changelog: https://gitea.jeanlucmakiola.de/makiolaj/calendula/src/branch/main/CHANGELOG.md
|
Changelog: https://codeberg.org/jlmakiola/calendula/src/branch/main/CHANGELOG.md
|
||||||
Donate: https://ko-fi.com/jeanlucmakiola
|
Donate: https://ko-fi.com/jeanlucmakiola
|
||||||
|
|
||||||
AutoName: Calendula
|
AutoName: Calendula
|
||||||
|
|||||||
@@ -6,6 +6,6 @@ Summary: A modern Material 3 Expressive calendar for Android.
|
|||||||
Categories:
|
Categories:
|
||||||
- Time
|
- Time
|
||||||
|
|
||||||
SourceCode: https://gitea.jeanlucmakiola.de/makiolaj/calendula
|
SourceCode: https://codeberg.org/jlmakiola/calendula
|
||||||
IssueTracker: https://gitea.jeanlucmakiola.de/makiolaj/calendula/issues
|
IssueTracker: https://codeberg.org/jlmakiola/calendula/issues
|
||||||
Donate: https://ko-fi.com/jeanlucmakiola
|
Donate: https://ko-fi.com/jeanlucmakiola
|
||||||
|
|||||||
@@ -7,6 +7,23 @@
|
|||||||
":semanticCommits",
|
":semanticCommits",
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// `config:recommended` brings in mergeConfidence:age-confidence-badges, whose
|
||||||
|
// Age column is a Mend badge. Mend's Merge Confidence index only covers Maven
|
||||||
|
// Central: org.jetbrains.kotlin, junit, truth, turbine et al resolve, but
|
||||||
|
// every androidx/compose artifact lives on Google's Maven repo and comes back
|
||||||
|
// as a grey UNKNOWN — i.e. most of this project. Renovate already knows the
|
||||||
|
// real answer, since it derives release timestamps itself for the
|
||||||
|
// minimumReleaseAge rules below (Google Maven serves `last-modified` on its
|
||||||
|
// POMs), so take the age from there and leave Mend to the Confidence column,
|
||||||
|
// which still carries signal for the Maven Central half.
|
||||||
|
prBodyDefinitions: {
|
||||||
|
Age: "{{#if releaseTimestamp}}{{{newVersionAgeInDays}}} d{{else}}unknown{{/if}}",
|
||||||
|
},
|
||||||
|
// Default heading links to the Merge Confidence docs; this column is ours now.
|
||||||
|
prBodyHeadingDefinitions: {
|
||||||
|
Age: "Age",
|
||||||
|
},
|
||||||
|
|
||||||
// No automerge: a dependency bump goes through the same review (and, for
|
// No automerge: a dependency bump goes through the same review (and, for
|
||||||
// anything touching the build, the same on-device check) as a feature
|
// anything touching the build, the same on-device check) as a feature
|
||||||
// before it can ride a release — see docs/RELEASING.md and the
|
// before it can ride a release — see docs/RELEASING.md and the
|
||||||
@@ -15,6 +32,22 @@
|
|||||||
|
|
||||||
// One reviewable surface; the dashboard issue lists everything pending.
|
// One reviewable surface; the dashboard issue lists everything pending.
|
||||||
dependencyDashboard: true,
|
dependencyDashboard: true,
|
||||||
|
|
||||||
|
// The cooling-off periods below are advisory, not a gate. "flexible" still
|
||||||
|
// prefers a version that has cleared its window, but when every candidate is
|
||||||
|
// too young it opens the PR at the newest one anyway, so merging early stays
|
||||||
|
// a judgement call. (The default, "strict", would suppress the PR entirely
|
||||||
|
// until a release aged in.) A still-young branch carries a yellow
|
||||||
|
// `renovate/stability-days` check so it's visible which side of the line
|
||||||
|
// it's on; with automerge off, nothing acts on that check by itself.
|
||||||
|
//
|
||||||
|
// NOT "none": that short-circuits the candidate loop in filter-checks.ts, and
|
||||||
|
// that loop is what calls postprocessRelease — the only thing that fetches a
|
||||||
|
// Maven artifact's Last-Modified header. Skipping it leaves releaseTimestamp
|
||||||
|
// unset, which empties the Age column and quietly makes minimumReleaseAge and
|
||||||
|
// the stability check no-ops, since both need that timestamp to compare.
|
||||||
|
internalChecksFilter: "flexible",
|
||||||
|
|
||||||
labels: ["dependencies"],
|
labels: ["dependencies"],
|
||||||
prConcurrentLimit: 5,
|
prConcurrentLimit: 5,
|
||||||
prHourlyLimit: 0,
|
prHourlyLimit: 0,
|
||||||
@@ -25,11 +58,28 @@
|
|||||||
|
|
||||||
// Gitea Actions workflows live under .gitea/workflows, not .github — extend
|
// Gitea Actions workflows live under .gitea/workflows, not .github — extend
|
||||||
// the github-actions manager (same syntax) to watch them too.
|
// the github-actions manager (same syntax) to watch them too.
|
||||||
|
// `fileMatch` is deprecated; the replacement takes the regex delimited, and
|
||||||
|
// Renovate's config migration was already rewriting this on every run.
|
||||||
"github-actions": {
|
"github-actions": {
|
||||||
fileMatch: ["^\\.gitea/workflows/[^/]+\\.ya?ml$"],
|
managerFilePatterns: ["/^\\.gitea/workflows/[^/]+\\.ya?ml$/"],
|
||||||
},
|
},
|
||||||
|
|
||||||
packageRules: [
|
packageRules: [
|
||||||
|
// Cooling-off period, scaled by blast radius: how long a release should
|
||||||
|
// have been out (and un-yanked, un-hotfixed) before it's considered
|
||||||
|
// settled. Advisory only — see `internalChecksFilter` above.
|
||||||
|
{
|
||||||
|
matchUpdateTypes: ["major"],
|
||||||
|
minimumReleaseAge: "30 days",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
matchUpdateTypes: ["minor"],
|
||||||
|
minimumReleaseAge: "20 days",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
matchUpdateTypes: ["patch", "digest", "pin", "rollback"],
|
||||||
|
minimumReleaseAge: "10 days",
|
||||||
|
},
|
||||||
// material3 is deliberately pinned to the 1.5 *alpha* line for the
|
// material3 is deliberately pinned to the 1.5 *alpha* line for the
|
||||||
// Expressive APIs (see gradle/libs.versions.toml). Follow the alpha train
|
// Expressive APIs (see gradle/libs.versions.toml). Follow the alpha train
|
||||||
// but keep it in its own PR, reviewed in isolation; revisit the pin when
|
// but keep it in its own PR, reviewed in isolation; revisit the pin when
|
||||||
@@ -52,5 +102,18 @@
|
|||||||
],
|
],
|
||||||
groupName: "test dependencies",
|
groupName: "test dependencies",
|
||||||
},
|
},
|
||||||
|
// Last word on the PR table. The merge-confidence preset sets prBodyColumns
|
||||||
|
// from inside a packageRule of its own, and only for the datasources Mend
|
||||||
|
// supports — so a plain top-level prBodyColumns would lose to it for maven
|
||||||
|
// deps, and the Gradle wrapper / Actions / container bumps would keep the
|
||||||
|
// default columns and show no age at all. A rule declared after it wins,
|
||||||
|
// and gives every PR the same table.
|
||||||
|
// "Pending" earns its place under a flexible filter: when the bump lands on
|
||||||
|
// a version that has cleared its window but a newer one hasn't, that newer
|
||||||
|
// version is named here rather than silently withheld.
|
||||||
|
{
|
||||||
|
matchPackageNames: ["*"],
|
||||||
|
prBodyColumns: ["Package", "Type", "Change", "Age", "Pending", "Confidence"],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user