Compare commits
54 Commits
39cc70be95
...
v2.16.0
| Author | SHA1 | Date | |
|---|---|---|---|
| bb34973047 | |||
| 108a1890be | |||
| 521c0ffd78 | |||
| 33a37d4105 | |||
| 6ccc0cbed2 | |||
|
|
6e6ffce271 | ||
|
|
23386bb998 | ||
|
|
28b1568486 | ||
|
|
5127971a37 | ||
|
|
0562af8d66 | ||
|
|
e3d3728bbd | ||
|
|
8cc9d075fd | ||
|
|
ede6d967c1 | ||
| c30f6ab318 | |||
| dd2b96b5fd | |||
| c22658db4c | |||
| 7b3893ddc6 | |||
| 9e28fa4981 | |||
| 7549c8fe04 | |||
| 75f699b714 | |||
| 3e821c9092 | |||
| 7602f158a3 | |||
| 052335e842 | |||
| 28fb7404d8 | |||
| 6ab235e1d8 | |||
| fb90c94e37 | |||
| 7a05fc3ac1 | |||
| bf38dac5d7 | |||
| 1e9581528e | |||
| 279872aa3b | |||
| ff6c4b6a4d | |||
| 7fb01ab8d1 | |||
| bdb16d069e | |||
| beb8536e8a | |||
| 63ba69729e | |||
| 01a6c8bab8 | |||
| f70b33b864 | |||
| 0df420e551 | |||
| 488de490f9 | |||
| f15fffa799 | |||
| 1499b5803d | |||
| b90f07a816 | |||
| 53d42341c0 | |||
| 55ffaad94f | |||
|
|
5bdde146c1 | ||
|
|
97f87fb927 | ||
|
|
469166c0bb | ||
|
|
b10965babe | ||
|
|
e5c122a2ee | ||
|
|
3aefc5c8f8 | ||
|
|
ecc643ac81 | ||
| 8e4d0defa2 | |||
| f459f7d39c | |||
| 6a49539ae5 |
@@ -1,11 +1,13 @@
|
|||||||
name: Release — F-Droid repo + Gitea release
|
name: Release — F-Droid repo + Gitea/Codeberg release
|
||||||
|
|
||||||
# A release is cut by merging a release branch into main with a bumped
|
# A release is cut by merging a release branch into main with a bumped
|
||||||
# versionName (see docs/RELEASING.md). This workflow reads that versionName and,
|
# versionName (see docs/RELEASING.md). This workflow reads that versionName and,
|
||||||
# if no matching tag exists yet, runs tests, builds + signs the APK, publishes
|
# if no matching tag exists yet, runs tests, builds + signs the APK, publishes
|
||||||
# it to the F-Droid repo, and only then creates the vX.Y.Z tag + Gitea release
|
# it to the F-Droid repo, creates the vX.Y.Z tag + Gitea release, and mirrors
|
||||||
# itself — the tag is an output of the pipeline, not its trigger. Ordinary
|
# that release to Codeberg with the signed APK + a SHA-256 checksum as a
|
||||||
# merges (no version bump) fall through `detect` and do nothing.
|
# direct-download channel — the tag is an output of the pipeline, not its
|
||||||
|
# trigger. Ordinary merges (no version bump) fall through `detect` and do
|
||||||
|
# nothing.
|
||||||
#
|
#
|
||||||
# A manual workflow_dispatch (from a branch) runs the re-sign-only recovery
|
# A manual workflow_dispatch (from a branch) runs the re-sign-only recovery
|
||||||
# path: it re-signs the existing F-Droid index with the repo key and re-uploads,
|
# path: it re-signs the existing F-Droid index with the repo key and re-uploads,
|
||||||
@@ -350,3 +352,88 @@ jobs:
|
|||||||
curl -s -X POST -H "Authorization: token $TOKEN" \
|
curl -s -X POST -H "Authorization: token $TOKEN" \
|
||||||
-F "attachment=@/tmp/$ASSET" \
|
-F "attachment=@/tmp/$ASSET" \
|
||||||
"$API/releases/$ID/assets?name=$ASSET" -o /dev/null -w "asset upload HTTP %{http_code}\n"
|
"$API/releases/$ID/assets?name=$ASSET" -o /dev/null -w "asset upload HTTP %{http_code}\n"
|
||||||
|
|
||||||
|
# Mirror the release to the Codeberg mirror as a direct-download channel
|
||||||
|
# for users who don't want F-Droid. Gitea already push-mirrors branches +
|
||||||
|
# tags to Codeberg, but releases aren't git objects so they don't sync —
|
||||||
|
# we create the release there over the API and attach the signed APK plus
|
||||||
|
# a SHA-256 checksum. The APK is identical to the F-Droid one (same app
|
||||||
|
# key), so this adds no trust surface. Best-effort: a Codeberg outage
|
||||||
|
# (it 504s under load) must never fail an already-published F-Droid
|
||||||
|
# release. Needs the CODEBERG_RELEASE_TOKEN secret; skips cleanly if unset.
|
||||||
|
- name: Publish release to Codeberg
|
||||||
|
if: env.IS_RELEASE == 'true'
|
||||||
|
continue-on-error: true
|
||||||
|
env:
|
||||||
|
TOKEN: ${{ secrets.CODEBERG_RELEASE_TOKEN }}
|
||||||
|
API: https://codeberg.org/api/v1/repos/jlmakiola/calendula
|
||||||
|
SHA: ${{ github.sha }}
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
if [ -z "${TOKEN:-}" ]; then
|
||||||
|
echo "CODEBERG_RELEASE_TOKEN not set — skipping Codeberg publish."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
TAG="v$VERSION"
|
||||||
|
APK="app/build/outputs/apk/release/app-release.apk"
|
||||||
|
if [ ! -f "$APK" ]; then echo "No release APK found — skipping." >&2; exit 1; fi
|
||||||
|
ASSET_APK="calendula_v${VERSION}.apk"
|
||||||
|
ASSET_SUM="${ASSET_APK}.sha256"
|
||||||
|
cp "$APK" "/tmp/$ASSET_APK"
|
||||||
|
( cd /tmp && sha256sum "$ASSET_APK" > "$ASSET_SUM" )
|
||||||
|
|
||||||
|
# Release notes: reuse the section extracted for the Gitea release,
|
||||||
|
# fall back to the CHANGELOG entry if that step's file is gone.
|
||||||
|
if [ ! -s release-notes.md ]; then
|
||||||
|
awk -v ver="$VERSION" '
|
||||||
|
$0 ~ "^## \\[" ver "\\]" { flag = 1; next }
|
||||||
|
/^## \[/ { flag = 0 }
|
||||||
|
flag' CHANGELOG.md > release-notes.md
|
||||||
|
sed -i -e '/./,$!d' release-notes.md
|
||||||
|
fi
|
||||||
|
[ -s release-notes.md ] || echo "_See CHANGELOG.md for ${VERSION}._" > release-notes.md
|
||||||
|
# The pipeline creates the tag via the Gitea API, which the push mirror
|
||||||
|
# (sync_on_commit only fires on real git pushes) doesn't propagate
|
||||||
|
# promptly — so a release POST that carries a target_commitish can
|
||||||
|
# outrun the mirror and 500 on a commit/tag Codeberg hasn't received.
|
||||||
|
# Push the tag straight to Codeberg so it's guaranteed present, then
|
||||||
|
# attach the release to that existing tag with NO target_commitish
|
||||||
|
# (which is what triggered the 500).
|
||||||
|
git tag -f "$TAG" "$SHA"
|
||||||
|
git push -f "https://jlmakiola:${TOKEN}@codeberg.org/jlmakiola/calendula.git" \
|
||||||
|
"refs/tags/$TAG"
|
||||||
|
python3 - "$TAG" <<'PY' > cb-payload.json
|
||||||
|
import json, sys
|
||||||
|
print(json.dumps({
|
||||||
|
"tag_name": sys.argv[1],
|
||||||
|
"name": sys.argv[1],
|
||||||
|
"body": open("release-notes.md").read(),
|
||||||
|
"draft": False,
|
||||||
|
"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 \
|
||||||
|
-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"
|
||||||
|
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
|
||||||
|
|
||||||
|
# Attach APK + checksum, replacing any prior asset of the same name.
|
||||||
|
for A in "$ASSET_APK" "$ASSET_SUM"; do
|
||||||
|
OLD=$(curl -s -H "Authorization: token $TOKEN" "$API/releases/$ID/assets" \
|
||||||
|
| jq -r --arg n "$A" '.[] | select(.name==$n) | .id')
|
||||||
|
[ -n "$OLD" ] && curl -s -X DELETE -H "Authorization: token $TOKEN" "$API/releases/$ID/assets/$OLD" >/dev/null || true
|
||||||
|
curl -s -X POST -H "Authorization: token $TOKEN" \
|
||||||
|
-F "attachment=@/tmp/$A" \
|
||||||
|
"$API/releases/$ID/assets?name=$A" -o /dev/null -w "asset $A HTTP %{http_code}\n"
|
||||||
|
done
|
||||||
|
echo "Published $TAG to Codeberg."
|
||||||
|
|||||||
57
CHANGELOG.md
57
CHANGELOG.md
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [2.16.0] — 2026-07-24
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Choose how the month view is laid out. A new **Month view style** setting
|
- Choose how the month view is laid out. A new **Month view style** setting
|
||||||
(Settings → Views) offers three ways to read a month, each shown with a
|
(Settings → Views) offers three ways to read a month, each shown with a
|
||||||
@@ -24,18 +26,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
The Agenda view is untouched by this and stays available in all three styles —
|
The Agenda view is untouched by this and stays available in all three styles —
|
||||||
the split layout lists a single day, while Agenda remains a rolling multi-day
|
the split layout lists a single day, while Agenda remains a rolling multi-day
|
||||||
window with its own range settings.
|
window with its own range settings.
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- The "Upcoming" agenda widget now scales its text and rows to the size you give
|
|
||||||
it. Previously it was laid out once for the smallest size and simply stretched
|
|
||||||
when enlarged, so the text stayed small no matter how big you made the widget.
|
|
||||||
Now a bigger widget gets bigger, more readable type and roomier rows, while the
|
|
||||||
default size looks exactly as before — no new setting; it follows the size you
|
|
||||||
already chose ([#51]).
|
|
||||||
|
|
||||||
## [2.16.0] — 2026-07-17
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Give an event its own time zone. A new **Time zone** field (under "more
|
- Give an event its own time zone. A new **Time zone** field (under "more
|
||||||
fields" in the event form) pins an event to a specific zone, so a call set for
|
fields" in the event form) pins an event to a specific zone, so a call set for
|
||||||
8:00 AM in New York stays 8:00 AM in New York wherever you open it — and keeps
|
8:00 AM in New York stays 8:00 AM in New York wherever you open it — and keeps
|
||||||
@@ -53,6 +43,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
today icon in the top bar — always there, on today or not, matching the
|
today icon in the top bar — always there, on today or not, matching the
|
||||||
familiar calendar-app pattern. Leave it off to keep the floating button as
|
familiar calendar-app pattern. Leave it off to keep the floating button as
|
||||||
before ([#60]).
|
before ([#60]).
|
||||||
|
- Choose what Calendula calls itself on your home screen. A new **App name**
|
||||||
|
setting (Settings → Appearance) switches the launcher label between
|
||||||
|
**Calendula** and **Calendar**, for launchers that can't rename apps
|
||||||
|
themselves. Pick from a full-screen chooser that previews both names as
|
||||||
|
launcher marks; the change applies at once ([#44]).
|
||||||
|
- Calendula now speaks **Arabic**, laid out right-to-left, and its French and
|
||||||
|
Italian translations have been brought up to date — thanks to the community
|
||||||
|
translators on Weblate. Pick a language under Settings → Language (or leave it
|
||||||
|
on the system default).
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Dates in the Month, Week and Day title bars now follow your language and
|
- Dates in the Month, Week and Day title bars now follow your language and
|
||||||
@@ -71,6 +70,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
"24. Jun – 31. Jun" restated the day numbers already printed in the column
|
"24. Jun – 31. Jun" restated the day numbers already printed in the column
|
||||||
headers right below it, in the widest string in the bar. A week that straddles
|
headers right below it, in the widest string in the bar. A week that straddles
|
||||||
two months keeps the outgoing month until it is fully gone ([#60]).
|
two months keeps the outgoing month until it is fully gone ([#60]).
|
||||||
|
- The custom recurrence picker has been redesigned and tightened up. As you
|
||||||
|
build a rule — "every 2 weeks on Mon & Wed, until a date" — the live summary
|
||||||
|
now describes exactly what will be saved rather than a near-copy that could
|
||||||
|
drift from it, the amount fields accept being left blank (reading as their
|
||||||
|
shown default instead of greying out OK), and the read-out no longer jumps
|
||||||
|
around as you tap weekdays ([#42]).
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- An all-day event no longer shows up again the day after it happened. In time
|
- An all-day event no longer shows up again the day after it happened. In time
|
||||||
@@ -80,6 +85,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
window reached back across that boundary and pulled the event forward onto
|
window reached back across that boundary and pulled the event forward onto
|
||||||
"today". Each all-day event now lists only on the day it actually falls on
|
"today". Each all-day event now lists only on the day it actually falls on
|
||||||
([#65]).
|
([#65]).
|
||||||
|
- A multi-day event now shows under every day it spans in the Agenda, not just
|
||||||
|
its first day, so a trip or a multi-day booking appears on each day it covers.
|
||||||
|
- The "Upcoming" agenda widget now scales its text and rows to the size you give
|
||||||
|
it. Previously it was laid out once for the smallest size and simply stretched
|
||||||
|
when enlarged, so the text stayed small no matter how big you made the widget.
|
||||||
|
Now a bigger widget gets bigger, more readable type and roomier rows, while the
|
||||||
|
default size looks exactly as before — no new setting; it follows the size you
|
||||||
|
already chose ([#51]).
|
||||||
|
- Calendula now appears under other apps' "Add to calendar" / "Save to calendar"
|
||||||
|
actions. Some apps (e.g. DB Navigator) fire the widely-used "insert event"
|
||||||
|
intent with the singular `vnd.android.cursor.item/event` type, which Calendula
|
||||||
|
didn't advertise — so it was left out of the chooser, and if it was your only
|
||||||
|
calendar app the save silently did nothing. It now accepts that form, plus the
|
||||||
|
`INSERT_OR_EDIT` action, and opens the new event prefilled for review ([#74]).
|
||||||
|
- Opening a `.ics`/`.vcs` file now works even when another app hands it over
|
||||||
|
mislabelled as a generic download (`application/octet-stream`), as some mail
|
||||||
|
clients, browsers and file managers do — Calendula recognises it by its file
|
||||||
|
extension instead of relying on the declared type ([#74]).
|
||||||
|
- A recurrence end date no longer lands a day late. West of UTC, setting a rule
|
||||||
|
to end "until" a given day could save and show the day after the one picked;
|
||||||
|
the end date now reads back as chosen ([#42]).
|
||||||
|
- The status- and navigation-bar icons stay legible over full-screen pickers in
|
||||||
|
dark theme. They could render dark-on-dark — a near-invisible black clock
|
||||||
|
against the dark picker — instead of switching to light ([#70]).
|
||||||
|
|
||||||
## [2.15.0] — 2026-07-15
|
## [2.15.0] — 2026-07-15
|
||||||
|
|
||||||
@@ -1079,3 +1108,7 @@ automatically, with zero telemetry and no internet permission.
|
|||||||
[#53]: https://codeberg.org/jlmakiola/calendula/issues/53
|
[#53]: https://codeberg.org/jlmakiola/calendula/issues/53
|
||||||
[#60]: https://codeberg.org/jlmakiola/calendula/issues/60
|
[#60]: https://codeberg.org/jlmakiola/calendula/issues/60
|
||||||
[#65]: https://codeberg.org/jlmakiola/calendula/issues/65
|
[#65]: https://codeberg.org/jlmakiola/calendula/issues/65
|
||||||
|
[#74]: https://codeberg.org/jlmakiola/calendula/issues/74
|
||||||
|
[#42]: https://codeberg.org/jlmakiola/calendula/issues/42
|
||||||
|
[#44]: https://codeberg.org/jlmakiola/calendula/issues/44
|
||||||
|
[#70]: https://codeberg.org/jlmakiola/calendula/issues/70
|
||||||
|
|||||||
@@ -113,10 +113,13 @@ android {
|
|||||||
lint {
|
lint {
|
||||||
// Community translations are expected to be partial — a missing string
|
// Community translations are expected to be partial — a missing string
|
||||||
// falls back to the English base at runtime — so don't fail the build on
|
// falls back to the English base at runtime — so don't fail the build on
|
||||||
// it. Stale/extra keys (ExtraTranslation) stay fatal; scripts/
|
// it. Likewise a translated <plurals> may not fill every CLDR quantity
|
||||||
|
// form its locale defines (e.g. Arabic needs "zero"); the missing form
|
||||||
|
// falls back to "other" at runtime, so MissingQuantity is informational
|
||||||
|
// too. Stale/extra keys (ExtraTranslation) stay fatal; scripts/
|
||||||
// check_translations.py guards the same invariants with clearer,
|
// check_translations.py guards the same invariants with clearer,
|
||||||
// translator-facing messages.
|
// translator-facing messages.
|
||||||
informational += "MissingTranslation"
|
informational += listOf("MissingTranslation", "MissingQuantity")
|
||||||
}
|
}
|
||||||
|
|
||||||
testOptions {
|
testOptions {
|
||||||
|
|||||||
@@ -112,6 +112,28 @@
|
|||||||
<data android:mimeType="text/x-vcalendar" />
|
<data android:mimeType="text/x-vcalendar" />
|
||||||
<data android:mimeType="application/ics" />
|
<data android:mimeType="application/ics" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<!-- Same .ics/.vcs data arriving mislabelled as a generic download —
|
||||||
|
application/octet-stream — the way many mail clients, browsers and
|
||||||
|
file managers hand off attachments. Matched by file extension, so
|
||||||
|
this stays a separate filter: a pathPattern here must not narrow
|
||||||
|
the MIME-typed VIEW filter above (that one has no path and must
|
||||||
|
keep matching regardless of name). The import handler ignores the
|
||||||
|
MIME type, so a let-through octet-stream .ics imports normally.
|
||||||
|
Best-effort: pathPattern is reliable for file:// (and content://
|
||||||
|
whose path carries the filename); content:// URIs that expose no
|
||||||
|
name still fall back to the MIME-typed filter above. -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<data android:scheme="content" />
|
||||||
|
<data android:scheme="file" />
|
||||||
|
<data android:host="*" />
|
||||||
|
<data android:mimeType="application/octet-stream" />
|
||||||
|
<data android:pathPattern=".*\\.ics" />
|
||||||
|
<data android:pathPattern=".*\\.vcs" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
<!-- Receive a .ics/.vcs shared from another app (same MIME set). -->
|
<!-- Receive a .ics/.vcs shared from another app (same MIME set). -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND" />
|
<action android:name="android.intent.action.SEND" />
|
||||||
@@ -123,11 +145,13 @@
|
|||||||
|
|
||||||
<!-- Let another app or widget (e.g. the Todo Agenda widget) launch us
|
<!-- Let another app or widget (e.g. the Todo Agenda widget) launch us
|
||||||
to create a new event, the way the AOSP calendar accepts it:
|
to create a new event, the way the AOSP calendar accepts it:
|
||||||
ACTION_INSERT on the events dir mime type, carrying the new
|
ACTION_INSERT on the events *dir* mime type, carrying the new
|
||||||
event's fields as CalendarContract extras
|
event's fields as CalendarContract extras
|
||||||
(MainActivity.insertFormOrNull, issue #30). ACTION_EDIT on the
|
(MainActivity.insertFormOrNull, issue #30). ACTION_EDIT on the
|
||||||
dir mime is AOSP's "edit a new event" — i.e. create — so it maps
|
dir mime is AOSP's "edit a new event" — i.e. create — so it maps
|
||||||
to the same prefilled create form. -->
|
to the same prefilled create form. (The far more common *item*-
|
||||||
|
typed INSERT — the form the Android docs' example and apps like
|
||||||
|
DB Navigator use — is the item filter below, issue #74.) -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.INSERT" />
|
<action android:name="android.intent.action.INSERT" />
|
||||||
<action android:name="android.intent.action.EDIT" />
|
<action android:name="android.intent.action.EDIT" />
|
||||||
@@ -135,14 +159,31 @@
|
|||||||
<data android:mimeType="vnd.android.cursor.dir/event" />
|
<data android:mimeType="vnd.android.cursor.dir/event" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<!-- Edit an existing event another app/assistant/widget points at:
|
<!-- Create or edit an event another app/assistant/widget points at,
|
||||||
ACTION_EDIT on content://com.android.calendar/events/<id>, the way
|
addressed by the provider's *item* MIME type. Three actions share
|
||||||
AOSP fires it. Opens the occurrence in the edit form (not the
|
this filter, told apart at runtime by the intent's data:
|
||||||
read-only detail — that's the VIEW filter above). Matched by the
|
|
||||||
provider's item MIME type, like the VIEW filter. The occurrence's
|
• ACTION_INSERT — create. This is the form the Android docs'
|
||||||
times ride as EXTRA_EVENT_BEGIN_TIME / EXTRA_EVENT_END_TIME when
|
"insert an event" example and many apps use
|
||||||
supplied (MainActivity.editEventKeyOrNull). -->
|
(setType("vnd.android.cursor.item/event")), e.g. DB Navigator's
|
||||||
|
"Save to calendar". The dir-typed filter above alone missed it,
|
||||||
|
so Calendula never showed in the chooser — and, when it was the
|
||||||
|
only calendar app, the intent resolved to nothing (issue #74).
|
||||||
|
• ACTION_INSERT_OR_EDIT — the third "add to calendar" action AOSP
|
||||||
|
and Google Calendar register; a create, or an edit when it
|
||||||
|
carries an event id.
|
||||||
|
• ACTION_EDIT — edit the existing event at
|
||||||
|
content://com.android.calendar/events/<id> (an id-less EDIT is a
|
||||||
|
create). Opens the occurrence in the edit form, not the
|
||||||
|
read-only detail — that's the VIEW filter above.
|
||||||
|
|
||||||
|
Create fields ride as CalendarContract extras; an edit's
|
||||||
|
occurrence times ride as EXTRA_EVENT_BEGIN_TIME /
|
||||||
|
EXTRA_EVENT_END_TIME when supplied (MainActivity.insertFormOrNull
|
||||||
|
/ editEventKeyOrNull). -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.INSERT" />
|
||||||
|
<action android:name="android.intent.action.INSERT_OR_EDIT" />
|
||||||
<action android:name="android.intent.action.EDIT" />
|
<action android:name="android.intent.action.EDIT" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<data android:mimeType="vnd.android.cursor.item/event" />
|
<data android:mimeType="vnd.android.cursor.item/event" />
|
||||||
|
|||||||
@@ -222,10 +222,12 @@ class MainActivity : AppCompatActivity() {
|
|||||||
* [buildInsertEventForm].
|
* [buildInsertEventForm].
|
||||||
*/
|
*/
|
||||||
private fun Intent.insertFormOrNull(): EventForm? {
|
private fun Intent.insertFormOrNull(): EventForm? {
|
||||||
// ACTION_EDIT on an existing event routes to the edit form instead
|
// ACTION_EDIT / ACTION_INSERT_OR_EDIT on an existing event route to the
|
||||||
// ([editEventKeyOrNull]); only an id-less EDIT is a create.
|
// edit form instead ([editEventKeyOrNull]); an id-less one is a create,
|
||||||
|
// as is any plain ACTION_INSERT.
|
||||||
val isCreate = action == Intent.ACTION_INSERT ||
|
val isCreate = action == Intent.ACTION_INSERT ||
|
||||||
(action == Intent.ACTION_EDIT && editEventKeyOrNull() == null)
|
((action == Intent.ACTION_EDIT || action == Intent.ACTION_INSERT_OR_EDIT) &&
|
||||||
|
editEventKeyOrNull() == null)
|
||||||
if (!isCreate) return null
|
if (!isCreate) return null
|
||||||
return buildInsertEventForm(
|
return buildInsertEventForm(
|
||||||
beginMillis = longExtraOrNull(CalendarContract.EXTRA_EVENT_BEGIN_TIME),
|
beginMillis = longExtraOrNull(CalendarContract.EXTRA_EVENT_BEGIN_TIME),
|
||||||
@@ -344,10 +346,11 @@ class MainActivity : AppCompatActivity() {
|
|||||||
* occurrence's times as `EXTRA_EVENT_BEGIN_TIME` / `EXTRA_EVENT_END_TIME`
|
* occurrence's times as `EXTRA_EVENT_BEGIN_TIME` / `EXTRA_EVENT_END_TIME`
|
||||||
* when it has them, otherwise we carry [NO_OCCURRENCE_TIME] and
|
* when it has them, otherwise we carry [NO_OCCURRENCE_TIME] and
|
||||||
* [EventEditViewModel.openForEdit] falls back to the event row's own
|
* [EventEditViewModel.openForEdit] falls back to the event row's own
|
||||||
* DTSTART/DTEND. An id-less `ACTION_EDIT` is a create instead ([insertFormOrNull]).
|
* DTSTART/DTEND. An id-less `ACTION_EDIT` — or `ACTION_INSERT_OR_EDIT`, which
|
||||||
|
* some apps fire — is a create instead ([insertFormOrNull]).
|
||||||
*/
|
*/
|
||||||
private fun Intent.editEventKeyOrNull(): LongArray? {
|
private fun Intent.editEventKeyOrNull(): LongArray? {
|
||||||
if (action != Intent.ACTION_EDIT) return null
|
if (action != Intent.ACTION_EDIT && action != Intent.ACTION_INSERT_OR_EDIT) return null
|
||||||
val uri = data ?: return null
|
val uri = data ?: return null
|
||||||
if (uri.host != CALENDAR_PROVIDER_HOST) return null
|
if (uri.host != CALENDAR_PROVIDER_HOST) return null
|
||||||
val segments = uri.pathSegments
|
val segments = uri.pathSegments
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package de.jeanlucmakiola.calendula.ui.common
|
||||||
|
|
||||||
|
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drag distance that turns a calendar page, shared by the month, week and day
|
||||||
|
* views so all three answer a swipe at the same point.
|
||||||
|
*
|
||||||
|
* It was 6dp once, which is inside the distance a tap wanders: brushing the grid
|
||||||
|
* changed the month, and a page that turns on an unintended gesture reads as the
|
||||||
|
* animation misfiring rather than as the gesture being over-eager.
|
||||||
|
*/
|
||||||
|
val CALENDAR_SWIPE_THRESHOLD = 24.dp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The whole-page horizontal swipe: one page per gesture, committed **the moment
|
||||||
|
* the drag clears [CALENDAR_SWIPE_THRESHOLD]** rather than when the finger lifts.
|
||||||
|
*
|
||||||
|
* Waiting for the lift meant the page sat still under a finger that had already
|
||||||
|
* travelled far enough to ask for it, and the answer only arrived once you let
|
||||||
|
* go — which reads as the view being slow rather than as a deliberate commit.
|
||||||
|
* Firing on the threshold is what makes the gesture feel like it is being
|
||||||
|
* followed. The trade is that a drag can no longer be taken back by dragging the
|
||||||
|
* other way; in practice, once you have moved 24dp deliberately you meant it, and
|
||||||
|
* the page you land on is one swipe back.
|
||||||
|
*
|
||||||
|
* Deliberately **horizontal-only**. The week and day timelines scroll vertically
|
||||||
|
* underneath this, and a two-dimensional detector here would claim those drags
|
||||||
|
* before the inner scroll ever saw them. As it is, a horizontal drag crosses this
|
||||||
|
* detector's slop while a vertical one is consumed below, and the two coexist.
|
||||||
|
* (The month view's split style needs a vertical axis as well, so it keeps its
|
||||||
|
* own axis-locking detector rather than using this.)
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun rememberCalendarPageSwipe(
|
||||||
|
onSwipeNext: () -> Unit,
|
||||||
|
onSwipePrev: () -> Unit,
|
||||||
|
): Modifier {
|
||||||
|
val threshold = with(LocalDensity.current) { CALENDAR_SWIPE_THRESHOLD.toPx() }
|
||||||
|
return Modifier.pointerInput(onSwipeNext, onSwipePrev) {
|
||||||
|
var accum = 0f
|
||||||
|
// One page per gesture: without this a long drag would keep re-firing
|
||||||
|
// every time the accumulator crossed the threshold again.
|
||||||
|
var fired = false
|
||||||
|
detectHorizontalDragGestures(
|
||||||
|
onDragStart = {
|
||||||
|
accum = 0f
|
||||||
|
fired = false
|
||||||
|
},
|
||||||
|
onDragEnd = {
|
||||||
|
accum = 0f
|
||||||
|
fired = false
|
||||||
|
},
|
||||||
|
onDragCancel = {
|
||||||
|
accum = 0f
|
||||||
|
fired = false
|
||||||
|
},
|
||||||
|
onHorizontalDrag = { _, drag ->
|
||||||
|
accum += drag
|
||||||
|
if (!fired) {
|
||||||
|
val commit = when {
|
||||||
|
accum < -threshold -> onSwipeNext
|
||||||
|
accum > threshold -> onSwipePrev
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
if (commit != null) {
|
||||||
|
fired = true
|
||||||
|
commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,6 @@ import androidx.compose.animation.animateColorAsState
|
|||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
|
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -43,7 +42,6 @@ import androidx.compose.material3.rememberDrawerState
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
@@ -79,6 +77,7 @@ import de.jeanlucmakiola.calendula.ui.common.NowLine
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.ViewSwitcherPill
|
import de.jeanlucmakiola.calendula.ui.common.ViewSwitcherPill
|
||||||
import de.jeanlucmakiola.calendula.ui.common.calendarSlideTransition
|
import de.jeanlucmakiola.calendula.ui.common.calendarSlideTransition
|
||||||
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarFadeSpec
|
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarFadeSpec
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarPageSwipe
|
||||||
import de.jeanlucmakiola.floret.identity.rememberReduceMotion
|
import de.jeanlucmakiola.floret.identity.rememberReduceMotion
|
||||||
import de.jeanlucmakiola.calendula.ui.common.next
|
import de.jeanlucmakiola.calendula.ui.common.next
|
||||||
import de.jeanlucmakiola.calendula.ui.common.LocalSoftenColors
|
import de.jeanlucmakiola.calendula.ui.common.LocalSoftenColors
|
||||||
@@ -266,8 +265,6 @@ private fun DayContent(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val threshold = with(density) { 24.dp.toPx() }
|
|
||||||
var dragAccum by remember { mutableFloatStateOf(0f) }
|
|
||||||
val slideSpec = rememberCalendarSlideSpec()
|
val slideSpec = rememberCalendarSlideSpec()
|
||||||
val fadeSpec = rememberCalendarFadeSpec()
|
val fadeSpec = rememberCalendarFadeSpec()
|
||||||
val reduceMotion = rememberReduceMotion()
|
val reduceMotion = rememberReduceMotion()
|
||||||
@@ -296,20 +293,7 @@ private fun DayContent(
|
|||||||
// Whole-page horizontal swipe, one level above the timeline's vertical
|
// Whole-page horizontal swipe, one level above the timeline's vertical
|
||||||
// scroll: a horizontal drag crosses this detector's slop, while a vertical
|
// scroll: a horizontal drag crosses this detector's slop, while a vertical
|
||||||
// drag is consumed by the inner scroll first — the two gestures coexist.
|
// drag is consumed by the inner scroll first — the two gestures coexist.
|
||||||
val swipeModifier = Modifier.pointerInput(Unit) {
|
val swipeModifier = rememberCalendarPageSwipe(onSwipeNext, onSwipePrev)
|
||||||
detectHorizontalDragGestures(
|
|
||||||
onDragStart = { dragAccum = 0f },
|
|
||||||
onDragEnd = {
|
|
||||||
when {
|
|
||||||
dragAccum < -threshold -> onSwipeNext()
|
|
||||||
dragAccum > threshold -> onSwipePrev()
|
|
||||||
}
|
|
||||||
dragAccum = 0f
|
|
||||||
},
|
|
||||||
onDragCancel = { dragAccum = 0f },
|
|
||||||
onHorizontalDrag = { _, drag -> dragAccum += drag },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
AnimatedContent(
|
AnimatedContent(
|
||||||
targetState = state,
|
targetState = state,
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
package de.jeanlucmakiola.calendula.ui.month
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibilityScope
|
||||||
|
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||||
|
import androidx.compose.animation.SharedTransitionScope
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import kotlinx.datetime.LocalDate
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What lets the split style's compact grid *become* the full month rather than be
|
||||||
|
* swapped for it (#53).
|
||||||
|
*
|
||||||
|
* The two grids are separate composables — the compact one draws dots, the
|
||||||
|
* expanded one draws the paged style's bars and pills — so nothing about them is
|
||||||
|
* shared by construction. Tagging the pieces that mean the same thing on both
|
||||||
|
* sides with the same [MonthMorphKey] hands Compose enough to animate one into
|
||||||
|
* the other: a day's cell grows, its number rides along, and each dot travels out
|
||||||
|
* to the bar it stood for.
|
||||||
|
*
|
||||||
|
* It travels as a composition local rather than as parameters because the pieces
|
||||||
|
* sit five levels below where the scopes exist, and because a null default is
|
||||||
|
* exactly the right meaning for everyone else: the paged, continuous, and dense
|
||||||
|
* styles share the same row and cell composables, provide nothing, and pay
|
||||||
|
* nothing. Reduced motion provides nothing either, which leaves the plain
|
||||||
|
* cross-fade underneath.
|
||||||
|
*/
|
||||||
|
internal sealed interface MonthMorphKey {
|
||||||
|
/** A day's background pill — the structural anchor the rest rides on. */
|
||||||
|
data class Cell(val date: LocalDate) : MonthMorphKey
|
||||||
|
|
||||||
|
data class DayNumber(val date: LocalDate) : MonthMorphKey
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One event *on one day*. Keyed by date as well as instance because a
|
||||||
|
* multi-day event has a dot on every day it covers but only one bar, drawn
|
||||||
|
* from where it starts: the start day's dot is the one that becomes the bar,
|
||||||
|
* and the rest are left unmatched on purpose. They fade where they stand
|
||||||
|
* while the bar sweeps out over them, which is the honest reading — one of
|
||||||
|
* them could not become the bar without the others teleporting into it.
|
||||||
|
*/
|
||||||
|
data class Event(val date: LocalDate, val instanceId: Long) : MonthMorphKey
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A day's "+N" marker. The compact grid writes it as a count beside the dots
|
||||||
|
* and the expanded one as a row of small dots under the bars, but they stand
|
||||||
|
* for the same events and appear on exactly the same days — both sides seat
|
||||||
|
* three and overflow the rest — so they are a matched pair, not two unrelated
|
||||||
|
* bits of text. Tagged, the marker travels with its day like everything else
|
||||||
|
* in the cell.
|
||||||
|
*/
|
||||||
|
data class Overflow(val date: LocalDate) : MonthMorphKey
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The selected day's outline. The expanded grid draws no outline — once the
|
||||||
|
* cells carry real event bars it is one mark too many — so this pairs the
|
||||||
|
* compact grid's outline with an *invisible* stand-in over the same cell
|
||||||
|
* there. Without one it had nowhere to travel and could only fade in place
|
||||||
|
* while everything around it moved.
|
||||||
|
*/
|
||||||
|
data class Selection(val date: LocalDate) : MonthMorphKey
|
||||||
|
|
||||||
|
/** The grab handle, which slides from the pane's seam to the foot of the grid. */
|
||||||
|
data object Handle : MonthMorphKey
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||||
|
internal class MonthMorphScope(
|
||||||
|
val shared: SharedTransitionScope,
|
||||||
|
val visibility: AnimatedVisibilityScope,
|
||||||
|
)
|
||||||
|
|
||||||
|
internal val LocalMonthMorph = compositionLocalOf<MonthMorphScope?> { null }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True while the grid is mid-morph, for content that has to stop clipping to let
|
||||||
|
* the travelling pieces through.
|
||||||
|
*
|
||||||
|
* A mark's two homes are in *different rows*: the dot for the 20th sits a third
|
||||||
|
* of the way down the compact grid, its bar most of the way down the expanded
|
||||||
|
* one. Clipped to the row it is arriving at, a mark spends the first half of its
|
||||||
|
* journey outside those bounds and is simply not drawn — so it appears from
|
||||||
|
* nowhere, part-way through, already near its destination. Rendering in place
|
||||||
|
* rather than in an overlay is what subjects them to that clip, and is worth
|
||||||
|
* keeping; the clip is what has to yield, and only while pieces are in flight.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||||
|
@Composable
|
||||||
|
internal fun morphInFlight(): Boolean {
|
||||||
|
val morph = LocalMonthMorph.current ?: return false
|
||||||
|
return morph.shared.isTransitionActive
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag content that is *the same thing* on both sides — a background pill, a day
|
||||||
|
* number — so it animates between its two positions and sizes.
|
||||||
|
*
|
||||||
|
* ### Why nothing renders in the overlay
|
||||||
|
*
|
||||||
|
* By default a travelling piece is painted into an overlay above the *entire*
|
||||||
|
* regular tree, so it can fly over anything in its way. That is right for a
|
||||||
|
* thumbnail crossing a screen, and wrong here: everything is moving inside one
|
||||||
|
* grid, and the overlay meant every untagged neighbour — the selection outline,
|
||||||
|
* the "+N" markers — spent the transition buried under pieces that had left the
|
||||||
|
* tree's z-order behind. Lifting each of them out in turn fixed the burying and
|
||||||
|
* bought a worse problem: they then floated over the grid on their own layer,
|
||||||
|
* out of step with it.
|
||||||
|
*
|
||||||
|
* Rendering in place puts everything back in one z-order and one clip, which is
|
||||||
|
* what makes the grid read as a single surface changing shape rather than a
|
||||||
|
* stack of pieces sliding past each other. Nothing here needs to escape its
|
||||||
|
* ancestors: the cells, marks and markers all travel within the grid.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||||
|
@Composable
|
||||||
|
internal fun Modifier.morphElement(key: MonthMorphKey): Modifier {
|
||||||
|
val morph = LocalMonthMorph.current ?: return this
|
||||||
|
return with(morph.shared) {
|
||||||
|
this@morphElement.sharedElement(
|
||||||
|
sharedContentState = rememberSharedContentState(key),
|
||||||
|
animatedVisibilityScope = morph.visibility,
|
||||||
|
renderInOverlayDuringTransition = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag content that means the same thing but *is drawn differently* on each side —
|
||||||
|
* a 5dp dot and a titled bar — so only the bounds are shared and the contents
|
||||||
|
* cross-fade inside them.
|
||||||
|
*
|
||||||
|
* [ResizeMode.RemeasureToBounds][SharedTransitionScope.ResizeMode] rather than
|
||||||
|
* scaling: a bar's title laid out at the dot's 5dp and then scaled up would
|
||||||
|
* arrive as a smear. Remeasuring keeps the text at its real size throughout and
|
||||||
|
* simply clips it while there is no room, so what grows is the pill, not the type.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||||
|
@Composable
|
||||||
|
internal fun Modifier.morphBounds(key: MonthMorphKey): Modifier {
|
||||||
|
val morph = LocalMonthMorph.current ?: return this
|
||||||
|
return with(morph.shared) {
|
||||||
|
this@morphBounds.sharedBounds(
|
||||||
|
sharedContentState = rememberSharedContentState(key),
|
||||||
|
animatedVisibilityScope = morph.visibility,
|
||||||
|
enter = fadeIn(),
|
||||||
|
exit = fadeOut(),
|
||||||
|
resizeMode = SharedTransitionScope.ResizeMode.RemeasureToBounds,
|
||||||
|
renderInOverlayDuringTransition = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
package de.jeanlucmakiola.calendula.ui.month
|
package de.jeanlucmakiola.calendula.ui.month
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||||
|
import androidx.compose.animation.SharedTransitionLayout
|
||||||
import androidx.compose.animation.core.RepeatMode
|
import androidx.compose.animation.core.RepeatMode
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.animation.core.snap
|
import androidx.compose.animation.core.snap
|
||||||
@@ -8,12 +11,13 @@ import androidx.compose.animation.core.animateFloat
|
|||||||
import androidx.compose.animation.core.infiniteRepeatable
|
import androidx.compose.animation.core.infiniteRepeatable
|
||||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.togetherWith
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
|
import androidx.compose.foundation.gestures.detectDragGestures
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -60,17 +64,23 @@ import androidx.compose.runtime.derivedStateOf
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.key
|
import androidx.compose.runtime.key
|
||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.clipToBounds
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
|
import androidx.compose.ui.draw.drawBehind
|
||||||
|
import androidx.compose.ui.geometry.CornerRadius
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
@@ -94,6 +104,7 @@ import de.jeanlucmakiola.calendula.ui.common.CalendarDrawer
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarFabColumn
|
import de.jeanlucmakiola.calendula.ui.common.CalendarFabColumn
|
||||||
import de.jeanlucmakiola.calendula.ui.common.TodayAction
|
import de.jeanlucmakiola.calendula.ui.common.TodayAction
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarFailure
|
import de.jeanlucmakiola.calendula.ui.common.CalendarFailure
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.CALENDAR_SWIPE_THRESHOLD
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
||||||
import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS
|
import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS
|
||||||
import de.jeanlucmakiola.calendula.ui.common.EventDimAlpha
|
import de.jeanlucmakiola.calendula.ui.common.EventDimAlpha
|
||||||
@@ -124,6 +135,7 @@ import kotlinx.datetime.plus
|
|||||||
import kotlinx.datetime.YearMonth
|
import kotlinx.datetime.YearMonth
|
||||||
import kotlinx.datetime.toJavaLocalDate
|
import kotlinx.datetime.toJavaLocalDate
|
||||||
import kotlinx.datetime.toLocalDateTime
|
import kotlinx.datetime.toLocalDateTime
|
||||||
|
import kotlin.math.abs
|
||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
import java.time.format.TextStyle as JavaTextStyle
|
import java.time.format.TextStyle as JavaTextStyle
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@@ -227,6 +239,19 @@ fun MonthScreen(
|
|||||||
.collect { (first, last) -> viewModel.onVisibleMonthsChanged(first, last) }
|
.collect { (first, last) -> viewModel.onVisibleMonthsChanged(first, last) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep the paged month tracking the scrolling grid. The scrolling styles
|
||||||
|
// navigate by scroll position and never touch the paged month otherwise, so
|
||||||
|
// without this it stays wherever paged navigation last left it (today's
|
||||||
|
// month, on a fresh open) — and switching to a paged style, or reseeding the
|
||||||
|
// other scrolling style's list state, which both read off it, would snap
|
||||||
|
// back there rather than staying on the month you were looking at.
|
||||||
|
LaunchedEffect(listState, scrolling, dense, weekStart) {
|
||||||
|
if (!scrolling) return@LaunchedEffect
|
||||||
|
snapshotFlow { visibleMonth }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.collect { viewModel.syncScrollMonth(it) }
|
||||||
|
}
|
||||||
|
|
||||||
val isOnCurrentMonth = titleMonth == YearMonth(today.year, today.month)
|
val isOnCurrentMonth = titleMonth == YearMonth(today.year, today.month)
|
||||||
|
|
||||||
// Continuous names the month on the block's own sticky header, so the bar
|
// Continuous names the month on the block's own sticky header, so the bar
|
||||||
@@ -285,6 +310,15 @@ fun MonthScreen(
|
|||||||
viewModel.goToDate(target)
|
viewModel.goToDate(target)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Selecting a day in the split grid can cross into a neighbour month — a
|
||||||
|
// tapped leading/trailing day follows to its own month. Point the slide the
|
||||||
|
// way the month is actually moving, so the incoming page travels the right
|
||||||
|
// direction instead of reusing whatever the last swipe left in slideDir.
|
||||||
|
val selectDay: (LocalDate) -> Unit = { date ->
|
||||||
|
val target = YearMonth(date.year, date.month)
|
||||||
|
if (target != month) slideDir = if (target < month) -1 else 1
|
||||||
|
viewModel.selectDate(date)
|
||||||
|
}
|
||||||
|
|
||||||
ModalNavigationDrawer(
|
ModalNavigationDrawer(
|
||||||
drawerState = drawerState,
|
drawerState = drawerState,
|
||||||
@@ -365,7 +399,7 @@ fun MonthScreen(
|
|||||||
onSwipeNext = goNext,
|
onSwipeNext = goNext,
|
||||||
onSwipePrev = goPrev,
|
onSwipePrev = goPrev,
|
||||||
onRetry = jumpToToday,
|
onRetry = jumpToToday,
|
||||||
onSelectDay = viewModel::selectDate,
|
onSelectDay = selectDay,
|
||||||
onOpenDay = onOpenDay,
|
onOpenDay = onOpenDay,
|
||||||
onEventClick = onEventClick,
|
onEventClick = onEventClick,
|
||||||
onCreateEvent = { onCreateEvent(it, null) },
|
onCreateEvent = { onCreateEvent(it, null) },
|
||||||
@@ -549,7 +583,13 @@ private val WEEK_NUMBER_GUTTER = 40.dp
|
|||||||
private val DAY_NUMBER_GAP = 4.dp
|
private val DAY_NUMBER_GAP = 4.dp
|
||||||
private val CELL_TOP_PADDING = 6.dp
|
private val CELL_TOP_PADDING = 6.dp
|
||||||
private val CELL_GAP = 2.dp
|
private val CELL_GAP = 2.dp
|
||||||
private val CELL_SHAPE = RoundedCornerShape(12.dp)
|
/** Named separately because the split style's selection outline draws its own
|
||||||
|
* rounded rect and has to match this radius exactly. */
|
||||||
|
private val CELL_CORNER = 12.dp
|
||||||
|
private val CELL_SHAPE = RoundedCornerShape(CELL_CORNER)
|
||||||
|
|
||||||
|
/** Width of the split style's selected-day outline. */
|
||||||
|
private val SPLIT_SELECTION_STROKE = 1.5.dp
|
||||||
private const val MAX_EVENT_ROWS = 3
|
private const val MAX_EVENT_ROWS = 3
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -573,6 +613,8 @@ internal fun MonthGrid(
|
|||||||
state: MonthUiState.Success,
|
state: MonthUiState.Success,
|
||||||
showWeekNumbers: Boolean,
|
showWeekNumbers: Boolean,
|
||||||
onOpenDay: (LocalDate) -> Unit,
|
onOpenDay: (LocalDate) -> Unit,
|
||||||
|
/** See [MonthWeekRow]'s `selected`: an anchor for the morph, never a mark. */
|
||||||
|
selected: LocalDate? = null,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -591,6 +633,7 @@ internal fun MonthGrid(
|
|||||||
inMonth = { it.month == month.month && it.year == month.year },
|
inMonth = { it.month == month.month && it.year == month.year },
|
||||||
showWeekNumbers = showWeekNumbers,
|
showWeekNumbers = showWeekNumbers,
|
||||||
onOpenDay = onOpenDay,
|
onOpenDay = onOpenDay,
|
||||||
|
selected = selected,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(1f),
|
.weight(1f),
|
||||||
@@ -781,51 +824,119 @@ internal fun DenseMonthGrid(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Which way a drag went, decided once per gesture and then held. */
|
||||||
|
private enum class DragAxis { Undecided, Horizontal, Vertical }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The month-changing horizontal swipe, shared by the paged and split styles.
|
* The month grid's drag gesture: horizontal pages the month, vertical expands or
|
||||||
* Accumulates the drag and commits past a threshold on release — the grid
|
* collapses the split style (#53). The grid doesn't follow the finger, so there is
|
||||||
* doesn't follow the finger, so there is no distance to rubber-band against.
|
* no distance to rubber-band against — it commits the moment the accumulated drag
|
||||||
|
* clears the threshold, once per gesture, exactly as
|
||||||
|
* [rememberCalendarPageSwipe][de.jeanlucmakiola.calendula.ui.common.rememberCalendarPageSwipe]
|
||||||
|
* does for the week and day views.
|
||||||
*
|
*
|
||||||
* The threshold matches the week and day views'. It used to be 6dp, which is
|
* This is the month's own detector rather than that shared one because it needs
|
||||||
* inside the distance a tap wanders: brushing the grid changed the month, and a
|
* two axes, and the axis is **locked on the first movement and held for the whole
|
||||||
* page that turns on an unintended gesture reads as the animation misfiring
|
* gesture** so a drag can page or expand but never both. Two independent
|
||||||
* rather than as the gesture being over-eager.
|
* detectors on one surface would each see their own component of a diagonal drag
|
||||||
|
* and both fire.
|
||||||
|
*
|
||||||
|
* The horizontal threshold is the shared one. The vertical is larger — swapping
|
||||||
|
* the whole layout out deserves a more deliberate pull than stepping to the next
|
||||||
|
* month.
|
||||||
|
*
|
||||||
|
* [onExpand]/[onCollapse] are null for the paged style, which leaves the vertical
|
||||||
|
* axis unclaimed: the lock still happens, so a vertical drag there does nothing
|
||||||
|
* rather than being re-read as a page turn.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
private fun rememberMonthSwipeModifier(
|
private fun rememberMonthSwipeModifier(
|
||||||
onSwipeNext: () -> Unit,
|
onSwipeNext: () -> Unit,
|
||||||
onSwipePrev: () -> Unit,
|
onSwipePrev: () -> Unit,
|
||||||
|
onExpand: (() -> Unit)? = null,
|
||||||
|
onCollapse: (() -> Unit)? = null,
|
||||||
): Modifier {
|
): Modifier {
|
||||||
val threshold = with(LocalDensity.current) { MONTH_SWIPE_THRESHOLD.toPx() }
|
val density = LocalDensity.current
|
||||||
var dragAccum by remember { mutableFloatStateOf(0f) }
|
val pageThreshold = with(density) { CALENDAR_SWIPE_THRESHOLD.toPx() }
|
||||||
return Modifier.pointerInput(Unit) {
|
val expandThreshold = with(density) { MONTH_EXPAND_THRESHOLD.toPx() }
|
||||||
detectHorizontalDragGestures(
|
return Modifier.pointerInput(onSwipeNext, onSwipePrev, onExpand, onCollapse) {
|
||||||
onDragStart = { dragAccum = 0f },
|
var accum = Offset.Zero
|
||||||
onDragEnd = {
|
var axis = DragAxis.Undecided
|
||||||
when {
|
var fired = false
|
||||||
dragAccum < -threshold -> onSwipeNext()
|
detectDragGestures(
|
||||||
dragAccum > threshold -> onSwipePrev()
|
onDragStart = {
|
||||||
}
|
accum = Offset.Zero
|
||||||
dragAccum = 0f
|
axis = DragAxis.Undecided
|
||||||
|
fired = false
|
||||||
|
},
|
||||||
|
onDragEnd = {
|
||||||
|
accum = Offset.Zero
|
||||||
|
axis = DragAxis.Undecided
|
||||||
|
fired = false
|
||||||
|
},
|
||||||
|
onDragCancel = {
|
||||||
|
accum = Offset.Zero
|
||||||
|
axis = DragAxis.Undecided
|
||||||
|
fired = false
|
||||||
|
},
|
||||||
|
onDrag = { _, drag ->
|
||||||
|
accum += drag
|
||||||
|
if (axis == DragAxis.Undecided) {
|
||||||
|
// Ties go horizontal, keeping paging the default reading of an
|
||||||
|
// ambiguous drag as it was before the vertical axis existed.
|
||||||
|
axis = if (abs(accum.x) >= abs(accum.y)) {
|
||||||
|
DragAxis.Horizontal
|
||||||
|
} else {
|
||||||
|
DragAxis.Vertical
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Both axes commit the instant the drag clears their threshold,
|
||||||
|
// rather than on release — see [rememberCalendarPageSwipe], which
|
||||||
|
// does the same for the week and day views.
|
||||||
|
if (!fired) {
|
||||||
|
val commit = when (axis) {
|
||||||
|
DragAxis.Horizontal -> when {
|
||||||
|
accum.x < -pageThreshold -> onSwipeNext
|
||||||
|
accum.x > pageThreshold -> onSwipePrev
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
DragAxis.Vertical -> when {
|
||||||
|
accum.y > expandThreshold -> onExpand
|
||||||
|
accum.y < -expandThreshold -> onCollapse
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
DragAxis.Undecided -> null
|
||||||
|
}
|
||||||
|
if (commit != null) {
|
||||||
|
fired = true
|
||||||
|
commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onDragCancel = { dragAccum = 0f },
|
|
||||||
onHorizontalDrag = { _, drag -> dragAccum += drag },
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Drag distance that commits a month change, matching the week and day views. */
|
/** Drag distance that commits an expand/collapse — deliberately longer than a page. */
|
||||||
private val MONTH_SWIPE_THRESHOLD = 24.dp
|
private val MONTH_EXPAND_THRESHOLD = 48.dp
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Split style content: the compact grid keeps the month swipe, the pane below it
|
* Split style content: the compact grid keeps the month swipe, the pane below it
|
||||||
* lists whatever day is selected.
|
* lists whatever day is selected — and a downward drag trades the pane away for
|
||||||
|
* the full paged grid, an upward one brings it back (#53).
|
||||||
*
|
*
|
||||||
* The grid slides between months like the paged style, which it can only do
|
* The grid slides between months like the paged style, which it can only do
|
||||||
* because it always reserves [SPLIT_GRID_ROWS] rows. Sized to its own month it
|
* because it always reserves [SPLIT_GRID_ROWS] rows. Sized to its own month it
|
||||||
* stood 4–6 rows tall, so every swipe shunted the pane up or down by a row on
|
* stood 4–6 rows tall, so every swipe shunted the pane up or down by a row on
|
||||||
* top of swapping the grid — the pane now holds still and only the grid moves.
|
* top of swapping the grid — the pane now holds still and only the grid moves.
|
||||||
|
*
|
||||||
|
* Expansion is deliberately **not** a stored preference. It is a way to look at
|
||||||
|
* the month you are on, not a fourth style; persisted, someone would expand it
|
||||||
|
* once and later find their Split style permanently changed with nothing on
|
||||||
|
* screen to explain why. [rememberSaveable] carries it across a rotation, which
|
||||||
|
* is as long as it should live.
|
||||||
*/
|
*/
|
||||||
|
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun SplitMonthContent(
|
private fun SplitMonthContent(
|
||||||
state: MonthUiState,
|
state: MonthUiState,
|
||||||
@@ -840,18 +951,137 @@ private fun SplitMonthContent(
|
|||||||
onEventClick: (EventInstance) -> Unit,
|
onEventClick: (EventInstance) -> Unit,
|
||||||
onCreateEvent: (LocalDate) -> Unit,
|
onCreateEvent: (LocalDate) -> Unit,
|
||||||
) {
|
) {
|
||||||
val slideSpec = rememberCalendarSlideSpec()
|
|
||||||
val fadeSpec = rememberCalendarFadeSpec()
|
val fadeSpec = rememberCalendarFadeSpec()
|
||||||
val reduceMotion = rememberReduceMotion()
|
val reduceMotion = rememberReduceMotion()
|
||||||
|
var expanded by rememberSaveable { mutableStateOf(false) }
|
||||||
|
// Back collapses before it does anything else. Expanding replaces the whole
|
||||||
|
// screen, so it is a place you can be, and every other place in the app can
|
||||||
|
// be backed out of. Declared deeper than CalendarHost's view-stack handler,
|
||||||
|
// which is what makes it win while it is enabled.
|
||||||
|
BackHandler(enabled = expanded) { expanded = false }
|
||||||
// The swipe wraps the grid rather than living inside it: mid-transition
|
// The swipe wraps the grid rather than living inside it: mid-transition
|
||||||
// there are two grids, and the gesture belongs to neither. The pane is left
|
// there are two grids, and the gesture belongs to neither. The pane is left
|
||||||
// out of it — it scrolls and is full of tappable rows.
|
// out of it — it scrolls and is full of tappable rows.
|
||||||
val swipeModifier = rememberMonthSwipeModifier(onSwipeNext, onSwipePrev)
|
val swipeModifier = rememberMonthSwipeModifier(
|
||||||
|
onSwipeNext = onSwipeNext,
|
||||||
|
onSwipePrev = onSwipePrev,
|
||||||
|
onExpand = { expanded = true },
|
||||||
|
onCollapse = { expanded = false },
|
||||||
|
)
|
||||||
|
|
||||||
when (state) {
|
when (state) {
|
||||||
MonthUiState.Loading -> MonthGridLoading()
|
MonthUiState.Loading -> MonthGridLoading()
|
||||||
is MonthUiState.Failure -> CalendarFailure(reason = state.reason, onRetry = onRetry)
|
is MonthUiState.Failure -> CalendarFailure(reason = state.reason, onRetry = onRetry)
|
||||||
is MonthUiState.Success -> Column(Modifier.fillMaxSize()) {
|
is MonthUiState.Success -> SharedTransitionLayout(Modifier.fillMaxSize()) {
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = expanded,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
// Both branches fill the same box — the grid grows into exactly the
|
||||||
|
// room the pane gives up — so there is no size change to contain.
|
||||||
|
// The travel between them is the shared elements' job, not a slide.
|
||||||
|
transitionSpec = { fadeIn(fadeSpec) togetherWith fadeOut(fadeSpec) },
|
||||||
|
label = "split-expand-transition",
|
||||||
|
) { isExpanded ->
|
||||||
|
CompositionLocalProvider(
|
||||||
|
// Null under reduced motion: nothing is tagged, nothing
|
||||||
|
// travels, and the cross-fade above is the whole transition.
|
||||||
|
LocalMonthMorph provides if (reduceMotion) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
MonthMorphScope(this@SharedTransitionLayout, this@AnimatedContent)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
SplitMonthBody(
|
||||||
|
expanded = isExpanded,
|
||||||
|
state = state,
|
||||||
|
selected = selected,
|
||||||
|
slideDir = slideDir,
|
||||||
|
showWeekNumbers = showWeekNumbers,
|
||||||
|
swipeModifier = swipeModifier,
|
||||||
|
onSelectDay = onSelectDay,
|
||||||
|
onOpenDay = onOpenDay,
|
||||||
|
onEventClick = onEventClick,
|
||||||
|
onCreateEvent = onCreateEvent,
|
||||||
|
onSetExpanded = { expanded = it },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The two faces of the split style, sharing one set of morph tags. */
|
||||||
|
@Composable
|
||||||
|
private fun SplitMonthBody(
|
||||||
|
expanded: Boolean,
|
||||||
|
state: MonthUiState.Success,
|
||||||
|
selected: LocalDate,
|
||||||
|
slideDir: Int,
|
||||||
|
showWeekNumbers: Boolean,
|
||||||
|
swipeModifier: Modifier,
|
||||||
|
onSelectDay: (LocalDate) -> Unit,
|
||||||
|
onOpenDay: (LocalDate) -> Unit,
|
||||||
|
onEventClick: (EventInstance) -> Unit,
|
||||||
|
onCreateEvent: (LocalDate) -> Unit,
|
||||||
|
onSetExpanded: (Boolean) -> Unit,
|
||||||
|
) {
|
||||||
|
if (expanded) {
|
||||||
|
SplitMonthExpanded(
|
||||||
|
state = state,
|
||||||
|
selected = selected,
|
||||||
|
slideDir = slideDir,
|
||||||
|
showWeekNumbers = showWeekNumbers,
|
||||||
|
swipeModifier = swipeModifier,
|
||||||
|
// A tap in the expanded grid picks the day and drops back, which
|
||||||
|
// gives the expanded month a job — a chooser you dip into — rather
|
||||||
|
// than a mode you can get stranded in. The collapse then runs with
|
||||||
|
// the selection already set, so the pane arrives showing the day
|
||||||
|
// you picked.
|
||||||
|
onPickDay = {
|
||||||
|
onSelectDay(it)
|
||||||
|
onSetExpanded(false)
|
||||||
|
},
|
||||||
|
onCollapse = { onSetExpanded(false) },
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
SplitMonthCollapsed(
|
||||||
|
state = state,
|
||||||
|
selected = selected,
|
||||||
|
slideDir = slideDir,
|
||||||
|
showWeekNumbers = showWeekNumbers,
|
||||||
|
swipeModifier = swipeModifier,
|
||||||
|
onSelectDay = onSelectDay,
|
||||||
|
onOpenDay = onOpenDay,
|
||||||
|
onEventClick = onEventClick,
|
||||||
|
onCreateEvent = onCreateEvent,
|
||||||
|
onExpand = { onSetExpanded(true) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The split style at rest: compact grid, handle, then the selected day's events. */
|
||||||
|
@Composable
|
||||||
|
private fun SplitMonthCollapsed(
|
||||||
|
state: MonthUiState.Success,
|
||||||
|
selected: LocalDate,
|
||||||
|
slideDir: Int,
|
||||||
|
showWeekNumbers: Boolean,
|
||||||
|
swipeModifier: Modifier,
|
||||||
|
onSelectDay: (LocalDate) -> Unit,
|
||||||
|
onOpenDay: (LocalDate) -> Unit,
|
||||||
|
onEventClick: (EventInstance) -> Unit,
|
||||||
|
onCreateEvent: (LocalDate) -> Unit,
|
||||||
|
onExpand: () -> Unit,
|
||||||
|
) {
|
||||||
|
val slideSpec = rememberCalendarSlideSpec()
|
||||||
|
val fadeSpec = rememberCalendarFadeSpec()
|
||||||
|
val reduceMotion = rememberReduceMotion()
|
||||||
|
|
||||||
|
Column(Modifier.fillMaxSize()) {
|
||||||
|
// Grid and handle drag as one surface — the handle is what advertises the
|
||||||
|
// gesture, so it has to answer to it as well as to a tap. The pane is
|
||||||
|
// outside: it scrolls, and is full of tappable rows.
|
||||||
|
Column(swipeModifier) {
|
||||||
AnimatedContent(
|
AnimatedContent(
|
||||||
// The selection travels *with* the state so each page keeps its
|
// The selection travels *with* the state so each page keeps its
|
||||||
// own. Read from outside, both pages would show the incoming
|
// own. Read from outside, both pages would show the incoming
|
||||||
@@ -859,7 +1089,6 @@ private fun SplitMonthContent(
|
|||||||
// grid — onto the new month's 1st, which the old grid still
|
// grid — onto the new month's 1st, which the old grid still
|
||||||
// shows among its trailing days — before the new page arrived.
|
// shows among its trailing days — before the new page arrived.
|
||||||
targetState = state to selected,
|
targetState = state to selected,
|
||||||
modifier = swipeModifier,
|
|
||||||
// Keyed on the month alone, so a provider notification refreshing
|
// Keyed on the month alone, so a provider notification refreshing
|
||||||
// the month you are on — or a tap moving the selection within it
|
// the month you are on — or a tap moving the selection within it
|
||||||
// — updates in place instead of sliding.
|
// — updates in place instead of sliding.
|
||||||
@@ -876,20 +1105,105 @@ private fun SplitMonthContent(
|
|||||||
onSelectDay = onSelectDay,
|
onSelectDay = onSelectDay,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
SplitDayPane(
|
SplitExpandHandle(expanded = false, onToggle = onExpand)
|
||||||
date = selected,
|
}
|
||||||
today = state.today,
|
SplitDayPane(
|
||||||
// Null, not empty: the selection moves to the new month before
|
date = selected,
|
||||||
// its data arrives, and a missing key means "not loaded yet".
|
today = state.today,
|
||||||
// Passing an empty list would claim the day was free.
|
// Null, not empty: the selection moves to the new month before
|
||||||
events = state.instancesByDay[selected],
|
// its data arrives, and a missing key means "not loaded yet".
|
||||||
zone = state.zone,
|
// Passing an empty list would claim the day was free.
|
||||||
onOpenDay = onOpenDay,
|
events = state.instancesByDay[selected],
|
||||||
onEventClick = onEventClick,
|
zone = state.zone,
|
||||||
onCreateEvent = onCreateEvent,
|
onOpenDay = onOpenDay,
|
||||||
modifier = Modifier.weight(1f).fillMaxWidth(),
|
onEventClick = onEventClick,
|
||||||
|
onCreateEvent = onCreateEvent,
|
||||||
|
modifier = Modifier.weight(1f).fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The split style pulled open: the pane is gone and the month gets the whole
|
||||||
|
* screen in the paged style's own vocabulary — real event bars and pills instead
|
||||||
|
* of dots. The handle stays, now at the foot of the grid, to pull it back.
|
||||||
|
*
|
||||||
|
* No selection marker here, deliberately. Once the cells carry real event bars
|
||||||
|
* the outline is one mark too many, and it has nothing to say: every day is a tap
|
||||||
|
* away from being the selected one, and tapping is what closes this view.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun SplitMonthExpanded(
|
||||||
|
state: MonthUiState.Success,
|
||||||
|
/** Marked nowhere here; it only anchors the outline's morph. */
|
||||||
|
selected: LocalDate,
|
||||||
|
slideDir: Int,
|
||||||
|
showWeekNumbers: Boolean,
|
||||||
|
swipeModifier: Modifier,
|
||||||
|
onPickDay: (LocalDate) -> Unit,
|
||||||
|
onCollapse: () -> Unit,
|
||||||
|
) {
|
||||||
|
val slideSpec = rememberCalendarSlideSpec()
|
||||||
|
val fadeSpec = rememberCalendarFadeSpec()
|
||||||
|
val reduceMotion = rememberReduceMotion()
|
||||||
|
|
||||||
|
// The whole screen drags here, handle included — there is no pane to keep out
|
||||||
|
// of it, and the handle is the obvious thing to reach for on the way back.
|
||||||
|
Column(Modifier.fillMaxSize().then(swipeModifier)) {
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = state to selected,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
contentKey = { (s, _) -> s.month },
|
||||||
|
transitionSpec = {
|
||||||
|
calendarSlideTransition(slideDir, slideSpec, fadeSpec, reduceMotion)
|
||||||
|
},
|
||||||
|
label = "split-expanded-month-transition",
|
||||||
|
) { (s, sel) ->
|
||||||
|
MonthGrid(
|
||||||
|
state = s,
|
||||||
|
showWeekNumbers = showWeekNumbers,
|
||||||
|
onOpenDay = onPickDay,
|
||||||
|
selected = sel,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
SplitExpandHandle(expanded = true, onToggle = onCollapse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The grab handle at the seam between grid and pane — the same M3 drag-handle
|
||||||
|
* pill a bottom sheet uses, for the same reason: it advertises that the surface
|
||||||
|
* moves.
|
||||||
|
*
|
||||||
|
* The drag itself lives on the grid, not here. This exists so the gesture is
|
||||||
|
* findable at all, and it takes taps too — a hidden swipe is no use to someone
|
||||||
|
* who never tries it, or who can't make the gesture.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun SplitExpandHandle(
|
||||||
|
expanded: Boolean,
|
||||||
|
onToggle: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val label = stringResource(
|
||||||
|
if (expanded) R.string.month_split_collapse else R.string.month_split_expand,
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(SPLIT_HANDLE_ROW_HEIGHT)
|
||||||
|
.clickable(onClick = onToggle)
|
||||||
|
.semantics { contentDescription = label },
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.size(width = SPLIT_HANDLE_WIDTH, height = SPLIT_HANDLE_HEIGHT)
|
||||||
|
// Tagged too, so it slides from the pane's seam down to the foot
|
||||||
|
// of the grid rather than blinking out at one and in at the other.
|
||||||
|
.morphElement(MonthMorphKey.Handle)
|
||||||
|
.background(MaterialTheme.colorScheme.outlineVariant, CircleShape),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -902,7 +1216,9 @@ private fun SplitMonthContent(
|
|||||||
*/
|
*/
|
||||||
private val SPLIT_ROW_HEIGHT = 46.dp
|
private val SPLIT_ROW_HEIGHT = 46.dp
|
||||||
private val SPLIT_DOT_SIZE = 5.dp
|
private val SPLIT_DOT_SIZE = 5.dp
|
||||||
private const val SPLIT_MAX_DOTS = 3
|
// Dots are capped by MAX_EVENT_ROWS, not a constant of their own: they stand for
|
||||||
|
// the paged grid's lanes, so the two caps have to be the same number or a dot
|
||||||
|
// would have no bar to become (#53).
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rows the split grid always reserves — the most any month needs. A month that
|
* Rows the split grid always reserves — the most any month needs. A month that
|
||||||
@@ -911,6 +1227,14 @@ private const val SPLIT_MAX_DOTS = 3
|
|||||||
*/
|
*/
|
||||||
private const val SPLIT_GRID_ROWS = 6
|
private const val SPLIT_GRID_ROWS = 6
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The expand handle: M3's drag-handle pill (32×4dp), in a row tall enough to be a
|
||||||
|
* comfortable tap target on its own.
|
||||||
|
*/
|
||||||
|
private val SPLIT_HANDLE_WIDTH = 32.dp
|
||||||
|
private val SPLIT_HANDLE_HEIGHT = 4.dp
|
||||||
|
private val SPLIT_HANDLE_ROW_HEIGHT = 24.dp
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The split style's grid (#53): the month compressed to day numbers and event
|
* The split style's grid (#53): the month compressed to day numbers and event
|
||||||
* dots, with the selected day listed underneath by [SplitDayPane].
|
* dots, with the selected day listed underneath by [SplitDayPane].
|
||||||
@@ -945,11 +1269,15 @@ internal fun SplitMonthGrid(
|
|||||||
.fillMaxHeight(),
|
.fillMaxHeight(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
week.days.forEach { day ->
|
week.days.forEachIndexed { col, day ->
|
||||||
val inMonth = day.month == month.month && day.year == month.year
|
val inMonth = day.month == month.month && day.year == month.year
|
||||||
|
// Seated by lane rather than gathered by colour, so each dot
|
||||||
|
// is the event the expanded grid draws in that same lane.
|
||||||
|
val seated = week.laneEvents(col, day, MAX_EVENT_ROWS)
|
||||||
SplitDayCell(
|
SplitDayCell(
|
||||||
date = day,
|
date = day,
|
||||||
events = state.instancesByDay[day].orEmpty(),
|
events = seated,
|
||||||
|
hidden = (week.countByDay[day] ?: 0) - seated.size,
|
||||||
isToday = day == state.today,
|
isToday = day == state.today,
|
||||||
// A page marks only the days its own month owns. Paging
|
// A page marks only the days its own month owns. Paging
|
||||||
// moves the selection before this month's replacement
|
// moves the selection before this month's replacement
|
||||||
@@ -979,7 +1307,7 @@ internal fun SplitMonthGrid(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* One compact day: its number over up to [SPLIT_MAX_DOTS] event dots.
|
* One compact day: its number over up to [MAX_EVENT_ROWS] lane-seated event dots.
|
||||||
*
|
*
|
||||||
* Selection and today are deliberately different signals — a tinted, outlined
|
* Selection and today are deliberately different signals — a tinted, outlined
|
||||||
* cell versus the filled circle the other views already use for today — so the
|
* cell versus the filled circle the other views already use for today — so the
|
||||||
@@ -989,6 +1317,8 @@ internal fun SplitMonthGrid(
|
|||||||
private fun SplitDayCell(
|
private fun SplitDayCell(
|
||||||
date: LocalDate,
|
date: LocalDate,
|
||||||
events: List<EventInstance>,
|
events: List<EventInstance>,
|
||||||
|
/** Events on this day that didn't fit a lane, shown as a "+N" beside the dots. */
|
||||||
|
hidden: Int,
|
||||||
isToday: Boolean,
|
isToday: Boolean,
|
||||||
isSelected: Boolean,
|
isSelected: Boolean,
|
||||||
inMonth: Boolean,
|
inMonth: Boolean,
|
||||||
@@ -1006,26 +1336,81 @@ private fun SplitDayCell(
|
|||||||
// Each cell fades its own outline, so moving the selection reads as one
|
// Each cell fades its own outline, so moving the selection reads as one
|
||||||
// mark crossing the grid rather than as an outline blinking out here and
|
// mark crossing the grid rather than as an outline blinking out here and
|
||||||
// reappearing there. Snapped under reduced motion.
|
// reappearing there. Snapped under reduced motion.
|
||||||
|
//
|
||||||
|
// Held as a State and read *inside the draw lambda below*, never unwrapped
|
||||||
|
// here. Read in composition it made every frame of the fade recompose all
|
||||||
|
// 42 cells at once, which is what made the outline shiver rather than fade.
|
||||||
|
// Kept in the draw phase, the animation touches nothing but the pixels.
|
||||||
val reduceMotion = rememberReduceMotion()
|
val reduceMotion = rememberReduceMotion()
|
||||||
val fadeSpec = rememberCalendarFadeSpec()
|
// The outline runs on the *fast* effects spec, unlike everything else here.
|
||||||
val selection by animateFloatAsState(
|
// It is a small mark that only ever appears or disappears, and at the shared
|
||||||
|
// pace it lingered after the thing it marks had already moved.
|
||||||
|
val outlineSpec = MaterialTheme.motionScheme.fastEffectsSpec<Float>()
|
||||||
|
val selection = animateFloatAsState(
|
||||||
targetValue = if (isSelected) 1f else 0f,
|
targetValue = if (isSelected) 1f else 0f,
|
||||||
animationSpec = if (reduceMotion) snap() else fadeSpec,
|
animationSpec = if (reduceMotion) snap() else outlineSpec,
|
||||||
label = "split-day-selection",
|
label = "split-day-selection",
|
||||||
)
|
)
|
||||||
|
val outlineColor = MaterialTheme.colorScheme.primary
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val outlineStroke = with(density) { SPLIT_SELECTION_STROKE.toPx() }
|
||||||
|
val outlineRadius = with(density) { CELL_CORNER.toPx() }
|
||||||
|
// Background and content are separate layers, mirroring how the paged row is
|
||||||
|
// built — and so the pill can be tagged for the morph on its own. Tagged as
|
||||||
|
// one piece with its contents inside, the dots would be nested shared
|
||||||
|
// elements within a shared element and travel twice.
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.padding(horizontal = CELL_GAP, vertical = 1.dp)
|
.padding(horizontal = CELL_GAP, vertical = 1.dp)
|
||||||
.clip(CELL_SHAPE)
|
|
||||||
.background(background)
|
|
||||||
.border(
|
|
||||||
width = 1.5.dp,
|
|
||||||
color = MaterialTheme.colorScheme.primary.copy(alpha = selection),
|
|
||||||
shape = CELL_SHAPE,
|
|
||||||
)
|
|
||||||
.selectable(selected = isSelected, onClick = onClick),
|
.selectable(selected = isSelected, onClick = onClick),
|
||||||
contentAlignment = Alignment.TopCenter,
|
contentAlignment = Alignment.TopCenter,
|
||||||
) {
|
) {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.morphElement(MonthMorphKey.Cell(date))
|
||||||
|
.clip(CELL_SHAPE)
|
||||||
|
.background(background),
|
||||||
|
)
|
||||||
|
// The outline is a layer of its own, deliberately *untagged*. Drawn on the
|
||||||
|
// morphing pill it rode the cell's shared bounds, so collapsing painted it
|
||||||
|
// at the expanded cell's size and shrank it down — a full-height outline
|
||||||
|
// flashing over a grid that no longer had full-height cells. Untagged it
|
||||||
|
// is laid out where the collapsed cell actually is and simply fades in,
|
||||||
|
// which is the only size it is ever true at.
|
||||||
|
//
|
||||||
|
// Tagged only while it is the selected day, and paired with an invisible
|
||||||
|
// stand-in over the same cell in the expanded grid, so it travels with its
|
||||||
|
// cell instead of fading in place. Tagged unconditionally it would put a
|
||||||
|
// key on all 42 cells against the one partner the expanded grid offers,
|
||||||
|
// and the other 41 would fly in from the layout origin.
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.then(
|
||||||
|
if (isSelected) {
|
||||||
|
Modifier.morphBounds(MonthMorphKey.Selection(date))
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.drawBehind {
|
||||||
|
val alpha = selection.value
|
||||||
|
if (alpha <= 0f) return@drawBehind
|
||||||
|
// Inset by half the stroke: a stroke straddles the path it
|
||||||
|
// follows, so drawn on the bounds themselves the outer half
|
||||||
|
// would fall outside the cell and be clipped to a hairline
|
||||||
|
// along the edges.
|
||||||
|
val inset = outlineStroke / 2f
|
||||||
|
drawRoundRect(
|
||||||
|
color = outlineColor.copy(alpha = alpha),
|
||||||
|
topLeft = Offset(inset, inset),
|
||||||
|
size = Size(size.width - outlineStroke, size.height - outlineStroke),
|
||||||
|
cornerRadius = CornerRadius(outlineRadius - inset),
|
||||||
|
style = Stroke(width = outlineStroke),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxSize().padding(top = 4.dp),
|
modifier = Modifier.fillMaxSize().padding(top = 4.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
@@ -1034,37 +1419,60 @@ private fun SplitDayCell(
|
|||||||
date = date,
|
date = date,
|
||||||
isToday = isToday,
|
isToday = isToday,
|
||||||
inMonth = inMonth,
|
inMonth = inMonth,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth().morphElement(MonthMorphKey.DayNumber(date)),
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(2.dp))
|
Spacer(Modifier.height(2.dp))
|
||||||
SplitDots(events = events, dark = dark)
|
SplitDots(date = date, events = events, hidden = hidden, dark = dark)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Up to three colour dots for a day, plus a count when more are hidden. */
|
/**
|
||||||
|
* One dot per seated event, in lane order, plus a "+N" for those that didn't fit.
|
||||||
|
*
|
||||||
|
* Deliberately *not* de-duplicated by colour: [events] arrives lane-seated so the
|
||||||
|
* dots line up with the bars the expanded grid draws, and collapsing two events
|
||||||
|
* that share a calendar into one dot would both undercount the day and leave a
|
||||||
|
* bar with no dot to grow out of.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
private fun SplitDots(events: List<EventInstance>, dark: Boolean) {
|
private fun SplitDots(date: LocalDate, events: List<EventInstance>, hidden: Int, dark: Boolean) {
|
||||||
if (events.isEmpty()) return
|
if (events.isEmpty()) return
|
||||||
val soften = LocalSoftenColors.current
|
val soften = LocalSoftenColors.current
|
||||||
val colors = remember(events) { events.map { it.color }.distinct().take(SPLIT_MAX_DOTS) }
|
|
||||||
val extra = events.size - colors.size
|
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
colors.forEach { argb ->
|
events.forEach { event ->
|
||||||
|
// The dot keeps its own circle and the bar its 4dp corners; they
|
||||||
|
// cross-fade inside shared bounds, and at the dot's size the two
|
||||||
|
// radii are a fraction of a pixel apart.
|
||||||
|
//
|
||||||
|
// morphBounds goes *outside* the size: the shared bounds have to be
|
||||||
|
// free to drive the measurement. Behind a fixed .size() the dot stayed
|
||||||
|
// 5dp for the whole transition however far its bar had travelled, and
|
||||||
|
// only snapped to full width once the transition ended — the growth
|
||||||
|
// never happened, it was clamped away.
|
||||||
|
//
|
||||||
|
// *Every* dot is tagged, including those in the middle of a multi-day
|
||||||
|
// run: the expanded grid gives each covered column its own anchor
|
||||||
|
// inside the bar, so each one has a real place to come from.
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.morphBounds(MonthMorphKey.Event(date, event.instanceId))
|
||||||
.size(SPLIT_DOT_SIZE)
|
.size(SPLIT_DOT_SIZE)
|
||||||
.background(eventFill(argb, dark, soften), CircleShape),
|
.background(eventFill(event.color, dark, soften), CircleShape),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (extra > 0) {
|
if (hidden > 0) {
|
||||||
|
// Tagged, not lifted: this count and the expanded grid's dot row are
|
||||||
|
// the same marker on the same day, so it travels with its cell like
|
||||||
|
// everything else rather than riding above the grid on its own layer.
|
||||||
Text(
|
Text(
|
||||||
text = "+$extra",
|
text = "+$hidden",
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.morphBounds(MonthMorphKey.Overflow(date)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1277,10 +1685,18 @@ private fun MonthWeekRow(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
blankOutside: Boolean = false,
|
blankOutside: Boolean = false,
|
||||||
labelMonthOnFirst: Boolean = false,
|
labelMonthOnFirst: Boolean = false,
|
||||||
|
/**
|
||||||
|
* The selected day, when there is one. Draws *nothing* — it only places the
|
||||||
|
* invisible counterpart the compact grid's outline morphs against. Null for
|
||||||
|
* every style but the split style's expanded form, which composes no extra
|
||||||
|
* layer at all.
|
||||||
|
*/
|
||||||
|
selected: LocalDate? = null,
|
||||||
) {
|
) {
|
||||||
val dark = isSystemInDarkTheme()
|
val dark = isSystemInDarkTheme()
|
||||||
val laneCount = (week.spans.maxOfOrNull { it.lane } ?: -1) + 1
|
val laneCount = (week.spans.maxOfOrNull { it.lane } ?: -1) + 1
|
||||||
val shownLanes = laneCount.coerceAtMost(MAX_EVENT_ROWS)
|
val shownLanes = laneCount.coerceAtMost(MAX_EVENT_ROWS)
|
||||||
|
val morphing = morphInFlight()
|
||||||
|
|
||||||
Row(modifier) {
|
Row(modifier) {
|
||||||
// Optional calendar-week gutter, sized so the seven day columns below
|
// Optional calendar-week gutter, sized so the seven day columns below
|
||||||
@@ -1312,6 +1728,7 @@ private fun MonthWeekRow(
|
|||||||
.weight(1f)
|
.weight(1f)
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.padding(horizontal = CELL_GAP, vertical = 1.dp)
|
.padding(horizontal = CELL_GAP, vertical = 1.dp)
|
||||||
|
.morphElement(MonthMorphKey.Cell(d))
|
||||||
.background(
|
.background(
|
||||||
color = when {
|
color = when {
|
||||||
inMonth(d) -> MaterialTheme.colorScheme.surfaceContainer
|
inMonth(d) -> MaterialTheme.colorScheme.surfaceContainer
|
||||||
@@ -1343,7 +1760,9 @@ private fun MonthWeekRow(
|
|||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
},
|
},
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.morphElement(MonthMorphKey.DayNumber(d)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1351,11 +1770,15 @@ private fun MonthWeekRow(
|
|||||||
// Breathing room between the day number (and today's circle) and the
|
// Breathing room between the day number (and today's circle) and the
|
||||||
// first event row.
|
// first event row.
|
||||||
Spacer(Modifier.height(DAY_NUMBER_GAP))
|
Spacer(Modifier.height(DAY_NUMBER_GAP))
|
||||||
|
// Clipped at rest so a bar can't spill into the row below, but not
|
||||||
|
// while a morph is in flight: a mark travels between two different
|
||||||
|
// rows, and clipping it to the one it is arriving at hides it for
|
||||||
|
// the whole first half of the journey. See [morphInFlight].
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.clipToBounds(),
|
.then(if (morphing) Modifier else Modifier.clipToBounds()),
|
||||||
) {
|
) {
|
||||||
// Spanning bars on their shared lanes.
|
// Spanning bars on their shared lanes.
|
||||||
week.spans.filter { it.lane < shownLanes }.forEach { span ->
|
week.spans.filter { it.lane < shownLanes }.forEach { span ->
|
||||||
@@ -1370,10 +1793,51 @@ private fun MonthWeekRow(
|
|||||||
x = colW * span.startCol,
|
x = colW * span.startCol,
|
||||||
y = EVENT_ROW_HEIGHT * span.lane,
|
y = EVENT_ROW_HEIGHT * span.lane,
|
||||||
)
|
)
|
||||||
|
// Anchored on the day the bar starts *in this row*,
|
||||||
|
// which is where its dot sat. A bar carried in from
|
||||||
|
// the previous week starts at column 0, and column
|
||||||
|
// 0's dot is the one that grows into it.
|
||||||
|
//
|
||||||
|
// Outside the width/height, so the shared bounds
|
||||||
|
// drive the measurement instead of being pinned to
|
||||||
|
// the bar's final size from the first frame. The
|
||||||
|
// offset stays outside it: that is where the bar
|
||||||
|
// sits, not how big it is.
|
||||||
|
.morphBounds(
|
||||||
|
MonthMorphKey.Event(
|
||||||
|
date = week.days[span.startCol],
|
||||||
|
instanceId = span.event.instanceId,
|
||||||
|
),
|
||||||
|
)
|
||||||
.width(colW * cols)
|
.width(colW * cols)
|
||||||
.height(EVENT_ROW_HEIGHT)
|
.height(EVENT_ROW_HEIGHT)
|
||||||
.padding(horizontal = CELL_GAP + 1.dp, vertical = 1.dp),
|
.padding(horizontal = CELL_GAP + 1.dp, vertical = 1.dp),
|
||||||
)
|
)
|
||||||
|
// One invisible slice of the bar per further column it
|
||||||
|
// covers. A multi-day event has a dot on every day but
|
||||||
|
// only one bar, so those other dots had nothing to travel
|
||||||
|
// to and sat still while the rest of the grid moved.
|
||||||
|
//
|
||||||
|
// These give each of them a real place inside the bar to
|
||||||
|
// come out of and go back into, at its own column — so a
|
||||||
|
// dot drops out of the bar above it rather than appearing
|
||||||
|
// from nowhere, or from the top of the grid, which is what
|
||||||
|
// an untagged dot and a partnerless tag respectively did.
|
||||||
|
for (c in (span.startCol + 1)..span.endCol) {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.offset(x = colW * c, y = EVENT_ROW_HEIGHT * span.lane)
|
||||||
|
.morphBounds(
|
||||||
|
MonthMorphKey.Event(
|
||||||
|
date = week.days[c],
|
||||||
|
instanceId = span.event.instanceId,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.width(colW)
|
||||||
|
.height(EVENT_ROW_HEIGHT)
|
||||||
|
.padding(horizontal = CELL_GAP + 1.dp, vertical = 1.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Single-day timed pills + overflow, per column. Pills fill the
|
// Single-day timed pills + overflow, per column. Pills fill the
|
||||||
// lane slots no bar occupies on THIS day (top-most first), so a
|
// lane slots no bar occupies on THIS day (top-most first), so a
|
||||||
@@ -1398,6 +1862,7 @@ private fun MonthWeekRow(
|
|||||||
x = colW * col,
|
x = colW * col,
|
||||||
y = EVENT_ROW_HEIGHT * freeSlots[i],
|
y = EVENT_ROW_HEIGHT * freeSlots[i],
|
||||||
)
|
)
|
||||||
|
.morphBounds(MonthMorphKey.Event(d, ev.instanceId))
|
||||||
.width(colW)
|
.width(colW)
|
||||||
.height(EVENT_ROW_HEIGHT)
|
.height(EVENT_ROW_HEIGHT)
|
||||||
.padding(horizontal = CELL_GAP + 1.dp, vertical = 1.dp),
|
.padding(horizontal = CELL_GAP + 1.dp, vertical = 1.dp),
|
||||||
@@ -1417,6 +1882,7 @@ private fun MonthWeekRow(
|
|||||||
dark = dark,
|
dark = dark,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.offset(x = colW * col, y = EVENT_ROW_HEIGHT * MAX_EVENT_ROWS)
|
.offset(x = colW * col, y = EVENT_ROW_HEIGHT * MAX_EVENT_ROWS)
|
||||||
|
.morphBounds(MonthMorphKey.Overflow(d))
|
||||||
.width(colW)
|
.width(colW)
|
||||||
.padding(horizontal = 3.dp),
|
.padding(horizontal = 3.dp),
|
||||||
)
|
)
|
||||||
@@ -1425,6 +1891,29 @@ private fun MonthWeekRow(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Invisible stand-in for the compact grid's selection outline, so it
|
||||||
|
// has somewhere to travel to and from. Its own layer rather than a
|
||||||
|
// child of the cell pill: that pill is itself a tagged piece, and a
|
||||||
|
// tag nested inside a tag travels twice. Padded to match the outline's
|
||||||
|
// own bounds over there, or it would arrive at the wrong size.
|
||||||
|
if (selected != null) {
|
||||||
|
Row(Modifier.matchParentSize()) {
|
||||||
|
week.days.forEach { d ->
|
||||||
|
if (d == selected && inMonth(d)) {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.padding(horizontal = CELL_GAP, vertical = 1.dp)
|
||||||
|
.morphBounds(MonthMorphKey.Selection(d)),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Spacer(Modifier.weight(1f).fillMaxHeight())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Tap layer: in month view a tap on any day opens that day. Padded and
|
// Tap layer: in month view a tap on any day opens that day. Padded and
|
||||||
// clipped to the background pill so the ripple matches it. A blanked
|
// clipped to the background pill so the ripple matches it. A blanked
|
||||||
// cell isn't part of this month, so it takes no taps either.
|
// cell isn't part of this month, so it takes no taps either.
|
||||||
|
|||||||
@@ -39,6 +39,34 @@ data class MonthWeek(
|
|||||||
val countByDay: Map<LocalDate, Int>,
|
val countByDay: Map<LocalDate, Int>,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The events occupying each lane of [day] — column [col] of this week — in lane
|
||||||
|
* order, capped at [laneCap] lanes.
|
||||||
|
*
|
||||||
|
* This is the same seating [spans]/[timedByDay] get when the paged grid draws a
|
||||||
|
* week: bars keep the lane the row layout gave them, and the day's timed events
|
||||||
|
* fill whatever slots are left, top-most first. Reading it here means the split
|
||||||
|
* style's dots and the paged style's bars describe a day in the *same order*, so
|
||||||
|
* dot _i_ and lane _i_ are the same event and one can morph into the other (#53).
|
||||||
|
*
|
||||||
|
* Deriving the dots independently — by distinct colour, as they first were —
|
||||||
|
* left dot _i_ standing for no particular event, and quietly merged two events
|
||||||
|
* that shared a calendar into a single dot.
|
||||||
|
*/
|
||||||
|
fun MonthWeek.laneEvents(col: Int, day: LocalDate, laneCap: Int): List<EventInstance> {
|
||||||
|
val byLane = arrayOfNulls<EventInstance>(laneCap)
|
||||||
|
spans.forEach { span ->
|
||||||
|
if (span.lane < laneCap && col in span.startCol..span.endCol) {
|
||||||
|
byLane[span.lane] = span.event
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val free = (0 until laneCap).filter { byLane[it] == null }
|
||||||
|
timedByDay[day].orEmpty().take(free.size).forEachIndexed { i, event ->
|
||||||
|
byLane[free[i]] = event
|
||||||
|
}
|
||||||
|
return byLane.filterNotNull()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* State for the continuous style (#38): a vertical stream of *self-contained*
|
* State for the continuous style (#38): a vertical stream of *self-contained*
|
||||||
* months rather than one undifferentiated run of weeks. Each month is keyed by
|
* months rather than one undifferentiated run of weeks. Each month is keyed by
|
||||||
|
|||||||
@@ -284,6 +284,21 @@ class MonthViewModel @Inject constructor(
|
|||||||
_selectedDate.value = todayDate
|
_selectedDate.value = todayDate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track [_month] to the month a scrolling style is showing. Those styles
|
||||||
|
* navigate by scroll position and never set [_month] themselves, so it would
|
||||||
|
* otherwise sit wherever paged navigation last left it (today's month, on a
|
||||||
|
* fresh open) — and a switch to a paged style, or a reseed of the other
|
||||||
|
* scrolling style's list, would jump there instead of holding position. The
|
||||||
|
* selection is realigned alongside it so that landing on the Split style
|
||||||
|
* shows a live day in the month on screen rather than a stale, off-month one.
|
||||||
|
*/
|
||||||
|
fun syncScrollMonth(ym: YearMonth) {
|
||||||
|
if (_month.value == ym) return
|
||||||
|
_month.value = ym
|
||||||
|
_selectedDate.value = selectionForMonth(ym, todayDate)
|
||||||
|
}
|
||||||
|
|
||||||
/** Jump to the month containing [date] (drawer jump-to-date). */
|
/** Jump to the month containing [date] (drawer jump-to-date). */
|
||||||
fun goToDate(date: LocalDate) {
|
fun goToDate(date: LocalDate) {
|
||||||
_month.value = YearMonth(date.year, date.month)
|
_month.value = YearMonth(date.year, date.month)
|
||||||
|
|||||||
@@ -39,8 +39,9 @@ import kotlinx.datetime.DayOfWeek
|
|||||||
* closing on tap would hide the very thing the screen is for. Back exits, the
|
* closing on tap would hide the very thing the screen is for. Back exits, the
|
||||||
* same as the App name picker, which stays open for the same reason.
|
* same as the App name picker, which stays open for the same reason.
|
||||||
*
|
*
|
||||||
* Below the preview it is the family's standard picker shape: connected grouped
|
* Under the preview sits the selected style's own one-line blurb, then the
|
||||||
* rows with a tonal highlight and [SelectedCheck] on the current one.
|
* family's standard picker shape: connected single-line grouped rows with a
|
||||||
|
* tonal highlight and [SelectedCheck] on the current one.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
internal fun MonthViewStylePicker(
|
internal fun MonthViewStylePicker(
|
||||||
@@ -59,7 +60,7 @@ internal fun MonthViewStylePicker(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||||
.height(PREVIEW_HEIGHT),
|
.height(PREVIEW_HEIGHT),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
@@ -84,12 +85,14 @@ internal fun MonthViewStylePicker(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PickerDescription(stringResource(R.string.settings_month_view_style_summary))
|
// The selected style's own blurb lives here, under its preview, rather
|
||||||
|
// than on every row — the rows stay single-line and dense, and the words
|
||||||
|
// describe exactly what is being shown above.
|
||||||
|
PickerDescription(stringResource(selected.descriptionRes))
|
||||||
options.forEachIndexed { index, style ->
|
options.forEachIndexed { index, style ->
|
||||||
val isSelected = style == selected
|
val isSelected = style == selected
|
||||||
GroupedRow(
|
GroupedRow(
|
||||||
title = stringResource(style.labelRes),
|
title = stringResource(style.labelRes),
|
||||||
summary = stringResource(style.descriptionRes),
|
|
||||||
position = positionOf(index, options.size),
|
position = positionOf(index, options.size),
|
||||||
selected = isSelected,
|
selected = isSelected,
|
||||||
trailing = if (isSelected) {
|
trailing = if (isSelected) {
|
||||||
@@ -105,5 +108,5 @@ internal fun MonthViewStylePicker(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val PREVIEW_HEIGHT = 240.dp
|
private val PREVIEW_HEIGHT = 280.dp
|
||||||
private val PREVIEW_SHAPE = RoundedCornerShape(12.dp)
|
private val PREVIEW_SHAPE = RoundedCornerShape(12.dp)
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import androidx.compose.animation.animateColorAsState
|
|||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
|
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -49,7 +48,6 @@ import androidx.compose.runtime.CompositionLocalProvider
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
@@ -94,6 +92,7 @@ import de.jeanlucmakiola.calendula.ui.common.rememberCurrentMinute
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.ViewSwitcherPill
|
import de.jeanlucmakiola.calendula.ui.common.ViewSwitcherPill
|
||||||
import de.jeanlucmakiola.calendula.ui.common.calendarSlideTransition
|
import de.jeanlucmakiola.calendula.ui.common.calendarSlideTransition
|
||||||
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarFadeSpec
|
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarFadeSpec
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarPageSwipe
|
||||||
import de.jeanlucmakiola.floret.identity.rememberReduceMotion
|
import de.jeanlucmakiola.floret.identity.rememberReduceMotion
|
||||||
import de.jeanlucmakiola.floret.locale.currentLocale
|
import de.jeanlucmakiola.floret.locale.currentLocale
|
||||||
import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat
|
import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat
|
||||||
@@ -296,8 +295,6 @@ private fun WeekContent(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val threshold = with(density) { 24.dp.toPx() }
|
|
||||||
var dragAccum by remember { mutableFloatStateOf(0f) }
|
|
||||||
val slideSpec = rememberCalendarSlideSpec()
|
val slideSpec = rememberCalendarSlideSpec()
|
||||||
val fadeSpec = rememberCalendarFadeSpec()
|
val fadeSpec = rememberCalendarFadeSpec()
|
||||||
val reduceMotion = rememberReduceMotion()
|
val reduceMotion = rememberReduceMotion()
|
||||||
@@ -329,20 +326,7 @@ private fun WeekContent(
|
|||||||
// vertical scroll: a horizontal drag only crosses *this* detector's slop,
|
// vertical scroll: a horizontal drag only crosses *this* detector's slop,
|
||||||
// while a vertical drag is consumed by the inner scroll first — so the two
|
// while a vertical drag is consumed by the inner scroll first — so the two
|
||||||
// gestures coexist without fighting.
|
// gestures coexist without fighting.
|
||||||
val swipeModifier = Modifier.pointerInput(Unit) {
|
val swipeModifier = rememberCalendarPageSwipe(onSwipeNext, onSwipePrev)
|
||||||
detectHorizontalDragGestures(
|
|
||||||
onDragStart = { dragAccum = 0f },
|
|
||||||
onDragEnd = {
|
|
||||||
when {
|
|
||||||
dragAccum < -threshold -> onSwipeNext()
|
|
||||||
dragAccum > threshold -> onSwipePrev()
|
|
||||||
}
|
|
||||||
dragAccum = 0f
|
|
||||||
},
|
|
||||||
onDragCancel = { dragAccum = 0f },
|
|
||||||
onHorizontalDrag = { _, drag -> dragAccum += drag },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
AnimatedContent(
|
AnimatedContent(
|
||||||
targetState = state,
|
targetState = state,
|
||||||
|
|||||||
246
app/src/main/res/values-ar/strings.xml
Normal file
246
app/src/main/res/values-ar/strings.xml
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string-array name="backup_interval_units">
|
||||||
|
<item>Minutes</item>
|
||||||
|
<item>Hours</item>
|
||||||
|
<item>Days</item>
|
||||||
|
<item>Weeks</item>
|
||||||
|
</string-array>
|
||||||
|
<string name="state_loading">جارٍ التحميل…</string>
|
||||||
|
<string name="state_retry">إعادة المحاولة</string>
|
||||||
|
<string name="app_tagline">تقويم عصري.</string>
|
||||||
|
<string name="state_failure_unknown">حدث خطأ ما.</string>
|
||||||
|
<string name="state_failure_permission">الوصول إلى التقويم مطلوب.</string>
|
||||||
|
<string name="state_failure_permission_action">منح الوصول</string>
|
||||||
|
<string name="state_failure_no_calendars">لم يتم تكوين أي تقويمات.</string>
|
||||||
|
<string name="state_failure_no_calendars_action">فتح إعدادات تقويم النظام</string>
|
||||||
|
<string name="state_failure_provider">تعذّر قراءة التقويم.</string>
|
||||||
|
<string name="permission_rationale_title">شاهد جميع أحداثك، بشكل جميل</string>
|
||||||
|
<string name="permission_rationale_body">Calendula يحتاج للوصول إلى تقويمك لعرض و إدارة أحداثك. هذا كل ما يطلبه مقدمًا — ولا يخرج أي شيء من جهازك على الإطلاق.</string>
|
||||||
|
<string name="permission_request_button">منح الوصول إلى التقويم</string>
|
||||||
|
<string name="permission_denied_title">رُفِض الوصول إلى التقويم</string>
|
||||||
|
<string name="permission_denied_body">Calendula لا يمكنه عرض الأحداث بدون الوصول إلى التقويم. يمكنك منحه مُجددًا في إعدادات النظام.</string>
|
||||||
|
<string name="permission_retry_button">أعِد المحاولة</string>
|
||||||
|
<string name="permission_open_settings_button">فتح إعدادات النظام</string>
|
||||||
|
<string name="permission_benefit_private_title">يبقى على جهازك</string>
|
||||||
|
<string name="permission_benefit_private_body">تقويماتك تتِم قراءتها محليًا ولا تُغادر الهاتف أبدًا.</string>
|
||||||
|
<string name="permission_benefit_sync_title">جميع تقويماتك، مع بعض</string>
|
||||||
|
<string name="permission_benefit_sync_body">جوجل، CalDAV، محلي — أي شيء تمت مزامنته مع الجهاز يظهر فورًا.</string>
|
||||||
|
<string name="permission_benefit_privacy_title">لا تتبع، أبدًا</string>
|
||||||
|
<string name="permission_benefit_privacy_body">لا تتبع عن بُعد، لا تحليلات، لا إعلانات.</string>
|
||||||
|
<string name="permission_privacy_footnote">يبقى على جهازك · لا يوجد إذن بالإنترنت</string>
|
||||||
|
<string name="month_prev">الشهر السابق</string>
|
||||||
|
<string name="month_next">الشهر القادم</string>
|
||||||
|
<string name="month_today_action">اليوم</string>
|
||||||
|
<string name="month_action_settings">إعدادات</string>
|
||||||
|
<string name="settings_title">الإعدادات</string>
|
||||||
|
<string name="settings_theme">الثيم</string>
|
||||||
|
<string name="settings_theme_system">النظام</string>
|
||||||
|
<string name="settings_theme_light">فاتح</string>
|
||||||
|
<string name="settings_theme_dark">داكن</string>
|
||||||
|
<string name="month_open_menu">افتح القائمة</string>
|
||||||
|
<string name="month_more_actions">المزيد من الإجراءات</string>
|
||||||
|
<string name="event_detail_edit">تعديل</string>
|
||||||
|
<string name="event_detail_delete">حذف</string>
|
||||||
|
<string name="event_detail_share">مشاركة</string>
|
||||||
|
<string name="event_detail_duplicate">نسخ</string>
|
||||||
|
<string name="event_share_chooser_title">مشاركة الحدث</string>
|
||||||
|
<string name="day_today_action">اليوم</string>
|
||||||
|
<string name="week_today_action">هذا الأسبوع</string>
|
||||||
|
<string name="week_number_label">أسبوع</string>
|
||||||
|
<string name="event_detail_back">العودة</string>
|
||||||
|
<string name="month_a11y_today_prefix">اليوم</string>
|
||||||
|
<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_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_delete_failed">تعذّر حذف الحدث</string>
|
||||||
|
<string name="event_delete_write_denied">Calendula يحتاج إلى صلاحية للكتابة لحذف الأحداث</string>
|
||||||
|
<string name="dialog_cancel">إلغاء</string>
|
||||||
|
<string name="dialog_ok">حسنًا</string>
|
||||||
|
<string name="event_edit_new_title">حدث جديد</string>
|
||||||
|
<string name="event_edit_close">إغلاق</string>
|
||||||
|
<string name="event_edit_save">حفظ</string>
|
||||||
|
<string name="event_edit_title_hint">إضافة عنوان</string>
|
||||||
|
<string name="event_edit_managed_hint">تتم الإدارة بواسطة ”%1$s“ — العنوان، التاريخ و التكرار تبقى في مزامنة من جهات اتصالك. التذكيرات، الموقع والملاحظات يمكن لك تعديلها.</string>
|
||||||
|
<string name="event_edit_starts">يبدأ</string>
|
||||||
|
<string name="event_edit_ends">ينتهي</string>
|
||||||
|
<string name="event_edit_error_end_before_start">ينتهي قبل أن يبدأ</string>
|
||||||
|
<string name="event_edit_error_no_calendar">لا تتوفر تقويمات قابلة للكتابة</string>
|
||||||
|
<string name="event_edit_save_failed">تعذّر حفظ الحدث</string>
|
||||||
|
<string name="event_edit_write_denied">Calendula يحتاج إلى صلاحية للكتابة لإنشاء أحداث</string>
|
||||||
|
<string name="event_edit_add_reminder">إضافة تذكير</string>
|
||||||
|
<string name="event_edit_remove_reminder">إزالة التذكير</string>
|
||||||
|
<string name="event_edit_attendees">الضيوف</string>
|
||||||
|
<string name="event_edit_add_guest">إضافة ضيف</string>
|
||||||
|
<string name="event_edit_add_guest_hint">إضافة ضيف بالبريد الإلكتروني…</string>
|
||||||
|
<string name="event_edit_add_guest_from_contacts">إضافة من جهات الاتصال</string>
|
||||||
|
<string name="event_edit_remove_guest">إزالة الضيف</string>
|
||||||
|
<string name="event_edit_attendee_required">مطلوب</string>
|
||||||
|
<string name="event_edit_attendee_optional">اختياري</string>
|
||||||
|
<string name="event_edit_attendees_note_synced">Calendula لا يرسل دعوات. قد يرسل حساب التقويم الخاص بك بريدًا إلكترونيًا إلى الضيوف عند مزامنته.</string>
|
||||||
|
<string name="event_edit_reminder_custom">مخصص</string>
|
||||||
|
<string name="reminder_unit_minutes">دقائق</string>
|
||||||
|
<string name="reminder_unit_hours">ساعات</string>
|
||||||
|
<string name="event_edit_attendees_note_local">مخزّن على هذا الجهاز. لن يتم إشعار أي شخص.</string>
|
||||||
|
<string name="event_edit_location_from_contacts">اختر عنوان من جهات الاتصال</string>
|
||||||
|
<string name="event_edit_add">أضِف</string>
|
||||||
|
<string name="reminder_unit_days">أيام</string>
|
||||||
|
<string name="reminder_unit_weeks">أسابيع</string>
|
||||||
|
<string name="event_edit_color_default">لون التقويم</string>
|
||||||
|
<string name="event_edit_color_custom">لون مخصص</string>
|
||||||
|
<string name="event_edit_visibility">مدى الرؤية</string>
|
||||||
|
<string name="event_edit_availability">التَّوافر</string>
|
||||||
|
<string name="event_edit_color">اللون</string>
|
||||||
|
<string name="event_edit_color_reset">إعادة تعيين</string>
|
||||||
|
<string name="event_edit_color_unsupported">غير مُتوفِّر لهذا التقويم</string>
|
||||||
|
<string name="event_edit_color_unsupported_hint">هذا التقويم لا ينشر أي مجموعة ألوان. يمكنك السماح بألوان مخصصة لمثل هذه التقويمات في الإعدادات.</string>
|
||||||
|
<string name="event_edit_color_sync_warning">هذا التقويم قد يزيل أو يستبدله اللون في مزامنته التالية.</string>
|
||||||
|
<string name="event_edit_conflict_title">الحدث غُيِّر في مكان آخر</string>
|
||||||
|
<string name="event_edit_conflict_body">أثناء قيامك بالتعديل، هذا الحدث تغيّر — عن طريق المزامنة أو تطبيق آخر. ماذا يجب أن يحدث لتغييراتك؟</string>
|
||||||
|
<string name="event_edit_conflict_overwrite">حفظ تغييراتي</string>
|
||||||
|
<string name="event_edit_conflict_discard">تجاهل تغييراتي</string>
|
||||||
|
<string name="event_edit_gone_title">تم حذف الحدث</string>
|
||||||
|
<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_access_public">علني</string>
|
||||||
|
<string name="event_access_default">الافتراضي</string>
|
||||||
|
<string name="event_availability_busy">مشغول</string>
|
||||||
|
<string name="event_edit_recurrence_times">مرّات</string>
|
||||||
|
<string name="recurrence_unit_days">أيام</string>
|
||||||
|
<string name="recurrence_unit_weeks">أسابيع</string>
|
||||||
|
<string name="recurrence_unit_months">شهور</string>
|
||||||
|
<string name="recurrence_unit_years">سنوات</string>
|
||||||
|
<string name="event_edit_recurrence_ends">ينتهي</string>
|
||||||
|
<string name="event_edit_recurrence_end_never">أبدًا</string>
|
||||||
|
<string name="event_edit_recurrence_end_until">في موعد</string>
|
||||||
|
<string name="event_edit_recurrence_end_count">بعد عدد من المرّات</string>
|
||||||
|
<string name="event_edit_recurrence_custom">مخصص</string>
|
||||||
|
<string name="event_edit_recurrence_every">كل</string>
|
||||||
|
<string name="event_edit_recurrence_none">لا يتكرر</string>
|
||||||
|
<string name="import_reminder_prompt_body_none">تم استيراد هذا الحدث بدون أي تذكير.</string>
|
||||||
|
<plurals name="import_reminder_prompt_body_existing">
|
||||||
|
<item quantity="one">تم استيراد هذا الحدث مع %1$d تذكير.</item>
|
||||||
|
<item quantity="two">تم استيراد هذا الحدث مع %1$d تذكيران.</item>
|
||||||
|
<item quantity="few">تم استيراد هذا الحدث مع %1$d تذكيرات.</item>
|
||||||
|
<item quantity="many">تم استيراد هذا الحدث مع %1$d تذكير.</item>
|
||||||
|
<item quantity="other">تم استيراد هذا الحدث مع %1$d تذكير.</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="event_edit_error_recurrence_ends_before_start">التكرار ينتهي قبل أن يبدأ الحدث</string>
|
||||||
|
<string name="import_reminder_prompt_keep">اتركه كما هو</string>
|
||||||
|
<string name="event_detail_calendar">التقويم</string>
|
||||||
|
<string name="event_detail_calendar_unknown">تقويم غير معروف</string>
|
||||||
|
<string name="event_detail_description">الوصف</string>
|
||||||
|
<string name="event_detail_all_day">كل يوم</string>
|
||||||
|
<string name="event_detail_location">الموقع</string>
|
||||||
|
<string name="event_detail_attendees">الحضور</string>
|
||||||
|
<string name="event_detail_recurrence">التكرار</string>
|
||||||
|
<string name="recurrence_daily">كل يوم</string>
|
||||||
|
<string name="recurrence_weekly">كل أسبوع</string>
|
||||||
|
<string name="recurrence_monthly">كل شهر</string>
|
||||||
|
<string name="recurrence_yearly">كل سنة</string>
|
||||||
|
<string name="recurrence_every_n_days">كل %1$d أيام</string>
|
||||||
|
<string name="recurrence_every_n_weeks">كل %1$d أسابيع</string>
|
||||||
|
<string name="recurrence_every_n_months">كل %1$d أشهر</string>
|
||||||
|
<string name="recurrence_every_n_years">كل %1$d سنوات</string>
|
||||||
|
<string name="event_detail_not_found">هذا الحدث لم يعد موجودًا.</string>
|
||||||
|
<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="recurrence_on_days">%1$s في %2$s</string>
|
||||||
|
<string name="event_attendee_unknown">—</string>
|
||||||
|
<string name="event_attendee_accepted">تم القُبول</string>
|
||||||
|
<string name="event_attendee_declined">تم الرَفض</string>
|
||||||
|
<string name="event_attendee_tentative">غير مُؤكِّد</string>
|
||||||
|
<string name="event_attendee_needs_action">لا يوجد رد</string>
|
||||||
|
<string name="event_detail_reminders">التذكيرات</string>
|
||||||
|
<string name="event_detail_timezone">المنطقة الزمنية</string>
|
||||||
|
<string name="event_status_tentative">غير مُؤكِّد</string>
|
||||||
|
<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_attendee_organizer">المُنظِّم</string>
|
||||||
|
<string name="event_attendee_optional">اختياري</string>
|
||||||
|
<string name="reminder_default">التذكير الافتراضي</string>
|
||||||
|
<string name="event_detail_self_response">ردَّك: %1$s</string>
|
||||||
|
<string name="event_attendee_resource">المُنسِّق</string>
|
||||||
|
<string name="reminder_at_time">في وقت الحدث</string>
|
||||||
|
<string name="event_untitled">(بدون عنوان)</string>
|
||||||
|
<string name="reminder_channel_name">تذكيرات الحدث</string>
|
||||||
|
<string name="reminder_channel_description">الإشعارات في أوقات التذكير الخاصه بأحداثك</string>
|
||||||
|
<string name="reminder_onboarding_title">لا تفوّت أي حدث أبدًا</string>
|
||||||
|
<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_reversible_title">يمكنك تغييره في أي وقت</string>
|
||||||
|
<string name="reminder_onboarding_enable_button">شَغِّل التذكيرات</string>
|
||||||
|
<string name="reminder_onboarding_skip_button">ليس الآن</string>
|
||||||
|
<string name="reminder_action_snooze">تأجيل</string>
|
||||||
|
<string name="reminder_action_dismiss">تجاهل</string>
|
||||||
|
<string name="reminder_day_tomorrow">غدًا</string>
|
||||||
|
<string name="reminder_day_yesterday">يوم أمس</string>
|
||||||
|
<string name="view_month">شهر</string>
|
||||||
|
<string name="view_week">أسبوع</string>
|
||||||
|
<string name="view_day">يوم</string>
|
||||||
|
<string name="view_agenda">جدول</string>
|
||||||
|
<string name="drawer_jump_to_date">الانتقال إلى تاريخ</string>
|
||||||
|
<string name="agenda_today_action">اليوم</string>
|
||||||
|
<string name="agenda_header_today">اليوم</string>
|
||||||
|
<string name="agenda_header_tomorrow">غدًا</string>
|
||||||
|
<string name="view_section">طريقة العرض</string>
|
||||||
|
<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_idle_hint">ابحث عن أحداثك بالعنوان، الموقع أو الملاحظات.</string>
|
||||||
|
<string name="search_back">الرجوع</string>
|
||||||
|
<string name="search_action">البحث</string>
|
||||||
|
<string name="widget_agenda_title">القادم</string>
|
||||||
|
<string name="widget_agenda_label">جدول Calendula</string>
|
||||||
|
<string name="widget_month_label">شهر Calendula</string>
|
||||||
|
<string name="widget_new_event">حدث جديد</string>
|
||||||
|
<string name="widget_needs_permission">افتح Calendula لمنح الوصول إلى التقويم</string>
|
||||||
|
<string name="widget_prev_month">الشهر السابق</string>
|
||||||
|
<string name="widget_next_month">الشهر القادم</string>
|
||||||
|
<string name="widget_today">اليوم</string>
|
||||||
|
<string name="filter_title">التقويمات</string>
|
||||||
|
<string name="settings_back">الرجوع</string>
|
||||||
|
<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_default_view">طريقة العرض الافتراضية</string>
|
||||||
|
<string name="settings_soften_colors">ألوان تقويم ناعمة</string>
|
||||||
|
<string name="settings_soften_colors_summary">خفف ألوان التقويم والأحداث لتتناسب مع الثيم. قم بإيقافه لإظهار الألوان الخام من مصدر التقويم.</string>
|
||||||
|
<string name="settings_font_headings">خط العناوين</string>
|
||||||
|
<string name="settings_font_body">خط النص</string>
|
||||||
|
<string name="settings_font_system">افتراضي النظام</string>
|
||||||
|
<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_auto">تلقائي</string>
|
||||||
|
<string name="settings_time_format_auto">تلقائي</string>
|
||||||
|
<string name="settings_time_format">تنسيق الوقت</string>
|
||||||
|
<string name="settings_week_numbers">أرقام الأسبوع</string>
|
||||||
|
<string name="settings_week_numbers_summary">إظهار أرقام أسابيع التقويم في عرض الشهر</string>
|
||||||
|
<string name="settings_time_format_12h">١٢-ساعة (٢:٠٠ م)</string>
|
||||||
|
<string name="settings_time_format_24h">٢٤-ساعة (١٤:٠٠)</string>
|
||||||
|
<string name="agenda_range_day">اليوم</string>
|
||||||
|
<string name="settings_agenda_show_today">إظهار اليوم دائمًا</string>
|
||||||
|
<string name="settings_agenda_show_today_hint">اترك اليوم في أعلى الجدول و ودجته، حتى بعد عدم وجود أي شيء اليوم.</string>
|
||||||
|
<string name="agenda_range_this_month">هذا الشهر</string>
|
||||||
|
<string name="agenda_range_this_week">هذا الأسبوع</string>
|
||||||
|
<string name="settings_past_events_show">إظهار</string>
|
||||||
|
<string name="settings_past_events_hide">إخفاء</string>
|
||||||
|
<string name="settings_agenda_header">جدول</string>
|
||||||
|
</resources>
|
||||||
@@ -475,19 +475,21 @@
|
|||||||
<string name="special_dates_default_title_anniversary">Jahrestag von {name} ({year})</string>
|
<string name="special_dates_default_title_anniversary">Jahrestag von {name} ({year})</string>
|
||||||
<string name="special_dates_default_title_custom">{name}</string>
|
<string name="special_dates_default_title_custom">{name}</string>
|
||||||
<string name="settings_week_numbers">Kalenderwochen</string>
|
<string name="settings_week_numbers">Kalenderwochen</string>
|
||||||
<string name="settings_week_numbers_summary">Kalenderwochen der Monatsansicht anzeigen</string>
|
<string name="settings_week_numbers_summary">Kalenderwochen in Monatsansicht zeigen</string>
|
||||||
<string name="calendars_export_title">Kalender exportieren</string>
|
<string name="calendars_export_title">Kalender exportieren</string>
|
||||||
<string name="calendars_export_hint">Wähle aus, welche Kalender in die .ics-Datei aufgenommen werden sollen.</string>
|
<string name="calendars_export_hint">Wähle aus, welche Kalender in die .ics-Datei aufgenommen werden sollen.</string>
|
||||||
<string name="calendars_export_action">Exportieren</string>
|
<string name="calendars_export_action">Exportieren</string>
|
||||||
<string name="calendars_restore_header">Wiederherstellen</string>
|
<string name="calendars_restore_header">Wiederherstellen</string>
|
||||||
<string name="calendars_restore_action">Aus .ics-Datei wiederherstellen</string>
|
<string name="calendars_restore_action">Aus .ics-Datei wiederherstellen</string>
|
||||||
<string name="calendars_restore_hint">Importiere Ereignisse aus einem Backup oder einer anderen Kalender-App.</string>
|
<string name="calendars_restore_hint">Einträge von einem Backup oder einer anderen Kalender App importieren.</string>
|
||||||
<string name="import_done_dedup_note">Bereits im Kalender vorhandene Ereignisse wurden übersprungen.</string>
|
<string name="import_done_dedup_note">Bereits im Kalender vorhandene Ereignisse wurden übersprungen.</string>
|
||||||
<string name="import_done_added_label">Hinzugefügt</string>
|
<string name="import_done_added_label">Hinzugefügt</string>
|
||||||
<string name="import_done_skipped_label">Duplikate</string>
|
<string name="import_done_skipped_label">Dopplungen</string>
|
||||||
<string name="import_button">Importieren</string>
|
<string name="import_button">Importieren</string>
|
||||||
<plurals name="import_title_count">
|
<plurals name="import_title_count">
|
||||||
<item quantity="one">%d Ereignis wird importiert</item>
|
<item quantity="one">%d Ereignis wird importiert</item>
|
||||||
<item quantity="other">%d Ereignisse werden importiert</item>
|
<item quantity="other">%d Ereignisse werden importiert</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<string name="event_detail_duplicate">Duplikate</string>
|
||||||
|
<string name="reminder_day_tomorrow">Morgen</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -10,27 +10,27 @@
|
|||||||
<string name="app_tagline">Un calendrier moderne.</string>
|
<string name="app_tagline">Un calendrier moderne.</string>
|
||||||
<string name="state_loading">Chargement…</string>
|
<string name="state_loading">Chargement…</string>
|
||||||
<string name="state_retry">Réessayer</string>
|
<string name="state_retry">Réessayer</string>
|
||||||
<string name="state_failure_unknown">Une erreur s\'est produit.</string>
|
<string name="state_failure_unknown">Une erreur s\'est produite.</string>
|
||||||
<string name="state_failure_permission">Un accès au Calendrier est nécessaire.</string>
|
<string name="state_failure_permission">Un accès au Calendrier est nécessaire.</string>
|
||||||
<string name="state_failure_permission_action">Autoriser l\'accès</string>
|
<string name="state_failure_permission_action">Accorder l\'accès</string>
|
||||||
<string name="state_failure_no_calendars">Aucun calendrier configuré.</string>
|
<string name="state_failure_no_calendars">Aucun calendrier configuré.</string>
|
||||||
<string name="state_failure_no_calendars_action">Ouvrir les paramètres du calendrier.</string>
|
<string name="state_failure_no_calendars_action">Ouvrir les paramètres systèmes du calendrier</string>
|
||||||
<string name="state_failure_provider">Ne peut pas lire le calendrier.</string>
|
<string name="state_failure_provider">Impossible de lire le calendrier.</string>
|
||||||
<string name="permission_rationale_title">Tous les évènements ont été trouvés.</string>
|
<string name="permission_rationale_title">Accédez à tous vos évènements, en beauté</string>
|
||||||
<string name="permission_rationale_body">Calendula a besoin d\'avoir accès à votre calendrier pour afficher et accéder aux événements. C\'est tout ce dont nous avons besoin, aucune données ne sort de votre appareil.</string>
|
<string name="permission_rationale_body">Calendula a besoin d\'accéder à votre calendrier pour afficher et gérer vos évènements. C\'est tout ce qu\'il demande — et rien ne quitte votre appareil.</string>
|
||||||
<string name="permission_request_button">Autoriser l\'accès au calendrier</string>
|
<string name="permission_request_button">Autoriser l\'accès au calendrier</string>
|
||||||
<string name="permission_denied_title">Accès au calendrier refusé.</string>
|
<string name="permission_denied_title">Accès au calendrier refusé</string>
|
||||||
<string name="permission_denied_body">Calendula ne peut pas afficher les évènements si elle ne peut pas accéder au calendrier. Vous pouvez l\'autoriser à tout moment dans les paramètres.</string>
|
<string name="permission_denied_body">Calendula ne peut pas afficher les évènements si elle ne peut pas accéder au calendrier. Vous pouvez l\'autoriser à tout moment dans les paramètres.</string>
|
||||||
<string name="permission_open_settings_button">Ouvrir les paramètres.</string>
|
<string name="permission_open_settings_button">Ouvrir les paramètres système</string>
|
||||||
<string name="permission_retry_button">Essayer à nouveau</string>
|
<string name="permission_retry_button">Essayer à nouveau</string>
|
||||||
<string name="permission_benefit_private_title">Reste sur votre appareil</string>
|
<string name="permission_benefit_private_title">Ne quitte pas votre appareil</string>
|
||||||
<string name="permission_benefit_private_body">Vos calendriers sont lus localement et ne quittent jamais le téléphone.</string>
|
<string name="permission_benefit_private_body">Vos calendriers sont lus localement et ne quittent jamais le téléphone.</string>
|
||||||
<string name="permission_benefit_sync_title">Tous vos calendriers, réunis</string>
|
<string name="permission_benefit_sync_title">Tous vos calendriers, réunis</string>
|
||||||
<string name="permission_benefit_privacy_title">Aucun suivi, jamais</string>
|
<string name="permission_benefit_privacy_title">Aucun suivi, jamais</string>
|
||||||
<string name="permission_benefit_privacy_body">Aucune collecte de données, aucune analyse, pas de pubs</string>
|
<string name="permission_benefit_privacy_body">Aucune collecte de données, aucune analyse, pas de pubs.</string>
|
||||||
<string name="permission_privacy_footnote">Reste sur votre appareil - Pas d\'utilisation d\'Internet</string>
|
<string name="permission_privacy_footnote">Reste sur votre appareil - Pas d\'utilisation d\'Internet</string>
|
||||||
<string name="month_prev">Mois précédent</string>
|
<string name="month_prev">Mois précédent</string>
|
||||||
<string name="month_next">Prochain mois</string>
|
<string name="month_next">Mois suivant</string>
|
||||||
<string name="month_today_action">Aujourd\'hui</string>
|
<string name="month_today_action">Aujourd\'hui</string>
|
||||||
<string name="month_more_actions">Plus d\'actions</string>
|
<string name="month_more_actions">Plus d\'actions</string>
|
||||||
<string name="month_open_menu">Ouvrir le menu</string>
|
<string name="month_open_menu">Ouvrir le menu</string>
|
||||||
@@ -153,4 +153,351 @@
|
|||||||
<string name="event_attendee_needs_action">Aucune réponse</string>
|
<string name="event_attendee_needs_action">Aucune réponse</string>
|
||||||
<string name="event_attendee_unknown">—</string>
|
<string name="event_attendee_unknown">—</string>
|
||||||
<string name="event_detail_reminders">Rappels</string>
|
<string name="event_detail_reminders">Rappels</string>
|
||||||
|
<string name="permission_benefit_sync_body">Google, CalDAV, local - Tout ce qui est synchronisé sur l\'appareil s\'affiche tout simplement.</string>
|
||||||
|
<string name="event_status_cancelled">Annulé</string>
|
||||||
|
<plurals name="duration_minutes">
|
||||||
|
<item quantity="one">%d minute</item>
|
||||||
|
<item quantity="many">%d minutes</item>
|
||||||
|
<item quantity="other">%d minutes</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="widget_new_event">Nouvel évènement</string>
|
||||||
|
<string name="widget_next_month">Mois suivant</string>
|
||||||
|
<string name="filter_title">Calendriers</string>
|
||||||
|
<string name="agenda_range_custom">Personnaliser…</string>
|
||||||
|
<string name="settings_section_calendars">Calendriers</string>
|
||||||
|
<string name="calendars_title">Calendriers</string>
|
||||||
|
<string name="calendars_backup_header">Sauvegarde</string>
|
||||||
|
<string name="calendars_auto_backup_every">Chaque %1$s</string>
|
||||||
|
<string name="backup_channel_name">Sauvegarde</string>
|
||||||
|
<string name="shortcut_new_event_short">Nouvel évènement</string>
|
||||||
|
<string name="qs_tile_new_event_label">Nouvel évènement</string>
|
||||||
|
<string name="event_detail_duplicate">Dupliquer</string>
|
||||||
|
<string name="event_detail_timezone">Fuseau horaire</string>
|
||||||
|
<string name="event_availability_free">Disponible</string>
|
||||||
|
<string name="event_access_private">Privé</string>
|
||||||
|
<string name="event_access_confidential">Confidentiel</string>
|
||||||
|
<string name="event_attendee_organizer">Organisateur</string>
|
||||||
|
<string name="event_attendee_resource">Ressource</string>
|
||||||
|
<string name="event_detail_self_response">Votre réponse : %1$s</string>
|
||||||
|
<string name="reminder_default">Rappel par défaut</string>
|
||||||
|
<plurals name="reminder_minutes">
|
||||||
|
<item quantity="one">%d minute avant</item>
|
||||||
|
<item quantity="many">%d minutes avant</item>
|
||||||
|
<item quantity="other">%d minutes avant</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="reminder_hours">
|
||||||
|
<item quantity="one">%d heure avant</item>
|
||||||
|
<item quantity="many">%d heures avant</item>
|
||||||
|
<item quantity="other">%d heures avant</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="reminder_days">
|
||||||
|
<item quantity="one">%d jour avant</item>
|
||||||
|
<item quantity="many">%d jours avant</item>
|
||||||
|
<item quantity="other">%d jours avant</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="reminder_weeks">
|
||||||
|
<item quantity="one">%d semaine avant</item>
|
||||||
|
<item quantity="many">%d semaines avant</item>
|
||||||
|
<item quantity="other">%d semaines avant</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="duration_hours">
|
||||||
|
<item quantity="one">%d heure</item>
|
||||||
|
<item quantity="many">%d heures</item>
|
||||||
|
<item quantity="other">%d heures</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="duration_days">
|
||||||
|
<item quantity="one">%d jour</item>
|
||||||
|
<item quantity="many">%d jours</item>
|
||||||
|
<item quantity="other">%d jours</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="duration_weeks">
|
||||||
|
<item quantity="one">%d semaine</item>
|
||||||
|
<item quantity="many">%d semaines</item>
|
||||||
|
<item quantity="other">%d semaines</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="event_untitled">(Pas de titre)</string>
|
||||||
|
<string name="reminder_channel_name">Rappels d\'évènement</string>
|
||||||
|
<string name="reminder_channel_description">Notifications au moment des rappels de vos évènements</string>
|
||||||
|
<string name="reminder_onboarding_title">Ne manquez jamais un évènement</string>
|
||||||
|
<string name="reminder_benefit_delivery_title">Vos rappels, libérés</string>
|
||||||
|
<string name="reminder_benefit_delivery_body">Chaque rappel à vos évènements vous délivre une notification, ponctuelle.</string>
|
||||||
|
<string name="reminder_onboarding_enable_button">Activer les rappels</string>
|
||||||
|
<string name="reminder_onboarding_skip_button">Plus tard</string>
|
||||||
|
<string name="reminder_day_tomorrow">Demain</string>
|
||||||
|
<string name="reminder_day_yesterday">Hier</string>
|
||||||
|
<string name="view_month">Mois</string>
|
||||||
|
<string name="view_week">Semaine</string>
|
||||||
|
<string name="view_day">Jour</string>
|
||||||
|
<string name="view_agenda">Agenda</string>
|
||||||
|
<string name="view_section">Vue</string>
|
||||||
|
<string name="drawer_jump_to_date">Aller à ...</string>
|
||||||
|
<string name="agenda_header_tomorrow">Demain</string>
|
||||||
|
<string name="agenda_empty_title">Vous êtes à jour</string>
|
||||||
|
<string name="agenda_no_more_today">Plus d\'évènements aujourd\'hui</string>
|
||||||
|
<string name="search_action">Rechercher</string>
|
||||||
|
<string name="search_hint">Rechercher des évènements</string>
|
||||||
|
<string name="search_idle_hint">Rechercher parmi vos évènements par titre, lieu, ou notes.</string>
|
||||||
|
<string name="search_empty">Pas d\'évènements correspondant à “%1$s”.</string>
|
||||||
|
<string name="widget_agenda_title">À venir</string>
|
||||||
|
<string name="widget_agenda_label">Agenda Calendula</string>
|
||||||
|
<string name="widget_month_label">Calendula Mois</string>
|
||||||
|
<string name="widget_refresh">Actualiser</string>
|
||||||
|
<string name="settings_section_appearance">Apparence</string>
|
||||||
|
<string name="settings_theme">Thème</string>
|
||||||
|
<string name="settings_theme_system">Système</string>
|
||||||
|
<string name="settings_theme_light">Clair</string>
|
||||||
|
<string name="settings_theme_dark">Sombre</string>
|
||||||
|
<string name="settings_default_view">Vue par défaut</string>
|
||||||
|
<string name="settings_dynamic_color">Couleurs dynamiques</string>
|
||||||
|
<string name="settings_dynamic_color_unavailable">Nécessite Android 12 ou plus récent</string>
|
||||||
|
<string name="settings_soften_colors">Adoucir les couleurs du calendrier</string>
|
||||||
|
<string name="settings_soften_colors_summary">Ajuster les couleurs du calendrier et des évènements au thème. Désactiver pour voir les couleurs brutes issues du calendrier.</string>
|
||||||
|
<string name="settings_font_headings">Police des titres</string>
|
||||||
|
<string name="settings_font_body">Police du corps</string>
|
||||||
|
<string name="settings_font_system">Valeur par défaut du système</string>
|
||||||
|
<string name="font_atkinson_hyperlegible">Atkinson Hyperlegible</string>
|
||||||
|
<string name="font_lora">Lora</string>
|
||||||
|
<string name="settings_font_choose_file">Sélectionner un fichier…</string>
|
||||||
|
<string name="settings_font_custom_selected">Police personnalisée</string>
|
||||||
|
<string name="settings_font_import_failed">Impossible d\'interpréter ce fichier comme police</string>
|
||||||
|
<string name="settings_week_start_auto">Automatique</string>
|
||||||
|
<string name="settings_week_numbers">Numéros de semaine</string>
|
||||||
|
<string name="settings_week_numbers_summary">Afficher les numéros de semaine dans la vue par mois</string>
|
||||||
|
<string name="event_attendee_optional">Optionnel</string>
|
||||||
|
<string name="agenda_today_action">Aujourd\'hui</string>
|
||||||
|
<string name="agenda_header_today">Aujourd\'hui</string>
|
||||||
|
<string name="search_back">Retour</string>
|
||||||
|
<string name="widget_prev_month">Mois précédent</string>
|
||||||
|
<string name="widget_today">Aujourd\'hui</string>
|
||||||
|
<string name="settings_title">Paramètres</string>
|
||||||
|
<string name="settings_back">Retour</string>
|
||||||
|
<string name="agenda_range_day">Aujourd\'hui</string>
|
||||||
|
<string name="agenda_range_this_week">Cette semaine</string>
|
||||||
|
<string name="back">Retour</string>
|
||||||
|
<string name="settings_special_dates_reminders">Rappels</string>
|
||||||
|
<string name="settings_special_dates_grant">Autoriser l\'accès</string>
|
||||||
|
<string name="dialog_save">Sauvegarder</string>
|
||||||
|
<string name="calendars_color_label">Couleur</string>
|
||||||
|
<string name="import_close">Fermer</string>
|
||||||
|
<string name="week_number_label">Semaine</string>
|
||||||
|
<string name="event_status_tentative">Provisoire</string>
|
||||||
|
<string name="reminder_at_time">Au moment de l’événement</string>
|
||||||
|
<string name="reminder_onboarding_body">Android n’affiche pas de rappels d’événements par lui-même — une application de calendrier doit le faire. Laissez Calendula faire ce travail.</string>
|
||||||
|
<string name="reminder_benefit_duplicates_title">Utiliser une deuxième application de calendrier ?</string>
|
||||||
|
<string name="reminder_benefit_duplicates_body">Si une autre application publie également des rappels, vous les verrez deux fois, désactivez-les là ou ici.</string>
|
||||||
|
<string name="reminder_benefit_reversible_title">modifier à tout moment</string>
|
||||||
|
<string name="reminder_benefit_reversible_body">Le commutateur se trouve dans les paramètres, sous Notifications.</string>
|
||||||
|
<string name="reminder_action_snooze">Snooze</string>
|
||||||
|
<string name="reminder_action_dismiss">Ignorer</string>
|
||||||
|
<string name="search_clear">Effacer</string>
|
||||||
|
<string name="widget_needs_permission">Ouvrez Calendula pour accorder l’accès au calendrier</string>
|
||||||
|
<string name="font_jetbrains_mono">JetBrains Mono</string>
|
||||||
|
<string name="settings_week_start">La semaine commence le</string>
|
||||||
|
<string name="settings_time_format">Format de l\'heure</string>
|
||||||
|
<string name="settings_time_format_auto">Automatique</string>
|
||||||
|
<string name="settings_time_format_12h">12 heures (2:00 PM)</string>
|
||||||
|
<string name="settings_time_format_24h">24 heures (14:00)</string>
|
||||||
|
<string name="settings_hour_lines">Lignes d\'heures</string>
|
||||||
|
<string name="settings_hour_lines_summary">Afficher une ligne de séparation à chaque heure dans la vue de la semaine et du jour</string>
|
||||||
|
<string name="settings_dim_completed">Dim événements terminés</string>
|
||||||
|
<string name="settings_dim_completed_summary">Estomper les événements qui se sont déjà terminés dans la vue du mois et de la semaine</string>
|
||||||
|
<string name="settings_past_events">Evènements passés</string>
|
||||||
|
<string name="settings_past_events_show">Montrer</string>
|
||||||
|
<string name="settings_past_events_dim">Dim</string>
|
||||||
|
<string name="settings_past_events_hide">Cacher</string>
|
||||||
|
<string name="settings_agenda_header">Agenda</string>
|
||||||
|
<string name="settings_agenda_range">Plage d’agenda</string>
|
||||||
|
<string name="settings_agenda_range_hint">Jusqu’à quelle date l’écran Agenda liste les événements.</string>
|
||||||
|
<string name="settings_agenda_widget_range">Plage de widgets Agenda</string>
|
||||||
|
<string name="settings_agenda_widget_range_hint">Jusqu’à quelle heure le widget de l’écran d’accueil de l’agenda liste les événements.</string>
|
||||||
|
<string name="settings_agenda_show_today">Toujours montrer aujourd’hui</string>
|
||||||
|
<string name="settings_agenda_show_today_hint">Gardez aujourd’hui en tête de l’agenda et de son widget, même s’il ne reste plus rien aujourd’hui.</string>
|
||||||
|
<string name="settings_agenda_range_bar">Barre de rang</string>
|
||||||
|
<string name="settings_agenda_range_bar_hint">Afficher une barre en haut de l’agenda nommant les dates affichées, avec un bouton pour changer la plage de la session</string>
|
||||||
|
<string name="agenda_range_this_month">Ce mois</string>
|
||||||
|
<string name="agenda_range_week">Les 7 jours prochains</string>
|
||||||
|
<string name="agenda_range_month">Les 30 jours prochains</string>
|
||||||
|
<string name="agenda_range_custom_hint">Jours</string>
|
||||||
|
<string name="agenda_range_override_hint">Juste pour l’instant — réinitialise votre plage d’enregistrement lorsque vous rouvrez Calendula.</string>
|
||||||
|
<string name="agenda_range_showing_label">Afficher tous les événements à venir pour</string>
|
||||||
|
<plurals name="agenda_range_days">
|
||||||
|
<item quantity="one">%d jour</item>
|
||||||
|
<item quantity="many">%d jours</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="settings_section_views">Vues</string>
|
||||||
|
<string name="settings_quick_switch_header">Bouton d’interrupteur rapide</string>
|
||||||
|
<string name="settings_quick_switch_hint">Choisissez les vues que le bouton en haut à droite fait défiler, puis faites-les glisser pour les réorganiser. Les vues désactivées restent accessibles depuis le menu de navigation.</string>
|
||||||
|
<string name="settings_drawer_order_header">Menu navigation</string>
|
||||||
|
<string name="settings_drawer_order_hint">Faites glisser pour réorganiser les vues répertoriées dans le menu de navigation.</string>
|
||||||
|
<string name="reorder_drag_handle">Faites glisser pour réorganiser</string>
|
||||||
|
<string name="settings_section_event_form">Nouveau formulaire d’événement</string>
|
||||||
|
<string name="settings_form_fields_hint">Champs affichés par défaut — tout le reste se trouve derrière « Plus de champs »</string>
|
||||||
|
<string name="settings_autofocus_title">Titre principal du nouvel événement</string>
|
||||||
|
<string name="settings_autofocus_title_hint">Lorsque vous démarrez un nouvel événement, placez le curseur dans le champ du titre et ouvrez immédiatement le clavier.</string>
|
||||||
|
<string name="settings_color_unsupported">Autoriser les couleurs dans les calendriers non pris en charge</string>
|
||||||
|
<string name="settings_color_unsupported_hint">Certains calendriers (par exemple, certains CalDAV) ne publient aucun ensemble de couleurs, une couleur d’événement personnalisée peut être supprimée ou remplacée lors de leur prochaine synchronisation. C’est une limitation de ces calendriers, pas quelque chose que Calendula peut réparer.</string>
|
||||||
|
<string name="settings_section_notifications">Notifications</string>
|
||||||
|
<string name="settings_reminders">rappels d\'événements</string>
|
||||||
|
<string name="settings_reminders_hint">Vous voyez des rappels deux fois ? Une autre application de calendrier les publie aussi — désactivez-les dans l’un des deux.</string>
|
||||||
|
<string name="settings_default_reminder">rappel par défaut</string>
|
||||||
|
<string name="settings_default_reminder_allday">événements d\'une journée entière</string>
|
||||||
|
<string name="settings_allday_reminder_time">Heure de rappel toute la journée</string>
|
||||||
|
<string name="settings_allday_reminder_time_hint">Les rappels pour les événements d’une journée entière se déclenchent à %1$s</string>
|
||||||
|
<string name="reminder_none">Aucun</string>
|
||||||
|
<string name="reminder_use_default">Utiliser le rappel par défaut</string>
|
||||||
|
<string name="reminder_custom_amount">montant</string>
|
||||||
|
<string name="reminder_custom_set">fixer</string>
|
||||||
|
<string name="settings_calendar_reminders_title">Rappels par calendrier</string>
|
||||||
|
<string name="settings_calendar_reminders_hint">Remplacer le paramètre par défaut par calendrier, séparément pour les événements planifiés et sur une journée. Un calendrier peut conserver le paramètre par défaut, l’abandonner ou définir le sien.</string>
|
||||||
|
<string name="settings_calendar_reminder_inherits">Défaut (%1$s)</string>
|
||||||
|
<string name="settings_reliable_delivery">livraison fiable</string>
|
||||||
|
<string name="settings_reliable_delivery_hint">Android peut retarder les rappels pour économiser la batterie. Exempter Calendula afin qu’ils arrivent à temps.</string>
|
||||||
|
<string name="settings_reliable_delivery_exempt">Exempté de l’optimisation de la batterie — les rappels arrivent à temps.</string>
|
||||||
|
<string name="settings_snooze_duration">Durée de répétition</string>
|
||||||
|
<string name="settings_manage_calendars">gérer les calendriers</string>
|
||||||
|
<string name="settings_manage_calendars_hint">Créer des calendriers locaux, gérer les calendriers synchronisés</string>
|
||||||
|
<string name="settings_section_language">Langue</string>
|
||||||
|
<string name="settings_language">langue de l\'application</string>
|
||||||
|
<string name="settings_language_auto">par défaut du système</string>
|
||||||
|
<string name="settings_translate">aider à traduire</string>
|
||||||
|
<string name="settings_translate_hint">Ajouter ou améliorer une langue sur Weblate</string>
|
||||||
|
<string name="settings_appearance_subtitle">Thème, vue par défaut, début de la semaine</string>
|
||||||
|
<string name="settings_views_subtitle">Bouton de sélection rapide et ordre du menu</string>
|
||||||
|
<string name="settings_event_form_subtitle">Champs par défaut pour les nouveaux événements</string>
|
||||||
|
<string name="settings_notifications_subtitle">rappels d\'événement</string>
|
||||||
|
<string name="settings_special_dates_subtitle">Anniversaires et dates de naissance des contacts</string>
|
||||||
|
<string name="settings_section_special_dates">Contact, dates spéciales</string>
|
||||||
|
<string name="settings_special_dates_enable">Afficher les dates de contact</string>
|
||||||
|
<string name="settings_special_dates_enable_hint">Reprendre les anniversaires et autres dates de vos contacts dans des calendriers locaux. Lit les contacts uniquement sur cet appareil — rien n’est téléchargé et vos contacts ne sont jamais modifiés.</string>
|
||||||
|
<string name="settings_special_dates_type_birthday">Anniversaires</string>
|
||||||
|
<string name="settings_special_dates_type_anniversary">Anniversaires</string>
|
||||||
|
<string name="settings_special_dates_type_custom">Autres dates</string>
|
||||||
|
<string name="settings_special_dates_template">Format du titre</string>
|
||||||
|
<string name="settings_special_dates_template_hint">Utilisez {name} pour le contact et {year} pour l’année (l’année de naissance ou l’année de début d’un anniversaire, masqué si inconnu).</string>
|
||||||
|
<string name="settings_special_dates_show_year">Montrer l\'année</string>
|
||||||
|
<string name="settings_special_dates_show_year_hint">Inclure {year} dans les titres lorsque l\'année est connue</string>
|
||||||
|
<string name="settings_special_dates_sync_now">Synchroniser maintenant</string>
|
||||||
|
<string name="settings_special_dates_never_synced">Pas encore synchronisé</string>
|
||||||
|
<string name="settings_special_dates_last_synced">Dernière synchronisation %1$s</string>
|
||||||
|
<string name="settings_special_dates_calendar_hint">Définissez la couleur et la visibilité de chaque calendrier dans les paramètres des calendriers.</string>
|
||||||
|
<string name="settings_calendar_reminders_managed_hint">Définir dans les dates spéciales de contact</string>
|
||||||
|
<string name="settings_special_dates_paused_title">suspendu</string>
|
||||||
|
<string name="settings_special_dates_paused_hint">Calendula ne peut plus lire vos contacts, donc ces calendriers ne se mettent pas à jour.</string>
|
||||||
|
<string name="settings_special_dates_disable_title">Désactiver les dates de contact ?</string>
|
||||||
|
<string name="settings_special_dates_disable_all_message">Cela supprime les calendriers de contact et leurs événements. Tous les rappels ou notes que vous leur avez ajoutés seront perdus.</string>
|
||||||
|
<string name="settings_special_dates_disable_type_message">Cela supprime le calendrier « %1$s » et ses événements. Tous les rappels ou notes que vous y avez ajoutés seront perdus.</string>
|
||||||
|
<string name="settings_special_dates_disable_confirm">désactiver</string>
|
||||||
|
<string name="settings_section_about">A propos</string>
|
||||||
|
<string name="settings_license">Licence</string>
|
||||||
|
<string name="settings_license_value">MIT</string>
|
||||||
|
<string name="settings_about_author">par Jean-Luc Makiola</string>
|
||||||
|
<string name="settings_about_source">Source</string>
|
||||||
|
<string name="settings_about_support">soutenir le développement</string>
|
||||||
|
<string name="settings_about_version">Version %1$s</string>
|
||||||
|
<string name="settings_about_logo_desc">Icône de l’application Calendula</string>
|
||||||
|
<string name="settings_report_problem">Rapporter un problème</string>
|
||||||
|
<string name="settings_report_problem_hint">Envoyer un rapport de plantage ou ouvrir le suivi des problèmes</string>
|
||||||
|
<string name="calendars_local_header">Vos calendriers</string>
|
||||||
|
<string name="calendars_local_empty">Pas encore de calendrier local. Créez-en un pour conserver les événements uniquement sur cet appareil.</string>
|
||||||
|
<string name="calendars_add">Ajouter un calendrier</string>
|
||||||
|
<string name="calendars_disable_hint">Désactivez un calendrier pour le retirer de l’application, ses événements, ses filtres et ses sélecteurs. Rien n’est supprimé et vous pouvez le réactiver à tout moment ici.</string>
|
||||||
|
<string name="calendars_show_in_app_a11y">Afficher « %1$s » dans l’application</string>
|
||||||
|
<string name="calendars_synced_header">Calendriers synchronisés</string>
|
||||||
|
<string name="calendars_synced_hint">Ils proviennent de comptes sur votre appareil. Créez et modifiez-les dans leur propre application.</string>
|
||||||
|
<string name="calendars_manage_in_app">Gérer dans l’application</string>
|
||||||
|
<string name="calendars_account_menu_a11y">Plus d’options pour %1$s</string>
|
||||||
|
<string name="calendars_enable_all">Tout activer</string>
|
||||||
|
<string name="calendars_disable_all">Tout désactiver</string>
|
||||||
|
<string name="calendars_add_account">Ajouter un compte</string>
|
||||||
|
<string name="calendars_new_title">Nouveau calendrier</string>
|
||||||
|
<string name="calendars_edit_title">Editer un agenda</string>
|
||||||
|
<string name="calendars_name_label">Nom</string>
|
||||||
|
<string name="calendars_description_hint">Ajouter une description</string>
|
||||||
|
<string name="calendars_delete_confirm_title">Supprimer un calendrier ?</string>
|
||||||
|
<string name="calendars_delete_confirm_message">\"%1$s\" et tous ses événements seront définitivement retirés de cet appareil.</string>
|
||||||
|
<string name="calendars_write_error">Impossible de sauvegarder les changements.</string>
|
||||||
|
<string name="calendars_backup_hint">Les calendriers locaux ne sont synchronisés nulle part, alors exportez-les vers un fichier .ics pour en garder une copie.</string>
|
||||||
|
<string name="calendars_backup_action">Exporter en tant que fichier .ics</string>
|
||||||
|
<string name="calendars_export_title">Exporter des calendriers</string>
|
||||||
|
<string name="calendars_export_hint">Choisissez les calendriers à inclure dans le fichier .ics.</string>
|
||||||
|
<string name="calendars_export_action">Exporter</string>
|
||||||
|
<string name="calendars_restore_header">Restaurer</string>
|
||||||
|
<string name="calendars_restore_action">Restaurer à partir du fichier .ics</string>
|
||||||
|
<string name="calendars_restore_hint">Importez des événements à partir d’une sauvegarde ou d’une autre application de calendrier.</string>
|
||||||
|
<string name="calendars_auto_backup">sauvegarde automatique</string>
|
||||||
|
<string name="calendars_auto_backup_hint">Exportez périodiquement vos calendriers locaux dans un dossier en tant que fichier .ics.</string>
|
||||||
|
<string name="calendars_auto_backup_folder">dossier de sauvegarde</string>
|
||||||
|
<string name="calendars_auto_backup_folder_unset">Cliquez pour choisir un dossier</string>
|
||||||
|
<string name="calendars_auto_backup_interval">Intervalle</string>
|
||||||
|
<string name="calendars_auto_backup_interval_min">Minimum 30 minutes.</string>
|
||||||
|
<string name="calendars_auto_backup_status_never">Pas encore de sauvegarde automatique</string>
|
||||||
|
<string name="calendars_auto_backup_status_ok">Dernière sauvegarde : %1$s</string>
|
||||||
|
<string name="calendars_auto_backup_status_failed">La dernière sauvegarde a échoué : %1$s</string>
|
||||||
|
<string name="backup_channel_description">Avertit si les sauvegardes automatiques échouent de manière répétée.</string>
|
||||||
|
<string name="backup_failed_title">La sauvegarde automatique a échoué</string>
|
||||||
|
<string name="backup_failed_text">Calendula n’a pas pu écrire le fichier de sauvegarde. Vérifiez le dossier de sauvegarde dans les paramètres.</string>
|
||||||
|
<string name="calendars_backup_failed">Impossible d’exporter la sauvegarde.</string>
|
||||||
|
<plurals name="calendars_backup_done">
|
||||||
|
<item quantity="one">Événement %d exporté.</item>
|
||||||
|
<item quantity="many">Événements %d exportés.</item>
|
||||||
|
<item quantity="other">Événements %d exportés.</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="import_title">importer des événements</string>
|
||||||
|
<string name="import_target_header">ajouter au calendrier</string>
|
||||||
|
<string name="import_empty">Aucun événement trouvé dans ce fichier.</string>
|
||||||
|
<string name="import_failed">Impossible de lire ce fichier.</string>
|
||||||
|
<string name="import_no_calendar">Pas de calendrier lisible à importer. Créez d’abord un calendrier local.</string>
|
||||||
|
<string name="import_done_title">Importation terminée</string>
|
||||||
|
<string name="import_done_dedup_note">Les événements déjà présents dans le calendrier ont été ignorés.</string>
|
||||||
|
<string name="import_done_added_label">ajouté</string>
|
||||||
|
<string name="import_done_skipped_label">Dupliquer</string>
|
||||||
|
<string name="import_warning_recurrence">Certaines occurrences modifiées d’événements récurrents ont été ignorées.</string>
|
||||||
|
<string name="import_warning_no_start">Un événement sans heure de début a été ignoré.</string>
|
||||||
|
<string name="import_warning_attendees">Les listes d’invités n’ont pas été importées.</string>
|
||||||
|
<string name="import_warning_timezone">Un fuseau horaire inconnu est revenu sur celui de votre appareil.</string>
|
||||||
|
<string name="import_button">Import</string>
|
||||||
|
<plurals name="import_title_count">
|
||||||
|
<item quantity="one">Importation de l’événement %d</item>
|
||||||
|
<item quantity="many">Importation des événements %d</item>
|
||||||
|
<item quantity="other">Importation des événements %d</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="import_event_count">
|
||||||
|
<item quantity="one">%d événement dans ce fichier.</item>
|
||||||
|
<item quantity="many">%d événements dans ce fichier.</item>
|
||||||
|
<item quantity="other">%d événements dans ce fichier.</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="import_action">
|
||||||
|
<item quantity="one">Importer l’événement %d</item>
|
||||||
|
<item quantity="many">Importer les événements %d</item>
|
||||||
|
<item quantity="other">Importer les événements %d</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="import_done_imported">
|
||||||
|
<item quantity="one">Événement %d importé.</item>
|
||||||
|
<item quantity="many">Événements %d importés.</item>
|
||||||
|
<item quantity="other">Événements %d importés.</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="import_done_skipped">
|
||||||
|
<item quantity="one">Ignoré %d déjà dans ce calendrier.</item>
|
||||||
|
<item quantity="many">Ignorés %d déjà dans ce calendrier.</item>
|
||||||
|
<item quantity="other">Ignorés %d déjà dans ce calendrier.</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="shortcut_new_event_long">créer un événement</string>
|
||||||
|
<string name="settings_qs_tile">Ajouter des paramètres rapides</string>
|
||||||
|
<string name="settings_qs_tile_hint">Ajoutez un bouton « Nouvel événement » au panneau Paramètres rapides.</string>
|
||||||
|
<string name="crash_dialog_title">%1$s a planté</string>
|
||||||
|
<string name="crash_dialog_message">%1$s a été fermé de manière inattendue la dernière fois. Vous pouvez aider à le corriger en envoyant ce rapport en tant que problème. Il reste sur votre appareil jusqu’à ce que vous choisissiez de le partager, et n’inclut aucune donnée personnelle ni contenu de calendrier — seuls les détails techniques ci-dessous.</string>
|
||||||
|
<string name="crash_dialog_report">rapport</string>
|
||||||
|
<string name="crash_dialog_dismiss">pas maintenant</string>
|
||||||
|
<string name="crash_report_issue_title">rapport de plantage</string>
|
||||||
|
<string name="crash_report_clip_label">%1$s rapport de plantage</string>
|
||||||
|
<string name="crash_report_copied">Rapport copié dans votre presse-papiers</string>
|
||||||
|
<string name="crash_report_open_failed">Impossible d’ouvrir le suivi des problèmes. Le rapport se trouve dans votre presse-papiers.</string>
|
||||||
|
<string name="crash_report_body_template">Merci d’avoir signalé un plantage dans %1$s. Veuillez ajouter tout ce dont vous vous souvenez sur ce que vous faisiez, puis soumettez-le.\n\n### Que s’est-il passé\n\n\n### Rapport de plantage\n%2$s\n</string>
|
||||||
|
<string name="crash_report_body_paste">_(Le rapport était trop long pour ce lien — collez-le depuis votre presse-papiers ici.)_</string>
|
||||||
|
<string name="special_dates_calendar_birthday">anniversaires</string>
|
||||||
|
<string name="special_dates_calendar_anniversary">Anniversaires</string>
|
||||||
|
<string name="special_dates_calendar_custom">dates spéciales</string>
|
||||||
|
<string name="special_dates_default_title_birthday">{name}\'s birthday ({year})</string>
|
||||||
|
<string name="special_dates_default_title_anniversary">{name}, anniversaire ({year})</string>
|
||||||
|
<string name="special_dates_default_title_custom">{name}</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<string name="state_failure_no_calendars_action">Apri le impostazioni calendario di sistema</string>
|
<string name="state_failure_no_calendars_action">Apri le impostazioni calendario di sistema</string>
|
||||||
<string name="state_failure_provider">Errore nella lettura del calendario.</string>
|
<string name="state_failure_provider">Errore nella lettura del calendario.</string>
|
||||||
<string name="permission_rationale_title">Gestisci magnificamente tutti i tuoi eventi</string>
|
<string name="permission_rationale_title">Gestisci magnificamente tutti i tuoi eventi</string>
|
||||||
<string name="permission_rationale_body">Calendula ha bisogno di accedere al tuo calendario per mostrare e gestire gli eventi. È l\'unica cosa di cui ha bisogno e nessuna informazione lascerà mai il tuo dispositivo.</string>
|
<string name="permission_rationale_body">Calendula ha bisogno di accedere al tuo calendario per mostrare e gestire gli eventi. È l\'unica cosa di cui ha bisogno: nessuna informazione lascerà mai il tuo dispositivo.</string>
|
||||||
<string name="permission_request_button">Concedi l\'accesso al calendario</string>
|
<string name="permission_request_button">Concedi l\'accesso al calendario</string>
|
||||||
<string name="permission_denied_title">Accesso al calendario negato</string>
|
<string name="permission_denied_title">Accesso al calendario negato</string>
|
||||||
<string name="permission_denied_body">Calendula non può mostrare gli eventi senza accesso al calendario. Puoi concederlo dalle impostazioni di sistema.</string>
|
<string name="permission_denied_body">Calendula non può mostrare gli eventi senza accesso al calendario. Puoi concederlo dalle impostazioni di sistema.</string>
|
||||||
@@ -228,7 +228,7 @@
|
|||||||
<string name="widget_new_event">Nuovo evento</string>
|
<string name="widget_new_event">Nuovo evento</string>
|
||||||
<string name="widget_needs_permission">Apri Calendula per concedere l\'accesso al calendario</string>
|
<string name="widget_needs_permission">Apri Calendula per concedere l\'accesso al calendario</string>
|
||||||
<string name="widget_prev_month">Mese precedente</string>
|
<string name="widget_prev_month">Mese precedente</string>
|
||||||
<string name="filter_title">Calendari</string>
|
<string name="filter_title">Calendario</string>
|
||||||
<string name="settings_section_appearance">Aspetto</string>
|
<string name="settings_section_appearance">Aspetto</string>
|
||||||
<string name="settings_theme">Tema</string>
|
<string name="settings_theme">Tema</string>
|
||||||
<string name="settings_theme_system">Sistema</string>
|
<string name="settings_theme_system">Sistema</string>
|
||||||
@@ -253,18 +253,18 @@
|
|||||||
<string name="settings_past_events_hide">Nascondi</string>
|
<string name="settings_past_events_hide">Nascondi</string>
|
||||||
<string name="settings_agenda_header">Agenda</string>
|
<string name="settings_agenda_header">Agenda</string>
|
||||||
<string name="settings_agenda_range">Intervallo dell\'Agenda</string>
|
<string name="settings_agenda_range">Intervallo dell\'Agenda</string>
|
||||||
<string name="settings_agenda_range_hint">Anticipo con cui vengono visualizzati gli eventi nell\'Agenda.</string>
|
<string name="settings_agenda_range_hint">Intervallo di visualizzazione degli eventi nell\'Agenda.</string>
|
||||||
<string name="settings_agenda_widget_range">Intervallo del widget Agenda</string>
|
<string name="settings_agenda_widget_range">Intervallo di visualizzazione degli eventi nel widget Agenda</string>
|
||||||
<string name="settings_agenda_widget_range_hint">Anticipo con cui vengono visualizzati gli eventi nel widget Agenda.</string>
|
<string name="settings_agenda_widget_range_hint">Intervallo di visualizzazione degli eventi nel widget Agenda.</string>
|
||||||
<string name="settings_agenda_range_bar">Barra dell\'intervallo</string>
|
<string name="settings_agenda_range_bar">Barra dell\'intervallo</string>
|
||||||
<string name="settings_agenda_range_bar_hint">Mostra una barra sopra l\'agenda che indica le date mostrate, con un pulsante per cambiare l\'intervallo scelto per la sessione</string>
|
<string name="settings_agenda_range_bar_hint">Nella vista Agenda, mostra una barra che indica l\'intervallo di date visualizzato, con un pulsante per cambiarlo</string>
|
||||||
<string name="agenda_range_day">Oggi</string>
|
<string name="agenda_range_day">Oggi</string>
|
||||||
<string name="agenda_range_this_month">Questo mese</string>
|
<string name="agenda_range_this_month">Questo mese</string>
|
||||||
<string name="agenda_range_week">Prossimi 7 giorni</string>
|
<string name="agenda_range_week">Prossimi 7 giorni</string>
|
||||||
<string name="agenda_range_month">Prossimi 30 giorni</string>
|
<string name="agenda_range_month">Prossimi 30 giorni</string>
|
||||||
<string name="agenda_range_custom">Personalizza…</string>
|
<string name="agenda_range_custom">Personalizza…</string>
|
||||||
<string name="agenda_range_custom_hint">Giorni</string>
|
<string name="agenda_range_custom_hint">Giorni</string>
|
||||||
<string name="agenda_range_override_hint">Solo questa volta, ripristina l\'intervallo salvato al riavvio di Calendula.</string>
|
<string name="agenda_range_override_hint">L\'intervallo viene modificato solo questa volta. La prossima volta che apri Calendula verrà visualizzato l\'intervallo di default.</string>
|
||||||
<string name="agenda_range_showing_label">Mostra tutti gli eventi in arrivo per</string>
|
<string name="agenda_range_showing_label">Mostra tutti gli eventi in arrivo per</string>
|
||||||
<plurals name="agenda_range_days">
|
<plurals name="agenda_range_days">
|
||||||
<item quantity="one">%d giorno</item>
|
<item quantity="one">%d giorno</item>
|
||||||
@@ -272,26 +272,26 @@
|
|||||||
<item quantity="other">%d giorni</item>
|
<item quantity="other">%d giorni</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="settings_section_event_form">Scheda Nuovo Evento</string>
|
<string name="settings_section_event_form">Scheda Nuovo Evento</string>
|
||||||
<string name="settings_form_fields_hint">Campi mostrati di default, tutti gli altri saranno in \"Altri campi\"</string>
|
<string name="settings_form_fields_hint">Campi di default, tutti gli altri saranno in \"Altri campi\"</string>
|
||||||
<string name="settings_autofocus_title">Subito al titolo su un nuovo evento</string>
|
<string name="settings_autofocus_title">Focus sul titolo</string>
|
||||||
<string name="settings_autofocus_title_hint">Quando crei un nuovo evento, posiziona subito il cursore sul campo Titolo e mostra la tastiera.</string>
|
<string name="settings_autofocus_title_hint">Quando crei un nuovo evento posiziona subito il cursore sul campo Titolo e mostra la tastiera.</string>
|
||||||
<string name="settings_color_unsupported">Abilita i colori sui calendari non supportati</string>
|
<string name="settings_color_unsupported">Abilita i colori sui calendari anche sui calendari che non li supportano</string>
|
||||||
<string name="settings_color_unsupported_hint">Alcuni calendari (e.g. alcuni CalDAV) non pubblicano il set di colori di cui sono dotati; i colori personalizzati assegnati a un evento possono essere persi o sovrascritti alla prossima sincronizzazione. È una limitazione di questo tipo di calendari, Calendula non può farci nulla.</string>
|
<string name="settings_color_unsupported_hint">Alcuni calendari (e.g. alcuni CalDAV) non comunicano il set di colori di cui sono dotati; se assegni colori personalizzati agli eventi può succedere che vengano persi o sovrascritti alla prossima sincronizzazione. È una limitazione di questo tipo di calendari, Calendula non può farci nulla.</string>
|
||||||
<string name="settings_section_notifications">Notifiche</string>
|
<string name="settings_section_notifications">Promemoria</string>
|
||||||
<string name="settings_reminders_hint">Vedi promemoria duplicati? Vuol dire che li sta mostrando anche un\'altra app. Disattivali qui o nell\'altra app.</string>
|
<string name="settings_reminders_hint">Vedi promemoria duplicati? Vuol dire che li sta mostrando anche un\'altra app. Disattivali qui o nell\'altra app.</string>
|
||||||
<string name="settings_default_reminder_allday">Eventi giornalieri</string>
|
<string name="settings_default_reminder_allday">Promemoria per eventi giornalieri</string>
|
||||||
<string name="settings_allday_reminder_time">Ora per i promemoria degli eventi giornalieri</string>
|
<string name="settings_allday_reminder_time">Orario di invio dei promemoria degli eventi giornalieri</string>
|
||||||
<string name="settings_allday_reminder_time_hint">I promemoria per gli eventi giornalieri vengono mandati alle %1$s</string>
|
<string name="settings_allday_reminder_time_hint">I promemoria per gli eventi giornalieri vengono inviati alle %1$s</string>
|
||||||
<string name="reminder_none">Nessuno</string>
|
<string name="reminder_none">Nessuno</string>
|
||||||
<string name="reminder_use_default">Usa il promemoria standard</string>
|
<string name="reminder_use_default">Promemoria standard</string>
|
||||||
<string name="reminder_custom_amount">Valore</string>
|
<string name="reminder_custom_amount">Valore</string>
|
||||||
<string name="reminder_custom_set">Imposta</string>
|
<string name="reminder_custom_set">Imposta</string>
|
||||||
<string name="settings_calendar_reminders_title">Promemoria per calendario</string>
|
<string name="settings_calendar_reminders_title">Promemoria per calendario</string>
|
||||||
<string name="settings_calendar_reminders_hint">Sovrascrivi i reminder standard con reminder specifici per calendario, separati per eventi giornalieri ed eventi ad ore. Ogni calendario può usare i promemoria di default, ignorarli o avere i suoi personalizzati.</string>
|
<string name="settings_calendar_reminders_hint">Sostituisci i promemoria standard con promemoria specifici per ogni calendario, dettagliando quelli per eventi standard ed quelli per eventi giornalieri. Ogni calendario può usare i promemoria di default, ignorarli o avere i suoi personalizzati.</string>
|
||||||
<string name="settings_calendar_reminder_inherits">Default (%1$s)</string>
|
<string name="settings_calendar_reminder_inherits">Default (%1$s)</string>
|
||||||
<string name="settings_reliable_delivery">Notifiche affidabili</string>
|
<string name="settings_reliable_delivery">Notifiche affidabili</string>
|
||||||
<string name="settings_reliable_delivery_hint">Android potrebbe posticipare le notifiche per risparmiare batteria. Escludi Calendula così che le notifiche possano arrivare puntuali.</string>
|
<string name="settings_reliable_delivery_hint">Android potrebbe posticipare le notifiche per migliorare la durata batteria. Escludi Calendula dall\'ottimizzazione della batteria per far arrivare puntuali i promemoria.</string>
|
||||||
<string name="settings_reliable_delivery_exempt">Escludi dall\'ottimizzazione batteria. I promemoria arriveranno puntuali.</string>
|
<string name="settings_reliable_delivery_exempt">I promemoria arriveranno puntuali.</string>
|
||||||
<string name="settings_snooze_duration">Durata del posticipo</string>
|
<string name="settings_snooze_duration">Durata del posticipo</string>
|
||||||
<string name="settings_section_calendars">Calendari</string>
|
<string name="settings_section_calendars">Calendari</string>
|
||||||
<string name="settings_manage_calendars">Gestisci calendari</string>
|
<string name="settings_manage_calendars">Gestisci calendari</string>
|
||||||
@@ -314,17 +314,17 @@
|
|||||||
<string name="settings_report_problem">Segnala un problema</string>
|
<string name="settings_report_problem">Segnala un problema</string>
|
||||||
<string name="settings_report_problem_hint">Invia un crash report o apri il gestore dei problemi</string>
|
<string name="settings_report_problem_hint">Invia un crash report o apri il gestore dei problemi</string>
|
||||||
<string name="calendars_title">Calendari</string>
|
<string name="calendars_title">Calendari</string>
|
||||||
<string name="calendars_local_header">I tuoi calendari</string>
|
<string name="calendars_local_header">Calendari locali</string>
|
||||||
<string name="calendars_local_empty">Non ci sono ancora calendari locali. Creane uno per avere gli eventi solo su questo dispositivo.</string>
|
<string name="calendars_local_empty">Non ci sono ancora calendari locali. Creane uno con eventi salvati solo su questo dispositivo.</string>
|
||||||
<string name="calendars_add">Aggiungi calendario</string>
|
<string name="calendars_add">Aggiungi calendario</string>
|
||||||
<string name="calendars_disable_hint">.Disattiva un calendario per non vederlo più sull\'app. Non viene cancellato nulla, puoi riattivarlo in qualsiasi momento.</string>
|
<string name="calendars_disable_hint">Disattiva un calendario per non vederlo più sull\'app. Non viene cancellato nulla, puoi riattivarlo in qualsiasi momento.</string>
|
||||||
<string name="calendars_show_in_app_a11y">Mostra \"%1$s\" nell\'app</string>
|
<string name="calendars_show_in_app_a11y">Mostra \"%1$s\" nell\'app</string>
|
||||||
<string name="calendars_synced_header">Calendari sincronizzati</string>
|
<string name="calendars_synced_header">Calendari sincronizzati</string>
|
||||||
<string name="calendars_synced_hint">Questi vengono dagli account presenti sul tuo dispositivo. Creali e modificali nella loro app.</string>
|
<string name="calendars_synced_hint">Questi calendari vengono dagli account sincronizzati sul tuo dispositivo. Puoi modificarli o crearne di nuovi dall\'app di sincronizzazione.</string>
|
||||||
<string name="calendars_manage_in_app">Gestisci in app</string>
|
<string name="calendars_manage_in_app">Gestisci in app</string>
|
||||||
<string name="calendars_account_menu_a11y">Altre opzioni per %1$s</string>
|
<string name="calendars_account_menu_a11y">Altre opzioni per %1$s</string>
|
||||||
<string name="calendars_enable_all">Attiva tutti</string>
|
<string name="calendars_enable_all">Mostra tutti</string>
|
||||||
<string name="calendars_disable_all">Disattiva tutti</string>
|
<string name="calendars_disable_all">Nascondi tutti</string>
|
||||||
<string name="calendars_add_account">Aggiungi account</string>
|
<string name="calendars_add_account">Aggiungi account</string>
|
||||||
<string name="calendars_new_title">Nuovo calendario</string>
|
<string name="calendars_new_title">Nuovo calendario</string>
|
||||||
<string name="calendars_edit_title">Modifica calendario</string>
|
<string name="calendars_edit_title">Modifica calendario</string>
|
||||||
@@ -350,7 +350,7 @@
|
|||||||
<string name="backup_channel_name">Backup</string>
|
<string name="backup_channel_name">Backup</string>
|
||||||
<string name="backup_channel_description">Avvisa se i backup automatici falliscono ripetutamente.</string>
|
<string name="backup_channel_description">Avvisa se i backup automatici falliscono ripetutamente.</string>
|
||||||
<string name="backup_failed_title">Backup automatico fallito</string>
|
<string name="backup_failed_title">Backup automatico fallito</string>
|
||||||
<string name="backup_failed_text">Calendula non è riuscito a esportare il file di backup. Controlla la cartella di backup nelle Impostazioni.</string>
|
<string name="backup_failed_text">Calendula non è riuscito a esportare il file di backup. Verifica quale sia la cartella di backup nelle Impostazioni.</string>
|
||||||
<string name="calendars_backup_failed">Impossibile esportare il backup.</string>
|
<string name="calendars_backup_failed">Impossibile esportare il backup.</string>
|
||||||
<plurals name="calendars_backup_done">
|
<plurals name="calendars_backup_done">
|
||||||
<item quantity="one">Esportato %d evento.</item>
|
<item quantity="one">Esportato %d evento.</item>
|
||||||
@@ -390,17 +390,17 @@
|
|||||||
</plurals>
|
</plurals>
|
||||||
<string name="shortcut_new_event_long">Crea un nuovo evento</string>
|
<string name="shortcut_new_event_long">Crea un nuovo evento</string>
|
||||||
<string name="qs_tile_new_event_label">Nuovo evento</string>
|
<string name="qs_tile_new_event_label">Nuovo evento</string>
|
||||||
<string name="settings_qs_tile">Aggiungi un toggle nelle Impostazioni Rapide</string>
|
<string name="settings_qs_tile">Aggiungi un riquadro nelle Impostazioni Rapide</string>
|
||||||
<string name="settings_qs_tile_hint">Aggiungi un toggle \"Nuovo evento\" nel pannello delle Impostazioni Rapide.</string>
|
<string name="settings_qs_tile_hint">Aggiungi un riquadro \"Nuovo evento\" nel pannello delle Impostazioni Rapide.</string>
|
||||||
<string name="crash_dialog_title">Calendula è crashato</string>
|
<string name="crash_dialog_title">%1$s crashato</string>
|
||||||
<string name="crash_dialog_message">Calendula si è chiuso inaspettatamente l\'ultima volta. Puoi contribuire allo sviluppo inviando questo report, che rimane sul tuo dispositivo finchè non scegli di inviarlo e non include informazioni personali o sui tuoi calendari:solo le informazioni tecniche qui sotto.</string>
|
<string name="crash_dialog_message">%1$s si è chiuso inaspettatamente l\'ultima volta. Puoi contribuire allo sviluppo inviando questo report, che rimane sul tuo dispositivo finchè non scegli di inviarlo e non include informazioni personali o sui tuoi calendari: solo le informazioni tecniche qui sotto.</string>
|
||||||
<string name="crash_dialog_report">Report</string>
|
<string name="crash_dialog_report">Report</string>
|
||||||
<string name="crash_dialog_dismiss">Non ora</string>
|
<string name="crash_dialog_dismiss">Non ora</string>
|
||||||
<string name="crash_report_issue_title">Crash report</string>
|
<string name="crash_report_issue_title">Crash report</string>
|
||||||
<string name="crash_report_clip_label">Calendula crash report</string>
|
<string name="crash_report_clip_label">%1$s crash report</string>
|
||||||
<string name="crash_report_copied">Report copiato negli Appunti</string>
|
<string name="crash_report_copied">Report copiato negli Appunti</string>
|
||||||
<string name="crash_report_open_failed">Impossibile aprire il gestore dei problemi. Il report è sugli Appunti.</string>
|
<string name="crash_report_open_failed">Impossibile aprire il gestore dei problemi. Il report è negli Appunti.</string>
|
||||||
<string name="crash_report_body_template">Grazie per riportare un crash di Calendula. Per favore, aggiungi ogni informazione che ricordi in merito a quello che stavi facendo, quindi invia\n\n### Cosa è successo\n\n\n###Crash report\n%1$s\n</string>
|
<string name="crash_report_body_template">Grazie per segnalare un crash di %1$s. Per favore, aggiungi ogni informazione che ricordi in merito a quello che stavi facendo, quindi invia\n\n### Cosa è successo\n\n\n###Crash report\n%2$s\n</string>
|
||||||
<string name="crash_report_body_paste">_(Il report era troppo lungo per questo link - copialo qui dai tuoi Appunti.)_</string>
|
<string name="crash_report_body_paste">_(Il report era troppo lungo per questo link - copialo qui dai tuoi Appunti.)_</string>
|
||||||
<string name="event_detail_recurrence">Ricorrenza</string>
|
<string name="event_detail_recurrence">Ricorrenza</string>
|
||||||
<string name="event_detail_recurring">Evento ricorrente</string>
|
<string name="event_detail_recurring">Evento ricorrente</string>
|
||||||
@@ -414,14 +414,14 @@
|
|||||||
<string name="settings_title">Impostazioni</string>
|
<string name="settings_title">Impostazioni</string>
|
||||||
<string name="settings_back">Indietro</string>
|
<string name="settings_back">Indietro</string>
|
||||||
<string name="agenda_range_this_week">Questa settimana</string>
|
<string name="agenda_range_this_week">Questa settimana</string>
|
||||||
<string name="settings_reminders">Promemoria evento</string>
|
<string name="settings_reminders">Promemoria per gli eventi</string>
|
||||||
<string name="settings_default_reminder">Promemoria standard</string>
|
<string name="settings_default_reminder">Promemoria per eventi standard</string>
|
||||||
<string name="settings_notifications_subtitle">Promemoria evento</string>
|
<string name="settings_notifications_subtitle">Promemoria evento</string>
|
||||||
<string name="shortcut_new_event_short">Nuovo evento</string>
|
<string name="shortcut_new_event_short">Nuovo evento</string>
|
||||||
<string name="event_edit_managed_hint">Gestito da “%1$s” - titolo, data e ripetizioni sono sincronizzate dai tuoi contatti. Promemoria e note sono modificabili dall\'utente.</string>
|
<string name="event_edit_managed_hint">Gestito da “%1$s” - titolo, data e ripetizioni sono sincronizzate dai tuoi contatti. Promemoria e note sono modificabili dall\'utente.</string>
|
||||||
<string name="settings_font_headings">Carattere del titolo</string>
|
<string name="settings_font_headings">Carattere del titolo</string>
|
||||||
<string name="settings_font_body">Carattere del corpo</string>
|
<string name="settings_font_body">Carattere del corpo</string>
|
||||||
<string name="settings_font_system">Predefinito di sistema</string>
|
<string name="settings_font_system">Lingua di sistema</string>
|
||||||
<string name="settings_font_choose_file">Scegli file…</string>
|
<string name="settings_font_choose_file">Scegli file…</string>
|
||||||
<string name="settings_font_custom_selected">Font personalizzato</string>
|
<string name="settings_font_custom_selected">Font personalizzato</string>
|
||||||
<string name="settings_font_import_failed">Impossibile leggere il file come font</string>
|
<string name="settings_font_import_failed">Impossibile leggere il file come font</string>
|
||||||
@@ -446,11 +446,11 @@
|
|||||||
<string name="settings_special_dates_show_year_hint">Include {year} nei titoli quando è noto</string>
|
<string name="settings_special_dates_show_year_hint">Include {year} nei titoli quando è noto</string>
|
||||||
<string name="settings_special_dates_sync_now">Sincronizza adesso</string>
|
<string name="settings_special_dates_sync_now">Sincronizza adesso</string>
|
||||||
<string name="settings_special_dates_never_synced">Ancora non sincronizzato</string>
|
<string name="settings_special_dates_never_synced">Ancora non sincronizzato</string>
|
||||||
<string name="settings_special_dates_last_synced">%1$s è un tempo relativo, per esempio \"5 minuti fa\"</string>
|
<string name="settings_special_dates_last_synced">Ultima sincronizzazione %1$s</string>
|
||||||
<string name="settings_special_dates_calendar_hint">Imposta colore e visibilità di ogni calendario nelle impostazioni dei calendari.</string>
|
<string name="settings_special_dates_calendar_hint">Imposta colore e visibilità di ogni calendario nelle impostazioni dei calendari.</string>
|
||||||
<string name="settings_special_dates_paused_title">In pausa</string>
|
<string name="settings_special_dates_paused_title">In pausa</string>
|
||||||
<string name="settings_special_dates_paused_hint">Calendula non può più leggere i tuoi contatti, quindi questi calendari non vengono aggiornati.</string>
|
<string name="settings_special_dates_paused_hint">Calendula non può più leggere i tuoi contatti, quindi questi calendari non vengono aggiornati.</string>
|
||||||
<string name="settings_special_dates_grant">Concedi accesso</string>
|
<string name="settings_special_dates_grant">Concedi l\'accesso</string>
|
||||||
<string name="settings_special_dates_disable_title">Disattivare date dei contatti?</string>
|
<string name="settings_special_dates_disable_title">Disattivare date dei contatti?</string>
|
||||||
<string name="settings_special_dates_disable_all_message">Questo cancella i calendari delle date dei contatti ed i loro eventi. Ogni promemoria o note che erano stati aggiunti saranno persi.</string>
|
<string name="settings_special_dates_disable_all_message">Questo cancella i calendari delle date dei contatti ed i loro eventi. Ogni promemoria o note che erano stati aggiunti saranno persi.</string>
|
||||||
<string name="settings_special_dates_disable_type_message">Questo cancella il calendario “%1$s” ed i suoi eventi. Ogni promemoria o note che erano stati aggiunti saranno persi.</string>
|
<string name="settings_special_dates_disable_type_message">Questo cancella il calendario “%1$s” ed i suoi eventi. Ogni promemoria o note che erano stati aggiunti saranno persi.</string>
|
||||||
@@ -466,4 +466,39 @@
|
|||||||
<string name="font_lora">Lora</string>
|
<string name="font_lora">Lora</string>
|
||||||
<string name="font_jetbrains_mono">JetBrains Mono</string>
|
<string name="font_jetbrains_mono">JetBrains Mono</string>
|
||||||
<string name="special_dates_default_title_custom">{name}</string>
|
<string name="special_dates_default_title_custom">{name}</string>
|
||||||
|
<string name="event_detail_duplicate">Duplica</string>
|
||||||
|
<string name="import_reminder_prompt_title">Vuoi applicare i promemoria di default?</string>
|
||||||
|
<string name="import_reminder_prompt_body_none">Questo evento è stato importato senza nessun promemoria.</string>
|
||||||
|
<plurals name="import_reminder_prompt_body_existing">
|
||||||
|
<item quantity="one">Questo evento è stato importato con %1$d promemoria.</item>
|
||||||
|
<item quantity="many">Questo evento è stato importato con %1$d promemoria.</item>
|
||||||
|
<item quantity="other">Questo evento è stato importato con %1$d promemoria.</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="import_reminder_prompt_apply">Applica promemoria di default</string>
|
||||||
|
<string name="import_reminder_prompt_keep">Lascia invariato</string>
|
||||||
|
<string name="reminder_day_tomorrow">Domani</string>
|
||||||
|
<string name="reminder_day_yesterday">Ieri</string>
|
||||||
|
<string name="agenda_no_more_today">Nessun altro evento per oggi</string>
|
||||||
|
<string name="back">Indietro</string>
|
||||||
|
<string name="settings_soften_colors">Attenua i colori del calendario</string>
|
||||||
|
<string name="settings_soften_colors_summary">I colori dei calendari e degli eventi vengono attenuati per adattarsi al tema. Deseleziona per mostrare i colori naturali.</string>
|
||||||
|
<string name="settings_week_numbers">Numeri di settimana</string>
|
||||||
|
<string name="settings_week_numbers_summary">Mostra i numeri di settimana nella vista Mese</string>
|
||||||
|
<string name="settings_agenda_show_today">Mostra sempre Oggi</string>
|
||||||
|
<string name="settings_agenda_show_today_hint">Mantieni Oggi in alto nell\'Agenda e nel widget, anche se non ci sono altri eventi in giornata.</string>
|
||||||
|
<string name="calendars_export_title">Esporta calendari</string>
|
||||||
|
<string name="calendars_export_hint">Scegli quali calendari includere nel file .ics.</string>
|
||||||
|
<string name="calendars_export_action">Esporta</string>
|
||||||
|
<string name="calendars_restore_header">Ripristina</string>
|
||||||
|
<string name="calendars_restore_action">Ripristina da un file .ics</string>
|
||||||
|
<string name="calendars_restore_hint">Importa eventi da un backup o da un\'altra app.</string>
|
||||||
|
<string name="import_done_dedup_note">Gli eventi già presenti nel calendario sono stati saltati.</string>
|
||||||
|
<string name="import_done_added_label">Aggiunto</string>
|
||||||
|
<string name="import_done_skipped_label">Duplicati</string>
|
||||||
|
<string name="import_button">Importa</string>
|
||||||
|
<plurals name="import_title_count">
|
||||||
|
<item quantity="one">Importando %d evento</item>
|
||||||
|
<item quantity="many">Importando %d eventi</item>
|
||||||
|
<item quantity="other">Importando %d eventi</item>
|
||||||
|
</plurals>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -47,11 +47,11 @@
|
|||||||
<string name="event_share_failed">Nie udało się udostępnić tego wydarzenia.</string>
|
<string name="event_share_failed">Nie udało się udostępnić tego wydarzenia.</string>
|
||||||
<string name="event_delete_title">Usunąć wydarzenie?</string>
|
<string name="event_delete_title">Usunąć wydarzenie?</string>
|
||||||
<string name="event_delete_body">Usunięto wydarzenie z Twojego kalendarza i wszystkich zsynchronizowanych urządzeń.</string>
|
<string name="event_delete_body">Usunięto wydarzenie z Twojego kalendarza i wszystkich zsynchronizowanych urządzeń.</string>
|
||||||
<string name="event_delete_recurring_title">Usuń powtarzające się wydarzenie</string>
|
<string name="event_delete_recurring_title">Usuń wydarzenie cykliczne</string>
|
||||||
<string name="event_delete_option_occurrence">Tylko to wystąpienie</string>
|
<string name="event_delete_option_occurrence">Tylko to wystąpienie</string>
|
||||||
<string name="event_delete_option_following">To i wszystkie następne wystąpienia</string>
|
<string name="event_delete_option_following">To i wszystkie następne wystąpienia</string>
|
||||||
<string name="event_delete_option_series">Wszystkie wystąpienia w serii</string>
|
<string name="event_delete_option_series">Wszystkie wystąpienia w serii</string>
|
||||||
<string name="event_edit_recurring_title">Edytuj powtarzające się wydarzenie</string>
|
<string name="event_edit_recurring_title">Edytuj wydarzenie cykliczne</string>
|
||||||
<string name="event_delete_failed">Nie udało się usunąć tego wydarzenia</string>
|
<string name="event_delete_failed">Nie udało się usunąć tego wydarzenia</string>
|
||||||
<string name="event_delete_write_denied">Calendula wymaga uprawnień do zapisu, aby móc usuwać wydarzenia</string>
|
<string name="event_delete_write_denied">Calendula wymaga uprawnień do zapisu, aby móc usuwać wydarzenia</string>
|
||||||
<string name="dialog_cancel">Anuluj</string>
|
<string name="dialog_cancel">Anuluj</string>
|
||||||
@@ -136,7 +136,7 @@
|
|||||||
<string name="event_detail_description">Opis</string>
|
<string name="event_detail_description">Opis</string>
|
||||||
<string name="event_detail_attendees">Uczestnicy</string>
|
<string name="event_detail_attendees">Uczestnicy</string>
|
||||||
<string name="event_detail_recurrence">Powtarzanie</string>
|
<string name="event_detail_recurrence">Powtarzanie</string>
|
||||||
<string name="event_detail_recurring">Powtarzające się wydarzenie</string>
|
<string name="event_detail_recurring">Wydarzenie cykliczne</string>
|
||||||
<string name="recurrence_daily">Codziennie</string>
|
<string name="recurrence_daily">Codziennie</string>
|
||||||
<string name="recurrence_weekly">Co tydzień</string>
|
<string name="recurrence_weekly">Co tydzień</string>
|
||||||
<string name="recurrence_monthly">Co miesiąc</string>
|
<string name="recurrence_monthly">Co miesiąc</string>
|
||||||
@@ -297,4 +297,224 @@
|
|||||||
<string name="settings_agenda_range">Zakres agendy</string>
|
<string name="settings_agenda_range">Zakres agendy</string>
|
||||||
<string name="settings_agenda_range_hint">Jak daleko w przód wyświetlane są wydarzenia na ekranie Agendy.</string>
|
<string name="settings_agenda_range_hint">Jak daleko w przód wyświetlane są wydarzenia na ekranie Agendy.</string>
|
||||||
<string name="settings_agenda_widget_range">Zakres widżetu agendy</string>
|
<string name="settings_agenda_widget_range">Zakres widżetu agendy</string>
|
||||||
|
<string name="settings_agenda_widget_range_hint">Jak daleko w przód wyświetlane są wydarzenia w widżecie agendy na ekranie domowym.</string>
|
||||||
|
<string name="settings_agenda_range_bar">Pasek zakresu</string>
|
||||||
|
<string name="settings_agenda_range_bar_hint">Wyświetl u góry agendy pasek z zakresem widocznych dat oraz przyciskiem umożliwiającym zmianę zakresu na czas trwania sesji</string>
|
||||||
|
<string name="agenda_range_day">Dzisiaj</string>
|
||||||
|
<string name="agenda_range_this_week">Bieżący tydzień</string>
|
||||||
|
<string name="agenda_range_this_month">Bieżący miesiąc</string>
|
||||||
|
<string name="agenda_range_week">Następne 7 dni</string>
|
||||||
|
<string name="agenda_range_month">Następne 30 dni</string>
|
||||||
|
<string name="agenda_range_custom">Niestandardowy…</string>
|
||||||
|
<string name="agenda_range_custom_hint">Dni</string>
|
||||||
|
<string name="agenda_range_override_hint">Ustawienie tymczasowe — po ponownym otwarciu aplikacji Calendula przywrócony zostanie zapisany zakres.</string>
|
||||||
|
<string name="agenda_range_showing_label">Wyświetlanie wszystkich nadchodzących wydarzeń dla</string>
|
||||||
|
<plurals name="agenda_range_days">
|
||||||
|
<item quantity="one">%d dzień</item>
|
||||||
|
<item quantity="few">%d dni</item>
|
||||||
|
<item quantity="many">%d dni</item>
|
||||||
|
<item quantity="other">%d dni</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="settings_section_views">Widoki</string>
|
||||||
|
<string name="settings_quick_switch_header">Przycisk szybkiego przełączania</string>
|
||||||
|
<string name="settings_quick_switch_hint">Wybierz widoki, między którymi można przełączać się przyciskiem w prawym górnym rogu, i przeciągnij je, aby zmienić ich kolejność. Wyłączone widoki pozostaną dostępne w menu nawigacyjnym.</string>
|
||||||
|
<string name="settings_drawer_order_header">Menu nawigacyjne</string>
|
||||||
|
<string name="settings_drawer_order_hint">Przeciągnij aby zmienić kolejność widoków w menu nawigacyjnym.</string>
|
||||||
|
<string name="reorder_drag_handle">Przeciągnij by zmienić kolejność</string>
|
||||||
|
<string name="settings_section_event_form">Formularz nowego wydarzenia</string>
|
||||||
|
<string name="settings_form_fields_hint">Pola wyświetlane domyślnie — cała reszta znajduje się pod przyciskiem „Więcej pól”</string>
|
||||||
|
<string name="settings_autofocus_title">Ustaw kursor na tytule nowego wydarzenia</string>
|
||||||
|
<string name="settings_autofocus_title_hint">Kiedy tworzysz nowe wydarzenie, umieść kursor w polu tytułu i od razu otwórz klawiaturę.</string>
|
||||||
|
<string name="settings_color_unsupported">Zezwalaj na kolory w nieobsługiwanych kalendarzach</string>
|
||||||
|
<string name="settings_color_unsupported_hint">Niektóre kalendarze (np. niektóre CalDAV) nie udostępniają zestawu kolorów; niestandardowy kolor wydarzenia może zostać utracony lub nadpisany przy następnej synchronizacji. Jest to ograniczenie tych kalendarzy, a nie coś, co Calendula może naprawić.</string>
|
||||||
|
<string name="settings_section_notifications">Powiadomienia</string>
|
||||||
|
<string name="settings_reminders">Przypomnienia o wydarzeniach</string>
|
||||||
|
<string name="settings_reminders_hint">Widzisz podwójne przypomnienia? Inna aplikacja kalendarza również je wyświetla — wyłącz je w jednej z nich.</string>
|
||||||
|
<string name="settings_default_reminder">Domyślne przypomnienie</string>
|
||||||
|
<string name="settings_default_reminder_allday">Wydarzenia całodniowe</string>
|
||||||
|
<string name="settings_allday_reminder_time">Godzina przypomnienia o wydarzeniach całodniowych</string>
|
||||||
|
<string name="settings_allday_reminder_time_hint">Przypomnienia o wydarzeniach całodniowych zostaną pojawiają się o %1$s</string>
|
||||||
|
<string name="reminder_none">Brak</string>
|
||||||
|
<string name="reminder_use_default">Użyj domyślnego przypomnienia</string>
|
||||||
|
<string name="reminder_custom_amount">jednostek</string>
|
||||||
|
<string name="reminder_custom_set">Ustaw</string>
|
||||||
|
<string name="settings_calendar_reminders_title">Przypomnienia dla poszczególnych kalendarzy</string>
|
||||||
|
<string name="settings_calendar_reminders_hint">Zastąp ustawienia domyślne dla wybranych kalendarzy — oddzielnie dla wydarzeń terminowych i całodniowych. Kalendarz może zachować ustawienie domyślne, odrzucić je lub ustawić własne.</string>
|
||||||
|
<string name="settings_calendar_reminder_inherits">Domyślne (%1$s)</string>
|
||||||
|
<string name="settings_reliable_delivery">Niezawodne dostarczanie</string>
|
||||||
|
<string name="settings_reliable_delivery_hint">Android może opóźniać przypomnienia, aby oszczędzać baterię. Dodaj Calendula do wyjątków, żeby przychodziły na czas.</string>
|
||||||
|
<string name="settings_reliable_delivery_exempt">Wyłączono optymalizację baterii — przypomnienia przychodzą na czas.</string>
|
||||||
|
<string name="settings_snooze_duration">Długość drzemki</string>
|
||||||
|
<string name="settings_section_calendars">Kalendarze</string>
|
||||||
|
<string name="settings_manage_calendars">Zarządzaj kalendarzami</string>
|
||||||
|
<string name="settings_manage_calendars_hint">Twórz lokalne kalendarze; zarządzaj synchronizowanymi</string>
|
||||||
|
<string name="settings_section_language">Język</string>
|
||||||
|
<string name="settings_language">Język aplikacji</string>
|
||||||
|
<string name="settings_language_auto">Domyślny systemu</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_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_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>
|
||||||
|
<string name="settings_section_special_dates">Szczególne daty kontaktów</string>
|
||||||
|
<string name="settings_special_dates_enable">Pokaż daty kontaktów</string>
|
||||||
|
<string name="settings_special_dates_enable_hint">Zsynchronizuj urodziny i inne daty z kontaktów z lokalnymi kalendarzami. Odczytuje kontakty wyłącznie z tego urządzenia — nic nie jest przesyłane, a Twoje kontakty nigdy nie ulegają zmianie.</string>
|
||||||
|
<string name="settings_special_dates_type_birthday">Urodziny</string>
|
||||||
|
<string name="settings_special_dates_type_anniversary">Rocznice</string>
|
||||||
|
<string name="settings_special_dates_type_custom">Inne daty</string>
|
||||||
|
<string name="settings_special_dates_template">Format tytułu</string>
|
||||||
|
<string name="settings_special_dates_template_hint">Użyj {name} dla nazwy kontaktu i {year} dla roku (rok urodzenia lub rok rozpoczęcia rocznicy; ukryty, gdy jest nieznany).</string>
|
||||||
|
<string name="settings_special_dates_reminders">Przypomnienia</string>
|
||||||
|
<string name="settings_special_dates_show_year">Pokaż rok</string>
|
||||||
|
<string name="settings_special_dates_show_year_hint">Uwzględniaj {year} w tytułach, jeśli jest znany</string>
|
||||||
|
<string name="settings_special_dates_sync_now">Synchronizuj teraz</string>
|
||||||
|
<string name="settings_special_dates_never_synced">Jeszcze nie zsynchronizowano</string>
|
||||||
|
<string name="settings_special_dates_last_synced">Ostatnio zsynchronizowano %1$s</string>
|
||||||
|
<string name="settings_special_dates_calendar_hint">Ustaw kolor i widoczność każdego kalendarza w sekcji Kalendarze.</string>
|
||||||
|
<string name="settings_calendar_reminders_managed_hint">Ustaw w sekcji Szczególne daty kontaktów</string>
|
||||||
|
<string name="settings_special_dates_paused_title">Wstrzymano</string>
|
||||||
|
<string name="settings_special_dates_paused_hint">Aplikacja Calendula nie ma już dostępu do Twoich kontaktów, więc te kalendarze nie są aktualizowane.</string>
|
||||||
|
<string name="settings_special_dates_grant">Przyznaj dostęp</string>
|
||||||
|
<string name="settings_special_dates_disable_title">Wyłączyć daty kontaktów?</string>
|
||||||
|
<string name="settings_special_dates_disable_all_message">To spowoduje usunięcie kalendarzy ze szczególnymi datami kontaktów oraz ich wydarzeń. Wszelkie dodane do nich przypomnienia lub notatki zostaną utracone.</string>
|
||||||
|
<string name="settings_special_dates_disable_type_message">To spowoduje usunięcie kalendarza „%1$s” wraz z jego wydarzeniami. Wszelkie dodane do nich przypomnienia lub notatki zostaną utracone.</string>
|
||||||
|
<string name="settings_special_dates_disable_confirm">Wyłącz</string>
|
||||||
|
<string name="dialog_save">Zapisz</string>
|
||||||
|
<string name="settings_section_about">O aplikacji</string>
|
||||||
|
<string name="settings_license">Licencja</string>
|
||||||
|
<string name="settings_license_value">MIT</string>
|
||||||
|
<string name="settings_about_author">autorstwa Jean-Luc Makiola</string>
|
||||||
|
<string name="settings_about_source">Kod źródłowy</string>
|
||||||
|
<string name="settings_about_support">Wesprzyj rozwój</string>
|
||||||
|
<string name="settings_about_version">Wersja %1$s</string>
|
||||||
|
<string name="settings_about_logo_desc">Ikona aplikacji Calendula</string>
|
||||||
|
<string name="settings_report_problem">Zgłoś problem</string>
|
||||||
|
<string name="settings_report_problem_hint">Wyślij raport o awarii lub otwórz system zgłoszeń</string>
|
||||||
|
<string name="calendars_title">Kalendarze</string>
|
||||||
|
<string name="calendars_local_header">Twoje kalendarze</string>
|
||||||
|
<string name="calendars_local_empty">Brak lokalnych kalendarzy. Utwórz kalendarz, aby zapisywać wydarzenia tylko na tym urządzeniu.</string>
|
||||||
|
<string name="calendars_add">Dodaj kalendarz</string>
|
||||||
|
<string name="calendars_disable_hint">Wyłącz kalendarz, aby ukryć go w aplikacji — wraz z jego wydarzeniami, filtrami i selektorami. Nic nie zostanie usunięte, a w każdej chwili możesz go tutaj ponownie włączyć.</string>
|
||||||
|
<string name="calendars_show_in_app_a11y">Pokaż „%1$s” w aplikacji</string>
|
||||||
|
<string name="calendars_synced_header">Synchronizowane kalendarze</string>
|
||||||
|
<string name="calendars_synced_hint">Pochodzą z kont na Twoim urządzeniu. Możesz je tworzyć i edytować w powiązanych z nimi aplikacjach.</string>
|
||||||
|
<string name="calendars_manage_in_app">Zarządzaj w aplikacji</string>
|
||||||
|
<string name="calendars_account_menu_a11y">Więcej opcji dla %1$s</string>
|
||||||
|
<string name="calendars_enable_all">Włącz wszystkie</string>
|
||||||
|
<string name="calendars_disable_all">Wyłącz wszystkie</string>
|
||||||
|
<string name="calendars_add_account">Dodaj konto</string>
|
||||||
|
<string name="calendars_new_title">Nowy kalendarz</string>
|
||||||
|
<string name="calendars_edit_title">Edytuj kalendarz</string>
|
||||||
|
<string name="calendars_name_label">Nazwa</string>
|
||||||
|
<string name="calendars_color_label">Kolor</string>
|
||||||
|
<string name="calendars_description_hint">Dodaj opis</string>
|
||||||
|
<string name="calendars_delete_confirm_title">Usunąć kalendarz?</string>
|
||||||
|
<string name="calendars_delete_confirm_message">„%1$s” oraz wszystkie powiązane z nim wydarzenia zostaną trwale usunięte z tego urządzenia.</string>
|
||||||
|
<string name="calendars_write_error">Nie udało się zapisać zmiany.</string>
|
||||||
|
<string name="calendars_backup_header">Kopia zapasowa</string>
|
||||||
|
<string name="calendars_backup_hint">Kalendarze lokalne nie są nigdzie synchronizowane, więc wyeksportuj je do pliku .ics, aby zachować kopię.</string>
|
||||||
|
<string name="calendars_backup_action">Eksportuj jako plik .ics</string>
|
||||||
|
<string name="calendars_export_title">Eksportuj kalendarze</string>
|
||||||
|
<string name="calendars_export_hint">Wybierz kalendarze, które mają zostać uwzględnione w pliku .ics.</string>
|
||||||
|
<string name="calendars_export_action">Eksportuj</string>
|
||||||
|
<string name="calendars_restore_header">Przywróć</string>
|
||||||
|
<string name="calendars_restore_action">Przywróć z pliku .ics</string>
|
||||||
|
<string name="calendars_restore_hint">Importuj wydarzenia z kopii zapasowej lub innej aplikacji kalendarza.</string>
|
||||||
|
<string name="calendars_auto_backup">Automatyczna kopia zapasowa</string>
|
||||||
|
<string name="calendars_auto_backup_hint">Okresowo eksportuj swoje lokalne kalendarze do folderu jako plik .ics.</string>
|
||||||
|
<string name="calendars_auto_backup_folder">Folder kopii zapasowej</string>
|
||||||
|
<string name="calendars_auto_backup_folder_unset">Dotknij by wybrać folder</string>
|
||||||
|
<string name="calendars_auto_backup_interval">Okres</string>
|
||||||
|
<string name="calendars_auto_backup_every">Co %1$s</string>
|
||||||
|
<string name="calendars_auto_backup_interval_min">Minimum 30 minut.</string>
|
||||||
|
<string name="calendars_auto_backup_status_never">Nie ma jeszcze automatycznych kopii zapasowych</string>
|
||||||
|
<string name="calendars_auto_backup_status_ok">Ostatnia kopia zapasowa: %1$s</string>
|
||||||
|
<string name="calendars_auto_backup_status_failed">Ostatnia nieudana kopia zapasowa: %1$s</string>
|
||||||
|
<string name="backup_channel_name">Kopia zapasowa</string>
|
||||||
|
<string name="backup_channel_description">Ostrzega, jeśli automatyczne kopie zapasowe wielokrotnie się nie powiodą.</string>
|
||||||
|
<string name="backup_failed_title">Automatyczna kopia zapasowa nie powiodła się</string>
|
||||||
|
<string name="backup_failed_text">Calendula nie mogła zapisać pliku kopii zapasowej. Sprawdź folder kopii zapasowej w Ustawieniach.</string>
|
||||||
|
<string name="calendars_backup_failed">Nie udało się wyeksportować kopii zapasowej.</string>
|
||||||
|
<plurals name="calendars_backup_done">
|
||||||
|
<item quantity="one">Wyeksportowano %d wydarzenie.</item>
|
||||||
|
<item quantity="few">Wyeksportowano %d wydarzenia.</item>
|
||||||
|
<item quantity="many">Wyeksportowano %d wydarzeń.</item>
|
||||||
|
<item quantity="other">Wyeksportowano %d wydarzeń.</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="import_title">Importuj wydarzenia</string>
|
||||||
|
<string name="import_target_header">Dodaj do kalendarza</string>
|
||||||
|
<string name="import_empty">Nie znaleziono wydarzeń w tym w pliku.</string>
|
||||||
|
<string name="import_failed">Nie udało się odczytać tego pliku.</string>
|
||||||
|
<string name="import_no_calendar">Brak kalendarza z możliwością zapisu, do którego można zaimportować dane. Najpierw utwórz kalendarz lokalny.</string>
|
||||||
|
<string name="import_done_title">Import zakończony</string>
|
||||||
|
<string name="import_done_dedup_note">Wydarzenia znajdujące się już w kalendarzu zostały pominięte.</string>
|
||||||
|
<string name="import_done_added_label">Dodano</string>
|
||||||
|
<string name="import_done_skipped_label">Duplikaty</string>
|
||||||
|
<string name="import_close">Zamknij</string>
|
||||||
|
<string name="import_warning_recurrence">Niektóre zmienione wystąpienia zdarzeń cyklicznych zostały pominięte.</string>
|
||||||
|
<string name="import_warning_no_start">Wydarzenie bez określonej godziny rozpoczęcia zostało pominięte.</string>
|
||||||
|
<string name="import_warning_attendees">Listy gości nie zostały zaimportowane.</string>
|
||||||
|
<string name="import_warning_timezone">Nieznana strefa czasowa została zastąpiona strefą ustawioną na Twoim urządzeniu.</string>
|
||||||
|
<string name="import_button">Importuj</string>
|
||||||
|
<plurals name="import_title_count">
|
||||||
|
<item quantity="one">Importowanie %d wydarzenia</item>
|
||||||
|
<item quantity="few">Importowanie %d wydarzeń</item>
|
||||||
|
<item quantity="many">Importowanie %d wydarzeń</item>
|
||||||
|
<item quantity="other">Importowanie %d wydarzeń</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="import_event_count">
|
||||||
|
<item quantity="one">%d wydarzenie w tym pliku.</item>
|
||||||
|
<item quantity="few">%d wydarzenia w tym pliku.</item>
|
||||||
|
<item quantity="many">%d wydarzeń w tym pliku.</item>
|
||||||
|
<item quantity="other">%d wydarzeń w tym pliku.</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="import_action">
|
||||||
|
<item quantity="one">Importuj %d wydarzenie</item>
|
||||||
|
<item quantity="few">Importuj %d wydarzenia</item>
|
||||||
|
<item quantity="many">Importuj %d wydarzeń</item>
|
||||||
|
<item quantity="other">Importuj %d wydarzeń</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="import_done_imported">
|
||||||
|
<item quantity="one">Zaimportowano %d wydarzenie.</item>
|
||||||
|
<item quantity="few">Zaimportowano %d wydarzenia.</item>
|
||||||
|
<item quantity="many">Zaimportowano %d wydarzeń.</item>
|
||||||
|
<item quantity="other">Zaimportowano %d wydarzeń.</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="import_done_skipped">
|
||||||
|
<item quantity="one">Pominięto %d wydarzenie znajdujące się już w tym kalendarzu.</item>
|
||||||
|
<item quantity="few">Pominięto %d wydarzenia znajdujące się już w tym kalendarzu.</item>
|
||||||
|
<item quantity="many">Pominięto %d wydarzeń znajdujących się już w tym kalendarzu.</item>
|
||||||
|
<item quantity="other">Pominięto %d wydarzeń znajdujących się już w tym kalendarzu.</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="shortcut_new_event_short">Nowe wydarzenie</string>
|
||||||
|
<string name="shortcut_new_event_long">Utwórz nowe wydarzenie</string>
|
||||||
|
<string name="qs_tile_new_event_label">Nowe wydarzenie</string>
|
||||||
|
<string name="settings_qs_tile">Dodaj kafelek Szybkich ustawień</string>
|
||||||
|
<string name="settings_qs_tile_hint">Dodaj kafelek „Nowe wydarzenie” do panelu Szybkich ustawień.</string>
|
||||||
|
<string name="crash_dialog_title">Aplikacja %1$s uległa awarii</string>
|
||||||
|
<string name="crash_dialog_message">Aplikacja %1$s została nieoczekiwanie zamknięta przy ostatnim uruchomieniu. Możesz pomóc nam to naprawić, wysyłając zgłoszenie o błędzie. Pozostanie ono na Twoim urządzeniu, dopóki nie zdecydujesz się go udostępnić, i nie zawiera żadnych danych osobowych ani treści z kalendarza — jedynie poniższe szczegóły techniczne.</string>
|
||||||
|
<string name="crash_dialog_report">Zgłoś</string>
|
||||||
|
<string name="crash_dialog_dismiss">Nie teraz</string>
|
||||||
|
<string name="crash_report_issue_title">Raport o awarii</string>
|
||||||
|
<string name="crash_report_clip_label">Raport o awarii %1$s</string>
|
||||||
|
<string name="crash_report_copied">Raport skopiowany do schowka</string>
|
||||||
|
<string name="crash_report_open_failed">Nie udało się otworzyć systemu zgłoszeń. Raport znajduje się w Twoim schowku.</string>
|
||||||
|
<string name="crash_report_body_template">Dziękujemy za zgłoszenie awarii w aplikacji %1$s. Prosimy o dodanie wszelkich zapamiętanych szczegółów na temat wykonywanych czynności, a następnie przesłanie zgłoszenia.\n\n### Co się wydarzyło\n\n\n### Raport o awarii\n%2$s\n</string>
|
||||||
|
<string name="crash_report_body_paste">_(Raport był zbyt długi dla tego linku — wklej go tutaj ze schowka)._</string>
|
||||||
|
<string name="special_dates_calendar_custom">Szczególne daty</string>
|
||||||
|
<string name="special_dates_default_title_birthday">Urodziny {name} ({year})</string>
|
||||||
|
<string name="special_dates_default_title_anniversary">Rocznica {name} ({year})</string>
|
||||||
|
<string name="special_dates_default_title_custom">{name}</string>
|
||||||
|
<string name="event_detail_duplicate">Duplikuj</string>
|
||||||
|
<string name="reminder_day_tomorrow">Jutro</string>
|
||||||
|
<string name="reminder_day_yesterday">Wczoraj</string>
|
||||||
|
<string name="agenda_no_more_today">Brak zaplanowanych wydarzeń</string>
|
||||||
|
<string name="settings_soften_colors">Zredukuj intensywność kolorów kalendarza</string>
|
||||||
|
<string name="settings_soften_colors_summary">Dopasuj intensywność kolorów kalendarza i wydarzeń do motywu. Wyłącz tę opcję, aby wyświetlać oryginalne kolory ze źródła kalendarza.</string>
|
||||||
|
<string name="settings_agenda_show_today">Zawsze pokazuj dzisiejszy dzień</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_anniversary">Rocznice</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -6,4 +6,60 @@
|
|||||||
<item>Days</item>
|
<item>Days</item>
|
||||||
<item>Weeks</item>
|
<item>Weeks</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
<string name="app_name">Calendula</string>
|
||||||
|
<string name="app_tagline">Современный календарь.</string>
|
||||||
|
<string name="state_loading">Загрузка…</string>
|
||||||
|
<string name="state_retry">Попробовать ещё раз</string>
|
||||||
|
<string name="state_failure_unknown">Что-то пошло не так.</string>
|
||||||
|
<string name="state_failure_permission">Требуется доступ к календарю.</string>
|
||||||
|
<string name="state_failure_permission_action">Предоставить доступ</string>
|
||||||
|
<string name="state_failure_no_calendars">Нет настроенных календарей.</string>
|
||||||
|
<string name="state_failure_no_calendars_action">Перейти в системные настройки календаря</string>
|
||||||
|
<string name="state_failure_provider">Не удалось просмотреть календарь.</string>
|
||||||
|
<string name="permission_rationale_title">Следите за всеми своими событиями. С красотой</string>
|
||||||
|
<string name="permission_rationale_body">Calendula требуется доступ к вашему календарю, чтобы показывать и управлять вашими событиями. Это всё, что приложение требует с самого начала – и никакая информация не покидает ваш девайс.</string>
|
||||||
|
<string name="permission_request_button">Дать доступ к календарю</string>
|
||||||
|
<string name="permission_denied_title">В доступе к календарю отказано</string>
|
||||||
|
<string name="permission_denied_body">Calendula не может показать события без доступа к календарю. Предоставьте его снова в настройках телефона.</string>
|
||||||
|
<string name="permission_open_settings_button">Открыть настройки телефона</string>
|
||||||
|
<string name="permission_retry_button">Попытайтесь снова</string>
|
||||||
|
<string name="permission_benefit_private_title">Остаётся на вашем устройстве</string>
|
||||||
|
<string name="permission_benefit_private_body">Ваши календари просматриваются локально и информация никогда не покидает устройство.</string>
|
||||||
|
<string name="permission_benefit_sync_title">Все ваши календари. Вместе</string>
|
||||||
|
<string name="permission_benefit_sync_body">Google, CalDAV, локальный календарь – всё синхронизированное с устройством просто подключается.</string>
|
||||||
|
<string name="permission_benefit_privacy_title">Никакого отслеживания. Никогда</string>
|
||||||
|
<string name="permission_benefit_privacy_body">Ноль телеметрии. ноль аналитики, без рекламы.</string>
|
||||||
|
<string name="permission_privacy_footnote">Остаётся на устройстве · не требует доступа к интернету</string>
|
||||||
|
<string name="month_prev">Предыдущий месяц</string>
|
||||||
|
<string name="month_next">Следующий месяц</string>
|
||||||
|
<string name="month_today_action">Сегодня</string>
|
||||||
|
<string name="month_more_actions">Больше действий</string>
|
||||||
|
<string name="month_open_menu">Открыть меню</string>
|
||||||
|
<string name="month_action_settings">Настройки</string>
|
||||||
|
<string name="month_a11y_today_prefix">Сегодня</string>
|
||||||
|
<string name="week_today_action">Эта неделя</string>
|
||||||
|
<string name="week_number_label">Нед</string>
|
||||||
|
<string name="day_today_action">Сегодня</string>
|
||||||
|
<string name="event_detail_back">Назад</string>
|
||||||
|
<string name="event_detail_edit">Редактировать</string>
|
||||||
|
<string name="event_detail_delete">Удалить</string>
|
||||||
|
<string name="event_detail_share">Поделиться</string>
|
||||||
|
<string name="event_detail_duplicate">Дублировать</string>
|
||||||
|
<string name="event_share_chooser_title">Поделиться событием</string>
|
||||||
|
<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_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_delete_failed">Не удалось удалить событие</string>
|
||||||
|
<string name="event_delete_write_denied">Calendula требуется доступ для удаления событий</string>
|
||||||
|
<string name="dialog_cancel">Отмена</string>
|
||||||
|
<string name="dialog_ok">ОК</string>
|
||||||
|
<string name="event_edit_new_title">Новое событие</string>
|
||||||
|
<string name="event_edit_close">Закрыть</string>
|
||||||
|
<string name="event_edit_save">Сохранить</string>
|
||||||
|
<string name="event_edit_title_hint">Добавить имя</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -375,16 +375,17 @@
|
|||||||
<!-- Month view style (#38, #53) -->
|
<!-- Month view style (#38, #53) -->
|
||||||
<string name="settings_month_header">Month view</string>
|
<string name="settings_month_header">Month view</string>
|
||||||
<string name="settings_month_view_style">Month view style</string>
|
<string name="settings_month_view_style">Month view style</string>
|
||||||
<string name="settings_month_view_style_summary">Choose how the month view is laid out and how you move through it.</string>
|
|
||||||
<string name="month_style_paged">Pages</string>
|
<string name="month_style_paged">Pages</string>
|
||||||
<string name="month_style_paged_summary">One month at a time. Swipe sideways to change month.</string>
|
<string name="month_style_paged_summary">One month fills the screen. Swipe left or right to change month.</string>
|
||||||
<string name="month_style_continuous">Continuous</string>
|
<string name="month_style_continuous">Scrolling months</string>
|
||||||
<string name="month_style_continuous_summary">Scroll up and down through the months. Each month keeps to itself under its own heading, with no days borrowed from its neighbours.</string>
|
<string name="month_style_continuous_summary">Each month sits under its own heading, with a little space setting it apart from the next.</string>
|
||||||
<string name="month_style_dense">Dense</string>
|
<string name="month_style_dense">Seamless weeks</string>
|
||||||
<string name="month_style_dense_summary">The same endless scroll with the seams taken out — one unbroken run of weeks, months flowing into each other.</string>
|
<string name="month_style_dense_summary">The weeks run on without a break, each month flowing straight into the next with no gap between them.</string>
|
||||||
<string name="month_style_split">Split</string>
|
<string name="month_style_split">Split</string>
|
||||||
<string name="month_style_split_summary">A compact grid with dots for events, and the day you tap listed underneath.</string>
|
<string name="month_style_split_summary">A compact grid marks the days that have events, and the day you tap is listed underneath.</string>
|
||||||
<string name="month_split_no_events">Nothing scheduled</string>
|
<string name="month_split_no_events">Nothing scheduled</string>
|
||||||
|
<string name="month_split_expand">Show the whole month</string>
|
||||||
|
<string name="month_split_collapse">Show the day\'s events</string>
|
||||||
<string name="settings_quick_switch_header">Quick-switch button</string>
|
<string name="settings_quick_switch_header">Quick-switch button</string>
|
||||||
<string name="settings_quick_switch_hint">Choose which views the top-right button cycles through, and drag to reorder them. Turned-off views stay reachable from the navigation menu.</string>
|
<string name="settings_quick_switch_hint">Choose which views the top-right button cycles through, and drag to reorder them. Turned-off views stay reachable from the navigation menu.</string>
|
||||||
<string name="settings_drawer_order_header">Navigation menu</string>
|
<string name="settings_drawer_order_header">Navigation menu</string>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
-->
|
-->
|
||||||
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
|
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<locale android:name="en" />
|
<locale android:name="en" />
|
||||||
|
<locale android:name="ar" />
|
||||||
<locale android:name="de" />
|
<locale android:name="de" />
|
||||||
<locale android:name="es" />
|
<locale android:name="es" />
|
||||||
<locale android:name="fr" />
|
<locale android:name="fr" />
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
package de.jeanlucmakiola.calendula.ui.month
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||||
|
import kotlinx.datetime.DateTimeUnit
|
||||||
|
import kotlinx.datetime.DayOfWeek
|
||||||
|
import kotlinx.datetime.LocalDate
|
||||||
|
import kotlinx.datetime.Month
|
||||||
|
import kotlinx.datetime.TimeZone
|
||||||
|
import kotlinx.datetime.YearMonth
|
||||||
|
import kotlinx.datetime.atTime
|
||||||
|
import kotlinx.datetime.plus
|
||||||
|
import kotlinx.datetime.toInstant
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What lets the split style's dots morph into the paged style's bars (#53): both
|
||||||
|
* read a day off the *same* lane seating, so dot _i_ and lane _i_ are one event.
|
||||||
|
*/
|
||||||
|
class LaneEventsTest {
|
||||||
|
|
||||||
|
private val zone = TimeZone.UTC
|
||||||
|
private val jul26 = YearMonth(2026, Month.JULY)
|
||||||
|
|
||||||
|
/** July 2026 starts on a Wednesday, so this row — Jul 6–12 — sits wholly inside it. */
|
||||||
|
private fun rowOfJuly6(events: List<EventInstance>) =
|
||||||
|
layoutMonthWeeks(jul26, DayOfWeek.MONDAY, events, zone)[1]
|
||||||
|
|
||||||
|
private fun allDay(from: LocalDate, toInclusive: LocalDate, id: Long, color: Int = BLUE) =
|
||||||
|
EventInstance(
|
||||||
|
instanceId = id,
|
||||||
|
eventId = id,
|
||||||
|
calendarId = 1L,
|
||||||
|
title = "A$id",
|
||||||
|
start = from.atTime(0, 0).toInstant(zone),
|
||||||
|
end = toInclusive.plus(1, DateTimeUnit.DAY).atTime(0, 0).toInstant(zone),
|
||||||
|
isAllDay = true,
|
||||||
|
color = color,
|
||||||
|
location = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun timed(date: LocalDate, hour: Int, id: Long, color: Int = RED) = EventInstance(
|
||||||
|
instanceId = id,
|
||||||
|
eventId = id,
|
||||||
|
calendarId = 1L,
|
||||||
|
title = "T$id",
|
||||||
|
start = date.atTime(hour, 0).toInstant(zone),
|
||||||
|
end = date.atTime(hour + 1, 0).toInstant(zone),
|
||||||
|
isAllDay = false,
|
||||||
|
color = color,
|
||||||
|
location = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `a bar keeps its lane and the day's timed events fill what's left`() {
|
||||||
|
val bar = allDay(LocalDate(2026, 7, 7), LocalDate(2026, 7, 9), id = 1L)
|
||||||
|
val meeting = timed(LocalDate(2026, 7, 7), hour = 9, id = 2L)
|
||||||
|
val week = rowOfJuly6(listOf(bar, meeting))
|
||||||
|
|
||||||
|
// Jul 7 is column 1 of a Monday-anchored row starting Jul 6.
|
||||||
|
assertThat(week.laneEvents(col = 1, day = LocalDate(2026, 7, 7), laneCap = 3))
|
||||||
|
.containsExactly(bar, meeting)
|
||||||
|
.inOrder()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `a day the bar misses seats its own events from lane zero`() {
|
||||||
|
val bar = allDay(LocalDate(2026, 7, 7), LocalDate(2026, 7, 9), id = 1L)
|
||||||
|
val monday = timed(LocalDate(2026, 7, 6), hour = 9, id = 2L)
|
||||||
|
val week = rowOfJuly6(listOf(bar, monday))
|
||||||
|
|
||||||
|
assertThat(week.laneEvents(col = 0, day = LocalDate(2026, 7, 6), laneCap = 3))
|
||||||
|
.containsExactly(monday)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `a multi-day bar is seated on every day it covers`() {
|
||||||
|
val bar = allDay(LocalDate(2026, 7, 7), LocalDate(2026, 7, 9), id = 1L)
|
||||||
|
val week = rowOfJuly6(listOf(bar))
|
||||||
|
|
||||||
|
(1..3).forEach { col ->
|
||||||
|
val day = LocalDate(2026, 7, 6 + col)
|
||||||
|
assertThat(week.laneEvents(col, day, laneCap = 3)).containsExactly(bar)
|
||||||
|
}
|
||||||
|
assertThat(week.laneEvents(col = 4, day = LocalDate(2026, 7, 10), laneCap = 3)).isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The bug the colour-gathered dots had: one dot for two events on one calendar. */
|
||||||
|
@Test
|
||||||
|
fun `events sharing a colour each keep their own lane`() {
|
||||||
|
val first = timed(LocalDate(2026, 7, 6), hour = 9, id = 1L, color = RED)
|
||||||
|
val second = timed(LocalDate(2026, 7, 6), hour = 14, id = 2L, color = RED)
|
||||||
|
val week = rowOfJuly6(listOf(first, second))
|
||||||
|
|
||||||
|
assertThat(week.laneEvents(col = 0, day = LocalDate(2026, 7, 6), laneCap = 3))
|
||||||
|
.containsExactly(first, second)
|
||||||
|
.inOrder()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `seating stops at the cap and leaves the rest to the overflow count`() {
|
||||||
|
val events = (0 until 5).map { timed(LocalDate(2026, 7, 6), hour = 8 + it, id = it + 1L) }
|
||||||
|
val week = rowOfJuly6(events)
|
||||||
|
|
||||||
|
val seated = week.laneEvents(col = 0, day = LocalDate(2026, 7, 6), laneCap = 3)
|
||||||
|
assertThat(seated).hasSize(3)
|
||||||
|
assertThat(seated).containsExactlyElementsIn(events.take(3)).inOrder()
|
||||||
|
assertThat(week.countByDay[LocalDate(2026, 7, 6)]!! - seated.size).isEqualTo(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A bar parked below the cap is out of view, so it takes no dot with it. */
|
||||||
|
@Test
|
||||||
|
fun `a bar beyond the cap is left out`() {
|
||||||
|
val bars = (0 until 4).map {
|
||||||
|
allDay(LocalDate(2026, 7, 6), LocalDate(2026, 7, 8), id = it + 1L)
|
||||||
|
}
|
||||||
|
val week = rowOfJuly6(bars)
|
||||||
|
|
||||||
|
val seated = week.laneEvents(col = 0, day = LocalDate(2026, 7, 6), laneCap = 3)
|
||||||
|
assertThat(seated).hasSize(3)
|
||||||
|
assertThat(week.spans.filter { it.lane >= 3 }.map { it.event })
|
||||||
|
.containsNoneIn(seated)
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val BLUE = 0xFF3366CC.toInt()
|
||||||
|
const val RED = 0xFFCC3333.toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -82,9 +82,24 @@ release work when a merge actually cuts a release:
|
|||||||
merged commit, build & sign the release APK with the **app key**, copy it into
|
merged commit, build & sign the release APK with the **app key**, copy it into
|
||||||
the F-Droid repo, generate the per-version changelog, re-sign the index with
|
the F-Droid repo, generate the per-version changelog, re-sign the index with
|
||||||
the **repo key**, upload `repo/` + `metadata/`, then create the `vX.Y.Z` tag +
|
the **repo key**, upload `repo/` + `metadata/`, then create the `vX.Y.Z` tag +
|
||||||
Gitea release (CHANGELOG section as notes) and attach the R8 `mapping.txt`
|
Gitea release (CHANGELOG section as notes), attach the R8 `mapping.txt`, and
|
||||||
(best-effort). Ordinary merges with no version bump fall through `detect` and
|
mirror the release to **Codeberg** with the signed APK + a SHA-256 checksum
|
||||||
do nothing.
|
(both best-effort). Ordinary merges with no version bump fall through `detect`
|
||||||
|
and do nothing.
|
||||||
|
|
||||||
|
### Codeberg direct-download channel
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
unset. One-time setup: the repo's **Releases** unit must be enabled and a
|
||||||
|
`CODEBERG_RELEASE_TOKEN` secret (Codeberg access token, `write:repository` scope) added
|
||||||
|
to Gitea Actions.
|
||||||
|
|
||||||
### Manual re-sign / recovery
|
### Manual re-sign / recovery
|
||||||
|
|
||||||
@@ -103,6 +118,7 @@ for key rotation or repo recovery without publishing a new app version.
|
|||||||
| `FDROID_CONFIG_BASE64` | F-Droid `config.yml` (base64) — repo metadata + keystore passwords. |
|
| `FDROID_CONFIG_BASE64` | F-Droid `config.yml` (base64) — repo metadata + keystore passwords. |
|
||||||
| `HETZNER_HOST`, `HETZNER_USER`, `HETZNER_PASS` | Upload target for the F-Droid repo. |
|
| `HETZNER_HOST`, `HETZNER_USER`, `HETZNER_PASS` | Upload target for the F-Droid repo. |
|
||||||
| `GITHUB_TOKEN` | Provided by Gitea Actions; used to create the release + attach assets. |
|
| `GITHUB_TOKEN` | Provided by Gitea Actions; used to create the release + attach assets. |
|
||||||
|
| `CODEBERG_RELEASE_TOKEN` | Codeberg access token (`write:repository` scope) — creates the mirrored Codeberg release + uploads the APK/checksum. Best-effort; if unset the Codeberg step skips. |
|
||||||
|
|
||||||
The two keys are independent: the **app key** signs APKs; the **repo key**
|
The two keys are independent: the **app key** signs APKs; the **repo key**
|
||||||
signs the index (its fingerprint is what users pin). Neither key nor the
|
signs the index (its fingerprint is what users pin). Neither key nor the
|
||||||
|
|||||||
@@ -1,21 +1,46 @@
|
|||||||
### Added
|
### Added
|
||||||
- Give an event its own time zone. A new Time zone field (under "more fields" in
|
- Choose how the month view is laid out. A new **Month view style** setting
|
||||||
the event form) pins an event to a specific zone, so a call set for 8:00 AM in
|
(Settings → Views) offers three ways to read a month, each shown with a
|
||||||
New York stays 8:00 AM in New York wherever you open it, and keeps tracking
|
preview of the layout it produces:
|
||||||
that zone across daylight-saving changes. The form shows the local equivalent
|
- **Pages** — what you have today: one month at a time, swiped sideways.
|
||||||
under the times, and the event's details keep your local time first with the
|
- **Continuous** — scroll up and down through the weeks without a break
|
||||||
original noted beneath. Pick a zone from a searchable full-screen picker — by
|
between months. Because the weeks run on unbroken, no month is cut off and
|
||||||
city, IANA id, or abbreviation like "CEST". All-day events stay date-anchored
|
no day appears twice, where paging repeats a boundary week at the end of one
|
||||||
and carry no zone ([#31]).
|
month and the start of the next. The 1st of each month names itself so you
|
||||||
- Put the "jump to today" button in the toolbar. A new Today button in toolbar
|
always know where you are, and the title bar keeps up as you scroll ([#38]).
|
||||||
setting (Settings → Appearance, off by default) swaps the floating corner
|
- **Split** — a compact grid showing coloured dots for the days that have
|
||||||
button for a permanent today icon in the top bar — always there, matching the
|
something on them, with the day you tap listed in full underneath. Tap the
|
||||||
familiar calendar-app pattern. Leave it off to keep the floating button ([#60]).
|
date above the list to open the whole day ([#53]).
|
||||||
- Show the app as "Calendar" in your launcher. A new App name setting
|
|
||||||
(Settings → Appearance) switches the launcher label from "Calendula" to the
|
The Agenda view is untouched by this and stays available in all three styles —
|
||||||
generic "Calendar" for anyone who prefers it — handy on launchers that can't
|
the split layout lists a single day, while Agenda remains a rolling multi-day
|
||||||
rename apps themselves. Only the launcher name changes; your home-screen icon
|
window with its own range settings.
|
||||||
may move to a new spot after switching ([#44]).
|
- Give an event its own time zone. A new **Time zone** field (under "more
|
||||||
|
fields" in the event form) pins an event to a specific zone, so a call set for
|
||||||
|
8:00 AM in New York stays 8:00 AM in New York wherever you open it — and keeps
|
||||||
|
tracking that zone across daylight-saving changes instead of drifting an hour.
|
||||||
|
The form edits the event in its own zone and shows the local equivalent under
|
||||||
|
the times ("2:00 PM – 3:00 PM your time"); the event's details keep your local
|
||||||
|
time first and note the original beneath it, so both are always clear. Pick a
|
||||||
|
zone from a full-screen picker with your device zone and recent choices on top,
|
||||||
|
searching by city ("new york"), IANA id ("europe/berlin"), or abbreviation
|
||||||
|
("CEST") to gather every matching zone at once. All-day events stay
|
||||||
|
date-anchored and carry no zone, as before ([#31]).
|
||||||
|
- Put the "jump to today" button in the toolbar. A new **Today button in
|
||||||
|
toolbar** setting (Settings → Appearance, off by default) swaps the floating
|
||||||
|
button that fades into the corner while you're away from today for a permanent
|
||||||
|
today icon in the top bar — always there, on today or not, matching the
|
||||||
|
familiar calendar-app pattern. Leave it off to keep the floating button as
|
||||||
|
before ([#60]).
|
||||||
|
- Choose what Calendula calls itself on your home screen. A new **App name**
|
||||||
|
setting (Settings → Appearance) switches the launcher label between
|
||||||
|
**Calendula** and **Calendar**, for launchers that can't rename apps
|
||||||
|
themselves. Pick from a full-screen chooser that previews both names as
|
||||||
|
launcher marks; the change applies at once ([#44]).
|
||||||
|
- Calendula now speaks **Arabic**, laid out right-to-left, and its French and
|
||||||
|
Italian translations have been brought up to date — thanks to the community
|
||||||
|
translators on Weblate. Pick a language under Settings → Language (or leave it
|
||||||
|
on the system default).
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Dates in the Month, Week and Day title bars now follow your language and
|
- Dates in the Month, Week and Day title bars now follow your language and
|
||||||
@@ -34,4 +59,43 @@
|
|||||||
"24. Jun – 31. Jun" restated the day numbers already printed in the column
|
"24. Jun – 31. Jun" restated the day numbers already printed in the column
|
||||||
headers right below it, in the widest string in the bar. A week that straddles
|
headers right below it, in the widest string in the bar. A week that straddles
|
||||||
two months keeps the outgoing month until it is fully gone ([#60]).
|
two months keeps the outgoing month until it is fully gone ([#60]).
|
||||||
|
- The custom recurrence picker has been redesigned and tightened up. As you
|
||||||
|
build a rule — "every 2 weeks on Mon & Wed, until a date" — the live summary
|
||||||
|
now describes exactly what will be saved rather than a near-copy that could
|
||||||
|
drift from it, the amount fields accept being left blank (reading as their
|
||||||
|
shown default instead of greying out OK), and the read-out no longer jumps
|
||||||
|
around as you tap weekdays ([#42]).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- An all-day event no longer shows up again the day after it happened. In time
|
||||||
|
zones east of UTC, an all-day event — a birthday, say — set for one day also
|
||||||
|
appeared under the *next* day's heading in the Agenda (and the agenda widget),
|
||||||
|
because all-day events are anchored to UTC midnight and the following day's
|
||||||
|
window reached back across that boundary and pulled the event forward onto
|
||||||
|
"today". Each all-day event now lists only on the day it actually falls on
|
||||||
|
([#65]).
|
||||||
|
- A multi-day event now shows under every day it spans in the Agenda, not just
|
||||||
|
its first day, so a trip or a multi-day booking appears on each day it covers.
|
||||||
|
- The "Upcoming" agenda widget now scales its text and rows to the size you give
|
||||||
|
it. Previously it was laid out once for the smallest size and simply stretched
|
||||||
|
when enlarged, so the text stayed small no matter how big you made the widget.
|
||||||
|
Now a bigger widget gets bigger, more readable type and roomier rows, while the
|
||||||
|
default size looks exactly as before — no new setting; it follows the size you
|
||||||
|
already chose ([#51]).
|
||||||
|
- Calendula now appears under other apps' "Add to calendar" / "Save to calendar"
|
||||||
|
actions. Some apps (e.g. DB Navigator) fire the widely-used "insert event"
|
||||||
|
intent with the singular `vnd.android.cursor.item/event` type, which Calendula
|
||||||
|
didn't advertise — so it was left out of the chooser, and if it was your only
|
||||||
|
calendar app the save silently did nothing. It now accepts that form, plus the
|
||||||
|
`INSERT_OR_EDIT` action, and opens the new event prefilled for review ([#74]).
|
||||||
|
- Opening a `.ics`/`.vcs` file now works even when another app hands it over
|
||||||
|
mislabelled as a generic download (`application/octet-stream`), as some mail
|
||||||
|
clients, browsers and file managers do — Calendula recognises it by its file
|
||||||
|
extension instead of relying on the declared type ([#74]).
|
||||||
|
- A recurrence end date no longer lands a day late. West of UTC, setting a rule
|
||||||
|
to end "until" a given day could save and show the day after the one picked;
|
||||||
|
the end date now reads back as chosen ([#42]).
|
||||||
|
- The status- and navigation-bar icons stay legible over full-screen pickers in
|
||||||
|
dark theme. They could render dark-on-dark — a near-invisible black clock
|
||||||
|
against the dark picker — instead of switching to light ([#70]).
|
||||||
|
|
||||||
|
|||||||
Submodule floret-kit updated: 75b90d1b3b...b4d3ead8f0
Reference in New Issue
Block a user