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:
|
||||
- bug
|
||||
- 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'
|
||||
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
|
||||
if: steps.scope.outputs.code == 'true'
|
||||
uses: android-actions/setup-android@v3
|
||||
uses: https://github.com/android-actions/setup-android@v3
|
||||
with:
|
||||
# Default ("tools platform-tools") drags in the Android Emulator
|
||||
# (~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
|
||||
# heavy job from running on every merge to main.
|
||||
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
|
||||
outputs:
|
||||
is_release: ${{ steps.v.outputs.is_release }}
|
||||
@@ -41,8 +49,16 @@ jobs:
|
||||
- name: Resolve version and whether it is a new release
|
||||
id: v
|
||||
env:
|
||||
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
|
||||
# Tags are read from Codeberg, which is canonical — deliberately NOT
|
||||
# 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: |
|
||||
set -e
|
||||
VERSION=$(grep -oP 'versionName\s*=\s*"\K[^"]+' app/build.gradle.kts)
|
||||
@@ -60,15 +76,28 @@ jobs:
|
||||
fi
|
||||
# A tag for this version already existing means the release shipped on
|
||||
# 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")
|
||||
if [ "$STATUS" = "200" ]; then
|
||||
echo "Tag v$VERSION already exists — nothing to release."
|
||||
echo "is_release=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "No tag for v$VERSION yet — cutting the release."
|
||||
echo "is_release=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
#
|
||||
# Anything other than a clean 200/404 is treated as fatal rather than
|
||||
# as "no tag". A Codeberg outage or a network blip would otherwise
|
||||
# read as absent and re-cut a release that has already shipped —
|
||||
# republishing to F-Droid and Play. Failing here is recoverable; a
|
||||
# duplicate release is not.
|
||||
STATUS=$(curl -s -o /dev/null -w '%{http_code}' "$TAG_API/git/refs/tags/v$VERSION" || echo 000)
|
||||
case "$STATUS" in
|
||||
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.
|
||||
# Also runs on manual dispatch, where it skips the build and just re-signs and
|
||||
@@ -412,20 +441,31 @@ jobs:
|
||||
"prerelease": False,
|
||||
}))
|
||||
PY
|
||||
# Upsert (re-run safe). POST also creates the tag at target_commitish
|
||||
# if the push mirror hasn't synced it yet.
|
||||
ID=$(curl -s -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG" | jq -r '.id // empty')
|
||||
if [ -n "$ID" ]; then
|
||||
curl -s -o /dev/null -w "release PATCH HTTP %{http_code}\n" -X PATCH \
|
||||
# Create (or update) the release. Codeberg 500s on a POST/GET against a
|
||||
# tag it has only just received — the release request outruns the
|
||||
# indexing of the ref we pushed a moment ago — so a single attempt kept
|
||||
# failing and skipping the mirror even though the very same call
|
||||
# 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" \
|
||||
-d @cb-payload.json "$API/releases/$ID"
|
||||
else
|
||||
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"
|
||||
-d @cb-payload.json "$API/releases")
|
||||
echo "release POST attempt $attempt HTTP $CODE"
|
||||
ID=$(jq -r '.id // empty' cb-response.json 2>/dev/null || true)
|
||||
fi
|
||||
if [ -z "$ID" ]; then echo "Could not resolve Codeberg release id." >&2; exit 1; fi
|
||||
[ -n "$ID" ] && break
|
||||
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.
|
||||
for A in "$ASSET_APK" "$ASSET_SUM"; do
|
||||
|
||||
@@ -29,14 +29,30 @@ jobs:
|
||||
- name: Run Renovate
|
||||
run: renovate
|
||||
env:
|
||||
# Self-hosted Gitea, not github.com.
|
||||
RENOVATE_PLATFORM: gitea
|
||||
RENOVATE_ENDPOINT: https://gitea.jeanlucmakiola.de/api/v1
|
||||
# Bot-account token (Gitea secret). Needs repo read/write + PR scope.
|
||||
# Renovate targets Codeberg (canonical) while still RUNNING on the
|
||||
# Gitea runner. Moving the job to Codeberg would put a repo-write
|
||||
# token on the contributor-facing runner, which is exactly what the
|
||||
# .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 }}
|
||||
# Scope to this repo only — no org-wide autodiscovery.
|
||||
RENOVATE_AUTODISCOVER: 'false'
|
||||
RENOVATE_REPOSITORIES: '["makiolaj/calendula"]'
|
||||
# Commits/PRs authored as the bot, not a real maintainer.
|
||||
RENOVATE_REPOSITORIES: '["jlmakiola/calendula"]'
|
||||
# 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>'
|
||||
# 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
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -50,6 +50,11 @@ google-services.json
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Editor swap/backup files
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# F-Droid local artifacts (the pipeline generates them in CI)
|
||||
/fdroid/
|
||||
|
||||
|
||||
2
.gitmodules
vendored
2
.gitmodules
vendored
@@ -1,3 +1,3 @@
|
||||
[submodule "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
|
||||
|
||||
Hosted on self-hosted Gitea, released through self-hosted F-Droid repo on
|
||||
Hetzner. Same infrastructure as `HouseHoldKeaper`.
|
||||
**Codeberg (`jlmakiola/calendula`) is canonical** — git, issues, PRs, tags and
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Delivered in four releasable slices (plan:
|
||||
`docs/superpowers/plans/2026-06-11-03-write-support.md`). The V1 spec is a
|
||||
guide here, not a contract — scope per slice is decided as we go.
|
||||
Delivered in four releasable slices. The V1 spec is a guide here, not a
|
||||
contract — scope per slice is decided as we go.
|
||||
|
||||
| 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)
|
||||
then `feat/ics-import` (parser, restore, dedup by UID). Import is
|
||||
liberal-in/strict-out: skip-and-report foreign `VTIMEZONE` / `RECURRENCE-ID`
|
||||
/ guest lists it can't model. Plans:
|
||||
`docs/superpowers/plans/2026-06-18-05-ics-export.md` + `…-06-ics-import.md`.
|
||||
/ guest lists it can't model.
|
||||
11. **Snooze / dismiss notification actions** *(merged into release/v2.8.0)* —
|
||||
followed the `.ics` work; inherits v2.6's deferred exact-alarm/WorkManager
|
||||
decision (snooze must re-fire an alarm).
|
||||
|
||||
@@ -25,7 +25,7 @@ release-cut.
|
||||
|
||||
## 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] 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
|
||||
|
||||
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [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
|
||||
|
||||
### 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>
|
||||
|
||||
<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/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">
|
||||
@@ -105,10 +105,26 @@ without reinstalling. Or build from source — see below.
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
- **[Contributing](CONTRIBUTING.md)** — how to report, propose, and patch
|
||||
- **[Building from source](docs/BUILDING.md)** — requirements and Gradle tasks
|
||||
- **[Architecture](docs/ARCHITECTURE.md)** — the layered design and key pipelines
|
||||
- **[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
|
||||
|
||||
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.Notifications
|
||||
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.Tune
|
||||
import androidx.compose.material.icons.filled.UploadFile
|
||||
@@ -408,7 +409,9 @@ private fun LanguageRow(position: Position) {
|
||||
@Composable
|
||||
private fun AboutCard() {
|
||||
// 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(
|
||||
logo = { AppLogo() },
|
||||
appName = stringResource(R.string.app_name),
|
||||
@@ -424,6 +427,11 @@ private fun AboutCard() {
|
||||
label = stringResource(R.string.settings_license),
|
||||
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(
|
||||
icon = Icons.Default.Favorite,
|
||||
|
||||
@@ -53,11 +53,11 @@
|
||||
<string name="event_share_failed">تعذّر مشاركة هذا الحدث.</string>
|
||||
<string name="event_delete_title">حذف الحدث؟</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_following">هذا وجميع الأحداث اللاحقة</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_write_denied">Calendula يحتاج إلى صلاحية للكتابة لحذف الأحداث</string>
|
||||
<string name="dialog_cancel">إلغاء</string>
|
||||
@@ -108,7 +108,7 @@
|
||||
<string name="event_edit_conflict_discard_hint">الحدث يبقى كما هو الآن</string>
|
||||
<string name="import_reminder_prompt_title">هل تريد تطبيق تذكيرك الافتراضي؟</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_default">الافتراضي</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_count">%1$s، %2$d مرّات</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="event_attendee_unknown">—</string>
|
||||
<string name="event_attendee_accepted">تم القُبول</string>
|
||||
@@ -166,7 +166,7 @@
|
||||
<string name="event_status_cancelled">ملغي</string>
|
||||
<string name="event_availability_free">متاح</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_optional">اختياري</string>
|
||||
<string name="reminder_default">التذكير الافتراضي</string>
|
||||
@@ -180,7 +180,7 @@
|
||||
<string name="reminder_onboarding_body">Android لا يعرض تذكيرات الأحداث من نفسه — بل يجب أن يقوم تطبيق تقويم بذلك. دع Calendula يتولى هذه المهمة.</string>
|
||||
<string name="reminder_benefit_delivery_body">كل تذكير لأحداثك يصِل كإشعار، في الوقت المحدد تمامًا.</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_onboarding_enable_button">شَغِّل التذكيرات</string>
|
||||
<string name="reminder_onboarding_skip_button">ليس الآن</string>
|
||||
@@ -200,7 +200,7 @@
|
||||
<string name="search_hint">البحث عن الأحداث</string>
|
||||
<string name="search_clear">مسح</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_back">الرجوع</string>
|
||||
<string name="search_action">البحث</string>
|
||||
@@ -217,7 +217,7 @@
|
||||
<string name="back">رجوع</string>
|
||||
<string name="settings_section_appearance">المظهر</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_soften_colors">ألوان تقويم ناعمة</string>
|
||||
<string name="settings_soften_colors_summary">خفف ألوان التقويم والأحداث لتتناسب مع الثيم. قم بإيقافه لإظهار الألوان الخام من مصدر التقويم.</string>
|
||||
@@ -227,7 +227,7 @@
|
||||
<string name="settings_font_choose_file">اختر ملفًا…</string>
|
||||
<string name="settings_font_custom_selected">خط مخصص</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_time_format_auto">تلقائي</string>
|
||||
<string name="settings_time_format">تنسيق الوقت</string>
|
||||
@@ -243,4 +243,58 @@
|
||||
<string name="settings_past_events_show">إظهار</string>
|
||||
<string name="settings_past_events_hide">إخفاء</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>
|
||||
|
||||
@@ -354,7 +354,7 @@
|
||||
<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_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_notifications_subtitle">Przypomnienia o wydarzeniach</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="special_dates_calendar_birthday">Urodziny</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>
|
||||
|
||||
@@ -462,6 +462,7 @@
|
||||
<string name="settings_license_value">MIT</string>
|
||||
<string name="settings_about_author">by Jean-Luc Makiola</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_version">Version %1$s</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_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_license_url" translatable="false">https://gitea.jeanlucmakiola.de/makiolaj/calendula/src/branch/main/LICENSE</string>
|
||||
<string name="about_source_url" translatable="false">https://codeberg.org/jlmakiola/calendula</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_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
|
||||
field defaults, reminders toggle) and small state (last-used calendar).
|
||||
- **`ui/`** — one package per screen, each with Screen + ViewModel +
|
||||
UiState. Shared pieces in `ui/common/` (OptionCard — the app's only
|
||||
sanctioned selection-dialog style —, recurrence humanizer, FAB column,
|
||||
drawer, transitions).
|
||||
UiState. Shared pieces in `ui/common/` (recurrence humanizer, FAB column,
|
||||
drawer, transitions). Selection pickers are full-screen and come from
|
||||
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
|
||||
|
||||
@@ -81,7 +87,7 @@ exactly like an event tap.
|
||||
## Recurring writes
|
||||
|
||||
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
|
||||
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),
|
||||
mappers parse `ColumnReader`/plain maps instead of cursors, domain logic
|
||||
(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
|
||||
F-Droid repo and then mints the `vX.Y.Z` tag + release. See docs/RELEASING.md.
|
||||
|
||||
@@ -1,12 +1,35 @@
|
||||
# Building from source
|
||||
|
||||
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
|
||||
|
||||
- **Android SDK 36+**
|
||||
- **JDK 17** — if your default JDK is not 17, set `JAVA_HOME` explicitly.
|
||||
- **JDK 17** — not newer; the Android Gradle Plugin requires exactly 17. If your
|
||||
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.
|
||||
|
||||
@@ -14,11 +37,44 @@ The Gradle wrapper is checked in, so you don't need a system Gradle.
|
||||
|
||||
```bash
|
||||
./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
|
||||
```
|
||||
|
||||
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 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 |
|
||||
|---|---|
|
||||
| [`../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 |
|
||||
| [`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) |
|
||||
| [`../.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/REQUIREMENTS.md`](../.planning/REQUIREMENTS.md) | Requirement checklist per milestone |
|
||||
| [`../.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 |
|
||||
| [`superpowers/plans/`](superpowers/plans/) | Per-milestone implementation plans with task checklists — historical record of how each slice was built, including provider lessons learned |
|
||||
| [`design/`](design/) | Per-feature design notes kept for features whose provider behaviour is worth recording |
|
||||
| [`../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-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
|
||||
artifacts of the agentic workflow that built each milestone — they get
|
||||
status updates but are never rewritten. The `.planning/` files are living
|
||||
documents and should stay current.
|
||||
Conventions: the `.planning/` files are living documents and should stay current.
|
||||
`ARCHITECTURE.md` is the authoritative orientation tour — it is updated with the
|
||||
code, and is the right place for a lesson learned about the calendar provider.
|
||||
|
||||
@@ -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
|
||||
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
|
||||
still report a green `CI` check.
|
||||
- **`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
|
||||
(`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
|
||||
aren't git objects and don't sync, so the pipeline creates the release over the
|
||||
Codeberg API and attaches `calendula_v<version>.apk` + its `.sha256`. It's the
|
||||
Codeberg **push-mirrors** branches and tags to Gitea, but releases aren't git
|
||||
objects and don't sync in either direction, so the pipeline creates the release
|
||||
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
|
||||
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
|
||||
@@ -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
|
||||
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)
|
||||
|
||||
| Secret | Purpose |
|
||||
|
||||
@@ -18,9 +18,9 @@ Categories:
|
||||
- Calendar & Agenda
|
||||
License: MIT
|
||||
AuthorName: Jean-Luc Makiola
|
||||
SourceCode: https://gitea.jeanlucmakiola.de/makiolaj/calendula
|
||||
IssueTracker: https://gitea.jeanlucmakiola.de/makiolaj/calendula/issues
|
||||
Changelog: https://gitea.jeanlucmakiola.de/makiolaj/calendula/src/branch/main/CHANGELOG.md
|
||||
SourceCode: https://codeberg.org/jlmakiola/calendula
|
||||
IssueTracker: https://codeberg.org/jlmakiola/calendula/issues
|
||||
Changelog: https://codeberg.org/jlmakiola/calendula/src/branch/main/CHANGELOG.md
|
||||
Donate: https://ko-fi.com/jeanlucmakiola
|
||||
|
||||
AutoName: Calendula
|
||||
|
||||
@@ -6,6 +6,6 @@ Summary: A modern Material 3 Expressive calendar for Android.
|
||||
Categories:
|
||||
- Time
|
||||
|
||||
SourceCode: https://gitea.jeanlucmakiola.de/makiolaj/calendula
|
||||
IssueTracker: https://gitea.jeanlucmakiola.de/makiolaj/calendula/issues
|
||||
SourceCode: https://codeberg.org/jlmakiola/calendula
|
||||
IssueTracker: https://codeberg.org/jlmakiola/calendula/issues
|
||||
Donate: https://ko-fi.com/jeanlucmakiola
|
||||
|
||||
@@ -7,6 +7,23 @@
|
||||
":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
|
||||
// anything touching the build, the same on-device check) as a feature
|
||||
// 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.
|
||||
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"],
|
||||
prConcurrentLimit: 5,
|
||||
prHourlyLimit: 0,
|
||||
@@ -25,11 +58,28 @@
|
||||
|
||||
// Gitea Actions workflows live under .gitea/workflows, not .github — extend
|
||||
// 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": {
|
||||
fileMatch: ["^\\.gitea/workflows/[^/]+\\.ya?ml$"],
|
||||
managerFilePatterns: ["/^\\.gitea/workflows/[^/]+\\.ya?ml$/"],
|
||||
},
|
||||
|
||||
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
|
||||
// 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
|
||||
@@ -52,5 +102,18 @@
|
||||
],
|
||||
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