diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..f0736e4
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,20 @@
+# Normalise line endings in the repository, check out native on Windows.
+* text=auto
+
+# Scripts and wrappers that must keep LF whatever the platform.
+gradlew text eol=lf
+*.sh text eol=lf
+
+# Windows batch files need CRLF.
+*.bat text eol=crlf
+
+# Binary assets, never diffed or line-ending converted.
+*.jar binary
+*.ttf binary
+*.png binary
+*.apk binary
+*.aab binary
+*.keystore binary
+
+# Fonts are third party, keep them out of the language statistics.
+Rannah/app/src/main/res/font/* linguist-vendored
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..7149003
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,18 @@
+version: 2
+updates:
+ # The Gradle build, including the version catalog in gradle/libs.versions.toml.
+ - package-ecosystem: gradle
+ directory: /Rannah
+ schedule:
+ interval: monthly
+ open-pull-requests-limit: 5
+ commit-message:
+ prefix: build
+
+ - package-ecosystem: github-actions
+ directory: /
+ schedule:
+ interval: monthly
+ open-pull-requests-limit: 3
+ commit-message:
+ prefix: ci
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..f5fae45
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,45 @@
+name: CI
+
+on:
+ push:
+ branches: [master]
+ pull_request:
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+defaults:
+ run:
+ working-directory: Rannah
+
+jobs:
+ build:
+ name: Unit tests, lint and debug build
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v7
+
+ - uses: actions/setup-java@v5
+ with:
+ distribution: temurin
+ java-version: 17
+ cache: gradle
+
+ - name: Unit tests
+ run: ./gradlew testDebugUnitTest --no-daemon
+
+ - name: Android Lint
+ run: ./gradlew lintDebug --no-daemon
+
+ - name: Debug build
+ run: ./gradlew assembleDebug --no-daemon
+
+ - name: Upload reports on failure
+ if: failure()
+ uses: actions/upload-artifact@v7
+ with:
+ name: reports
+ path: |
+ Rannah/app/build/reports/
+ retention-days: 7
diff --git a/README.md b/README.md
index a128f2e..a8e6d67 100644
--- a/README.md
+++ b/README.md
@@ -1,21 +1,107 @@
-
-
+
+
-# رَنّة
-
-لكل موعد رَنّة
+
+
+
+
+
+**رَنّة** is an Arabic reminders app for Android. It handles the three things a reminder actually needs: ring at the right moment, survive a reboot, and let you answer it once. One-time, daily and recurring reminders, snooze, completion confirmation, and Hijri dates next to Gregorian.
+
+It works entirely offline. There is no account, no analytics, and the app declares no internet permission at all.
+
+**Status:** released and in use. Version 1.1.0 is on the [releases page](https://github.com/Mod578/Rannah/releases/latest), with an APK, an app bundle and a `SHA256SUMS.txt` to check them against.
+
+## Install
+
+1. Download `rannah-1.1.0.apk` from the [latest release](https://github.com/Mod578/Rannah/releases/latest).
+2. Open it and allow installation from this source when Android asks.
+3. Grant the notification and exact alarm permissions on first launch, otherwise reminders cannot ring on time.
+
+Requires Android 8.0 or newer.
+
+## Features
+
+- One-time, daily, weekly, monthly and yearly reminders
+- Snooze by your default duration, or by a duration you pick for a single occurrence
+- Skip today's occurrence without breaking the series
+- Confirm completion with a deliberate slide, so a half-awake tap never records a task as done
+- Pause and resume a recurring reminder
+- Hijri dates alongside Gregorian, with a user adjustment offset
+- Arabic natural language entry: «ذكرني كل يوم الساعة ٩ بالدواء» becomes a scheduled daily reminder
+- Home screen widget showing the next reminder
+- Fully Arabic, right to left throughout
+
+## How it works
+
+The database is the source of truth and every alarm is derivable from it. Nothing about a reminder lives only in `AlarmManager`, so a reboot, a process death, a clock change or an app update can rebuild the whole schedule without losing anything.
+
+`ReminderScheduler` owns the lifecycle, and every action through it is idempotent, so a replayed broadcast or a double tap cannot give one occurrence two outcomes.
+
+Full notes in [ARCHITECTURE.md](Rannah/docs/ARCHITECTURE.md).
+
+## Build from source
+
+Requires JDK 17 and the Android SDK at API level 35.
+
+```bash
+cd Rannah
+./gradlew testDebugUnitTest # unit tests
+./gradlew lintDebug # Android Lint
+./gradlew assembleDebug # installable debug build
+```
+
+The APK lands in `app/build/outputs/apk/debug/`. Release builds read their signing credentials from a properties file outside the repository, and still assemble unsigned when that file is absent.
+
+
+Project layout
+
+```
+Rannah/
+├── app/src/main/java/com/bal/reminders/
+│ ├── ui/ Compose screens and ViewModels
+│ ├── domain/ models, recurrence, occurrence state
+│ ├── data/ Room database, DAO, repository, settings
+│ ├── scheduling/ lifecycle owner, alarms, notifications, receivers
+│ ├── parser/ Arabic text to a schedule
+│ ├── format/ Gregorian and Hijri formatting
+│ └── widget/ home screen widget
+├── app/src/test/ JVM unit tests
+├── app/schemas/ exported Room schemas, used by the migration tests
+└── docs/ architecture and privacy
+```
+
+
+
+## Tech stack
+
+
+
+
+
+
+
+## Privacy
+
+No account, no ads, no analytics, no tracking. Reminder data is stored locally on the device.
+
+One honest caveat: Android's own backup may copy the app's data to your Google account, depending on your device settings. The [privacy statement](Rannah/docs/PRIVACY.md) says so plainly rather than claiming everything stays on the device.
+
+
+بالعربية
+
-رَنّة تطبيق عربي للتذكيرات يتيح إنشاء تذكير لمرة واحدة أو يومي أو متكرر، مع التأجيل وتأكيد الإنجاز وإدارة موعد اليوم بوضوح.
+**رَنّة** تطبيق عربي للتذكيرات: لمرة واحدة، أو يومي، أو متكرر، مع التأجيل وتأكيد الإنجاز وإدارة موعد اليوم بوضوح.
-## المزايا
+**المزايا**
- تذكير لمرة واحدة أو يومي أو متكرر
- تأجيل بمدة افتراضية، أو بمدة تختارها لموعد واحد
@@ -23,33 +109,22 @@
- تأكيد الإنجاز بالسحب
- إيقاف التذكير المتكرر واستئنافه
- عرض التاريخ الميلادي والهجري
+- إدخال بالعربية الطبيعية: «ذكرني كل يوم الساعة ٩ بالدواء»
- واجهة عربية بالكامل
- حفظ البيانات محليًا على الجهاز
-## التنزيل
-
-[تنزيل أحدث إصدار](https://github.com/Mod578/Rannah/releases/latest)
-
-ملف `APK` هو الملف المستخدم للتثبيت المباشر على أندرويد. قد يطلب النظام السماح بالتثبيت من هذا المصدر.
+**التنزيل**
-## الخصوصية
+[أحدث إصدار](https://github.com/Mod578/Rannah/releases/latest). ملف `APK` للتثبيت المباشر على أندرويد 8.0 أو أحدث، وقد يطلب النظام السماح بالتثبيت من هذا المصدر.
-- بلا حساب
-- بلا إعلانات
-- بلا تحليلات أو تتبع
-- تُحفظ بيانات التذكيرات محليًا على الجهاز
-- قد يشمل النسخ الاحتياطي في أندرويد بيانات التطبيق وفق إعدادات الجهاز
+**الخصوصية**
-[بيان الخصوصية](Rannah/docs/PRIVACY.md)
+بلا حساب، وبلا إعلانات، وبلا تحليلات أو تتبع. تُحفظ بيانات التذكيرات محليًا، وقد يشمل النسخ الاحتياطي في أندرويد بيانات التطبيق وفق إعدادات الجهاز. التفاصيل في [بيان الخصوصية](Rannah/docs/PRIVACY.md).
-## المتطلبات
-
-أندرويد 8.0 أو أحدث.
-
-## المطوّر
+
-محمد المطيري
+
-[LinkedIn](https://www.linkedin.com/in/mutiri) · [البريد الإلكتروني](mailto:mutirieng@gmail.com)
+## Author
-
+Mohammed Almutairi. [LinkedIn](https://www.linkedin.com/in/mutiri) · [mutirieng@gmail.com](mailto:mutirieng@gmail.com)
diff --git a/Rannah/app/src/main/java/com/bal/reminders/BalApp.kt b/Rannah/app/src/main/java/com/bal/reminders/BalApp.kt
index 127487f..2229d91 100644
--- a/Rannah/app/src/main/java/com/bal/reminders/BalApp.kt
+++ b/Rannah/app/src/main/java/com/bal/reminders/BalApp.kt
@@ -29,8 +29,8 @@ class BalApp : Application(), Configuration.Provider {
super.onCreate()
notificationPresenter.ensureChannels()
ReconcileWorker.ensureScheduled(this)
- // Restore alarms whenever the process starts — covers the case where a
- // force-stop wiped them and the user just reopened the app — and clean up
+ // Restore alarms whenever the process starts, covers the case where a
+ // force-stop wiped them and the user just reopened the app, and clean up
// one-time reminders that were completed on a previous day.
appScope.launch {
scheduler.pruneFinished()
diff --git a/Rannah/app/src/main/java/com/bal/reminders/alarm/AlarmActivity.kt b/Rannah/app/src/main/java/com/bal/reminders/alarm/AlarmActivity.kt
index 8d3fcd5..b164b15 100644
--- a/Rannah/app/src/main/java/com/bal/reminders/alarm/AlarmActivity.kt
+++ b/Rannah/app/src/main/java/com/bal/reminders/alarm/AlarmActivity.kt
@@ -73,8 +73,8 @@ import kotlinx.coroutines.delay
* The full-screen alarm surface, shown over the lock screen while the ringer
* sounds. It offers exactly two answers: «تأجيل» postpones and closes; «تم»
* reveals a deliberate slide-to-confirm, and only completing that slide records
- * the occurrence as done. Nothing here — a background tap, the back gesture,
- * leaving the screen — ever counts as completion.
+ * the occurrence as done. Nothing here (a background tap, the back gesture,
+ * leaving the screen) ever counts as completion.
*/
@AndroidEntryPoint
class AlarmActivity : ComponentActivity() {
@@ -191,7 +191,7 @@ private fun AlarmScreen(viewModel: AlarmViewModel) {
}
}
// The label reads the setting, so it is always describing what the
- // button will actually do — including right after the user changed it.
+ // button will actually do, including right after the user changed it.
val snoozeLabel = context.resources.getQuantityString(
R.plurals.notification_snooze_minutes,
state.defaultSnoozeMinutes,
@@ -206,7 +206,7 @@ private fun AlarmScreen(viewModel: AlarmViewModel) {
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
- // While the ring sounds, the bell swings from its loop — a calm
+ // While the ring sounds, the bell swings from its loop, a calm
// pendulum, the one place motion says "this is happening now".
val swing by rememberInfiniteTransition(label = "swing").animateFloat(
initialValue = -9f,
@@ -296,7 +296,7 @@ private fun AlarmScreen(viewModel: AlarmViewModel) {
)
}
Spacer(Modifier.height(8.dp))
- // «مدة أخرى»: one quiet, ordinary, focusable button — not a
+ // «مدة أخرى»: one quiet, ordinary, focusable button, not a
// long-press, which TalkBack and switch access cannot reach and
// nobody discovers. Everything it offers lives one layer down,
// so the ringing screen keeps its two large answers.
diff --git a/Rannah/app/src/main/java/com/bal/reminders/alarm/AlarmViewModel.kt b/Rannah/app/src/main/java/com/bal/reminders/alarm/AlarmViewModel.kt
index fd229d0..b3ddba1 100644
--- a/Rannah/app/src/main/java/com/bal/reminders/alarm/AlarmViewModel.kt
+++ b/Rannah/app/src/main/java/com/bal/reminders/alarm/AlarmViewModel.kt
@@ -99,7 +99,7 @@ class AlarmViewModel @Inject constructor(
confirming.value = false
}
- /** «تأجيل»: the global default, applied now — not a number frozen into the reminder. */
+ /** «تأجيل»: the global default, applied now, not a number frozen into the reminder. */
fun snooze() = resolveOnce { id, occurrence ->
scheduler.snooze(id, occurrence, SnoozeRequest.Default)
}
diff --git a/Rannah/app/src/main/java/com/bal/reminders/data/Mappers.kt b/Rannah/app/src/main/java/com/bal/reminders/data/Mappers.kt
index b88cac1..aef6513 100644
--- a/Rannah/app/src/main/java/com/bal/reminders/data/Mappers.kt
+++ b/Rannah/app/src/main/java/com/bal/reminders/data/Mappers.kt
@@ -23,7 +23,7 @@ private const val TYPE_HIJRI_MONTHLY_LEGACY = "hijri_monthly"
/**
* رَنّة dropped categories, priorities, alert modes, follow-up, per-reminder
- * alarm options and — since 1.1 — the per-reminder snooze length. The columns
+ * alarm options and, since 1.1, the per-reminder snooze length. The columns
* survive so old databases migrate losslessly (and a downgrade would still
* read); every new row writes these neutral defaults, and nothing in the app
* reads them back. Hijri schedule columns are also preserved so reminders saved
diff --git a/Rannah/app/src/main/java/com/bal/reminders/data/db/BalDatabase.kt b/Rannah/app/src/main/java/com/bal/reminders/data/db/BalDatabase.kt
index cf34729..48c6e8b 100644
--- a/Rannah/app/src/main/java/com/bal/reminders/data/db/BalDatabase.kt
+++ b/Rannah/app/src/main/java/com/bal/reminders/data/db/BalDatabase.kt
@@ -96,8 +96,8 @@ abstract class BalDatabase : RoomDatabase() {
* v4: drops `stopMarksCompleted`.
*
* The column let «إيقاف الصوت» record a completion. That made one verb
- * mean two things — silence a ringer, and assert that an obligation in
- * the world was met — and which one it meant depended on a switch buried
+ * mean two things: silence a ringer, and assert that an obligation in
+ * the world was met, and which one it meant depended on a switch buried
* in customization. رَنّة now has exactly one rule: stopping a sound
* stops a sound. Completion is always its own deliberate act.
*
@@ -183,13 +183,13 @@ abstract class BalDatabase : RoomDatabase() {
/**
* v6: one answer per occurrence, and one global snooze length.
*
- * The table shape does not change — this migration is entirely about
+ * The table shape does not change: this migration is entirely about
* data that older builds could produce and 1.1 no longer can:
*
* - An occurrence could hold **both** a `completed` and a `skipped`
* record. The unique index is per (reminder, occurrence, status), so
* SQLite always allowed the pair, and two surfaces racing could write
- * it. «تم» is the stronger claim — it asserts the task happened — so a
+ * it. «تم» is the stronger claim: it asserts the task happened, so a
* contradicting `skipped` row is dropped and the completion stands.
* - A `missed` record alongside an answer said two things about one
* occurrence. The answer is the later and truer one; the `missed` row
diff --git a/Rannah/app/src/main/java/com/bal/reminders/data/db/Entities.kt b/Rannah/app/src/main/java/com/bal/reminders/data/db/Entities.kt
index b8a255a..f8ca997 100644
--- a/Rannah/app/src/main/java/com/bal/reminders/data/db/Entities.kt
+++ b/Rannah/app/src/main/java/com/bal/reminders/data/db/Entities.kt
@@ -14,19 +14,19 @@ data class ReminderEntity(
val priority: Int,
/** one of: once, daily, weekly, monthly, yearly */
val recurrenceType: String,
- /** gregorian or hijri — the calendar the once/monthly/yearly dates live in */
+ /** gregorian or hijri: the calendar the once/monthly/yearly dates live in */
@ColumnInfo(defaultValue = "gregorian") val calendar: String = "gregorian",
/** trigger time as minutes from midnight */
val timeMinutes: Int,
- /** ISO local date — Gregorian one-time only */
+ /** ISO local date: Gregorian one-time only */
val date: String?,
- /** Hijri year — Hijri one-time only */
+ /** Hijri year: Hijri one-time only */
val year: Int?,
- /** month 1..12 — yearly and Hijri one-time */
+ /** month 1..12: yearly and Hijri one-time */
val month: Int?,
- /** bitmask, bit (isoDayOfWeek - 1) — only for weekly */
+ /** bitmask, bit (isoDayOfWeek - 1), only for weekly */
val daysOfWeek: Int,
- /** 1..31 — monthly, yearly and Hijri one-time */
+ /** 1..31: monthly, yearly and Hijri one-time */
val dayOfMonth: Int?,
val enabled: Boolean,
/** standard or alarm */
diff --git a/Rannah/app/src/main/java/com/bal/reminders/data/db/ReminderDao.kt b/Rannah/app/src/main/java/com/bal/reminders/data/db/ReminderDao.kt
index 859954b..a1d54b5 100644
--- a/Rannah/app/src/main/java/com/bal/reminders/data/db/ReminderDao.kt
+++ b/Rannah/app/src/main/java/com/bal/reminders/data/db/ReminderDao.kt
@@ -91,8 +91,8 @@ interface ReminderDao {
/**
* Records «تم» or «تخطي اليوم» for one occurrence, and refuses if that
* occurrence already has *either* answer. The unique index alone cannot
- * express this — it is per (reminder, occurrence, status), so a COMPLETED and
- * a SKIPPED row for the same occurrence are both legal to SQLite — and a
+ * express this: it is per (reminder, occurrence, status), so a COMPLETED and
+ * a SKIPPED row for the same occurrence are both legal to SQLite, and a
* plain unique index on (reminder, occurrence) would forbid the MISSED row
* that legitimately precedes a late answer. So the invariant lives here, in
* one transaction: read, decide, write.
@@ -175,7 +175,7 @@ interface ReminderDao {
/**
* Drops occurrence records that are past on both axes: recorded before
* [before] *and* for an occurrence before it. The second condition protects a
- * reminder completed ahead of time — its record is young but its occurrence
+ * reminder completed ahead of time: its record is young but its occurrence
* is still in the future, and it is the only thing marking that occurrence
* resolved.
*/
diff --git a/Rannah/app/src/main/java/com/bal/reminders/domain/OccurrenceState.kt b/Rannah/app/src/main/java/com/bal/reminders/domain/OccurrenceState.kt
index 4acb1f1..ac48586 100644
--- a/Rannah/app/src/main/java/com/bal/reminders/domain/OccurrenceState.kt
+++ b/Rannah/app/src/main/java/com/bal/reminders/domain/OccurrenceState.kt
@@ -8,41 +8,41 @@ import java.time.ZoneId
/**
* The single user-facing state of a reminder's current occurrence. Every surface
- * — home, details, and (through the same instants) notifications and alarm
- * restoration — reads this instead of re-deriving state from raw fields, so they
+ * home, details, and (through the same instants) notifications and alarm
+ * restoration: reads this instead of re-deriving state from raw fields, so they
* can never disagree.
*/
enum class ReminderPhase {
- /** قادم — enabled, next occurrence is still ahead (today-later or a future day). */
+ /** قادم: enabled, next occurrence is still ahead (today-later or a future day). */
UPCOMING,
- /** مؤجل — postponed; waiting until a known instant. */
+ /** مؤجل: postponed; waiting until a known instant. */
SNOOZED,
- /** يحتاج تأكيدك — today's occurrence has passed and is still unresolved. */
+ /** يحتاج تأكيدك: today's occurrence has passed and is still unresolved. */
NEEDS_CONFIRMATION,
/**
- * متأخر — a one-time reminder whose day is behind us and which was never
+ * متأخر: a one-time reminder whose day is behind us and which was never
* answered. It is deliberately *not* [NEEDS_CONFIRMATION]: that state belongs
* to today, and filing a three-week-old errand under «اليوم» with nothing but
* a clock time told the user something untrue. An overdue reminder is shown
- * with its real date, above the day, until it is completed or deleted — it is
+ * with its real date, above the day, until it is completed or deleted, it is
* never silently discarded.
*/
OVERDUE,
- /** مكتمل — a one-time reminder that has been completed. */
+ /** مكتمل: a one-time reminder that has been completed. */
COMPLETED,
- /** متوقف مؤقتًا — paused (disabled), or a legacy ended recurring series. */
+ /** متوقف مؤقتًا: paused (disabled), or a legacy ended recurring series. */
PAUSED,
}
/**
* The resolved current occurrence of one reminder. [occurrenceAt] is the identity
- * an action (complete/undo) must use — the same identity the alarm and the home
- * use — and [displayAt] is the instant to show (the snooze time when SNOOZED, the
+ * an action (complete/undo) must use: the same identity the alarm and the home
+ * use, and [displayAt] is the instant to show (the snooze time when SNOOZED, the
* occurrence/next time otherwise).
*/
data class ReminderOccurrence(
@@ -107,7 +107,7 @@ object OccurrenceStateResolver {
view(ReminderPhase.COMPLETED, onceInstant(reminder.schedule, zone), reminder.completedAt)
}
}
- // Paused: silent, subdued, reachable — and [displayAt] is the occurrence
+ // Paused: silent, subdued, reachable, and [displayAt] is the occurrence
// it would return to, so «استئناف» is never a leap in the dark.
if (!reminder.enabled) {
return view(ReminderPhase.PAUSED, null, nextFrom(reminder, now, zone))
diff --git a/Rannah/app/src/main/java/com/bal/reminders/domain/ReminderRepository.kt b/Rannah/app/src/main/java/com/bal/reminders/domain/ReminderRepository.kt
index c51e625..b07f145 100644
--- a/Rannah/app/src/main/java/com/bal/reminders/domain/ReminderRepository.kt
+++ b/Rannah/app/src/main/java/com/bal/reminders/domain/ReminderRepository.kt
@@ -12,7 +12,7 @@ interface ReminderRepository {
fun observeById(id: Long): Flow
suspend fun getById(id: Long): Reminder?
- /** Enabled, not-completed reminders — the set that needs alarms. */
+ /** Enabled, not-completed reminders, the set that needs alarms. */
suspend fun getActive(): List
/** Inserts when id == 0, updates otherwise. Returns the reminder id. */
diff --git a/Rannah/app/src/main/java/com/bal/reminders/domain/model/DeletedReminder.kt b/Rannah/app/src/main/java/com/bal/reminders/domain/model/DeletedReminder.kt
index 270a609..958e7ee 100644
--- a/Rannah/app/src/main/java/com/bal/reminders/domain/model/DeletedReminder.kt
+++ b/Rannah/app/src/main/java/com/bal/reminders/domain/model/DeletedReminder.kt
@@ -2,8 +2,8 @@ package com.bal.reminders.domain.model
/**
* A reminder that was just deleted, with the occurrence records that went with
- * it. رَنّة never keeps a deleted row in the database waiting to be revived —
- * deletion is a real delete — so the only thing standing between «حذف» and
+ * it. رَنّة never keeps a deleted row in the database waiting to be revived,
+ * deletion is a real delete, so the only thing standing between «حذف» and
* «تراجع» is this snapshot, held in memory for as long as the undo is offered.
* If the process dies first, the deletion simply stands.
*/
diff --git a/Rannah/app/src/main/java/com/bal/reminders/domain/model/OccurrenceRecord.kt b/Rannah/app/src/main/java/com/bal/reminders/domain/model/OccurrenceRecord.kt
index 956ffeb..7475ae0 100644
--- a/Rannah/app/src/main/java/com/bal/reminders/domain/model/OccurrenceRecord.kt
+++ b/Rannah/app/src/main/java/com/bal/reminders/domain/model/OccurrenceRecord.kt
@@ -15,8 +15,8 @@ enum class OccurrenceStatus(val id: String) {
/**
* «تم تجاهله»: the alert reached the user and was pushed away without an
- * answer. Nothing writes this any more — it belonged to the removed
- * follow-up feature — but databases from those versions still hold such
+ * answer. Nothing writes this any more: it belonged to the removed
+ * follow-up feature, but databases from those versions still hold such
* rows, so it stays readable rather than being silently re-read as
* something it was not.
*/
@@ -33,8 +33,8 @@ enum class OccurrenceStatus(val id: String) {
* An unrecognised value reads as [MISSED], never as [COMPLETED].
*
* The fallback used to be "completed", which meant a row written by a
- * build this one does not know about — a downgrade, a restored backup, a
- * future status — would be read as *the user asserting the task was
+ * build this one does not know about, a downgrade, a restored backup, a
+ * future status, would be read as *the user asserting the task was
* done*, and would silently resolve the occurrence. Guessing wrong
* towards "unanswered" costs one extra ring; guessing wrong towards
* "done" loses the reminder.
diff --git a/Rannah/app/src/main/java/com/bal/reminders/domain/model/Reminder.kt b/Rannah/app/src/main/java/com/bal/reminders/domain/model/Reminder.kt
index 1e682cd..24ec421 100644
--- a/Rannah/app/src/main/java/com/bal/reminders/domain/model/Reminder.kt
+++ b/Rannah/app/src/main/java/com/bal/reminders/domain/model/Reminder.kt
@@ -6,7 +6,7 @@ import java.time.Instant
* A single reminder. رَنّة has one alerting behaviour: every reminder rings as a
* full-screen alarm at its time, and is either postponed («تأجيل») or confirmed
* done («تم»). There is no per-reminder alert style, follow-up policy, ringtone
- * or snooze length to configure — the whole point of the app is that it just
+ * or snooze length to configure: the whole point of the app is that it just
* rings, and that «مدة التأجيل الافتراضية» in settings means what it says for
* every reminder, not only for the ones created after it was changed.
*/
@@ -19,7 +19,7 @@ data class Reminder(
/** Set while a fired occurrence is snoozed; overrides the natural next occurrence. */
val snoozedUntil: Instant? = null,
/**
- * The occurrence a snooze is postponing — its identity, kept across any number
+ * The occurrence a snooze is postponing, its identity, kept across any number
* of «تأجيل» taps. Postponing moves *when* رَنّة asks again; it never turns the
* 9:00 occurrence into a 9:10 one. Completing later therefore resolves the
* occurrence that actually rang, so it cannot come back as «يحتاج تأكيدك».
diff --git a/Rannah/app/src/main/java/com/bal/reminders/domain/model/Schedule.kt b/Rannah/app/src/main/java/com/bal/reminders/domain/model/Schedule.kt
index 0e296a4..bf3b987 100644
--- a/Rannah/app/src/main/java/com/bal/reminders/domain/model/Schedule.kt
+++ b/Rannah/app/src/main/java/com/bal/reminders/domain/model/Schedule.kt
@@ -9,18 +9,18 @@ import java.time.LocalTime
* question the editor asks: «ما نوع التذكير؟».
*
* This is a *presentation* classification over the one scheduling engine, not a
- * second model. [DAILY] is [Schedule.Daily] — a preset, not a separate code path
- * — but it is named and chosen on its own because "every day" is what most
+ * second model. [DAILY] is [Schedule.Daily], a preset, not a separate code path
+ * but it is named and chosen on its own because "every day" is what most
* people actually want and burying it inside «متكرر» made them hunt for it.
*/
enum class ReminderKind {
- /** مرة واحدة — one Gregorian date and time, then it is finished. */
+ /** مرة واحدة: one Gregorian date and time, then it is finished. */
ONCE,
- /** يومي — every day at the same time. */
+ /** يومي: every day at the same time. */
DAILY,
- /** متكرر — weekly days, monthly, or yearly. */
+ /** متكرر: weekly days, monthly, or yearly. */
RECURRING,
}
diff --git a/Rannah/app/src/main/java/com/bal/reminders/format/BalFormats.kt b/Rannah/app/src/main/java/com/bal/reminders/format/BalFormats.kt
index dfde28a..60db2ff 100644
--- a/Rannah/app/src/main/java/com/bal/reminders/format/BalFormats.kt
+++ b/Rannah/app/src/main/java/com/bal/reminders/format/BalFormats.kt
@@ -23,7 +23,7 @@ object BalFormats {
val arabicLocale: Locale = Locale("ar")
- /** ٩:٠٠ — converts ASCII digits to Arabic-Indic. */
+ /** ٩:٠٠: converts ASCII digits to Arabic-Indic. */
fun arabicDigits(text: String): String =
buildString(text.length) {
text.forEach { ch -> append(if (ch in '0'..'9') '٠' + (ch - '0') else ch) }
@@ -67,7 +67,7 @@ object BalFormats {
fun dayName(day: DayOfWeek): String = day.getDisplayName(TextStyle.FULL, arabicLocale)
- /** «أحد» — the bare name, for «كل أحد» where the article would be wrong. */
+ /** «أحد»: the bare name, for «كل أحد» where the article would be wrong. */
fun bareDayName(day: DayOfWeek): String = dayName(day).removePrefix("ال")
/** «٩:٠٠ صباحًا» for an instant in [zone]. */
@@ -85,7 +85,7 @@ object BalFormats {
/** «الأربعاء» for the date's actual weekday (calendar-independent). */
fun weekdayName(date: LocalDate): String = dayName(date.dayOfWeek)
- /** «السبت، ١٨ يوليو» — the stable home header (weekday + day + month, no year). */
+ /** «السبت، ١٨ يوليو»: the stable home header (weekday + day + month, no year). */
fun headerDate(date: LocalDate): String =
weekdayName(date) + "، " + arabicDigits(date.format(DAY_MONTH))
@@ -126,7 +126,7 @@ object BalFormats {
"${weekdayName(date)} ${gregorianDate(date)}" to hijriFull(date)
fun dayNames(days: Set): String {
- // Saturday-first ordering — the Arabic week.
+ // Saturday-first ordering: the Arabic week.
val ordered = listOf(
DayOfWeek.SATURDAY, DayOfWeek.SUNDAY, DayOfWeek.MONDAY, DayOfWeek.TUESDAY,
DayOfWeek.WEDNESDAY, DayOfWeek.THURSDAY, DayOfWeek.FRIDAY,
@@ -155,7 +155,7 @@ object BalFormats {
private val ALL_DAYS = DayOfWeek.entries.toSet()
/**
- * The kind badge every surface wears — creation, home, details — so nobody
+ * The kind badge every surface wears (creation, home, details), so nobody
* has to open documentation, or infer from a chip, to know what they made:
* «مرة واحدة», «يومي», «أيام العمل», «كل أحد», «الأحد والثلاثاء», «أسبوعي»,
* «شهري», «سنوي».
@@ -246,7 +246,7 @@ object BalFormats {
}
/**
- * A moment in the shortest words that are still exact — the one formatter
+ * A moment in the shortest words that are still exact, the one formatter
* every surface uses for "when": the home rows, the closed rows, the details
* status lines, the history and the widget.
*
@@ -256,7 +256,7 @@ object BalFormats {
* The weekday form is used only inside the coming week, where «الأحد» can
* only mean one Sunday; a date in the past or further out is named outright,
* so nothing ever reads as a relative day it is not. Everything is derived
- * from the instant that was actually scheduled — there is no fixed phrase
+ * from the instant that was actually scheduled, there is no fixed phrase
* anywhere that assumes "tomorrow".
*/
fun dateTime(
diff --git a/Rannah/app/src/main/java/com/bal/reminders/parser/ArabicReminderParser.kt b/Rannah/app/src/main/java/com/bal/reminders/parser/ArabicReminderParser.kt
index 70b85fa..e9f20c0 100644
--- a/Rannah/app/src/main/java/com/bal/reminders/parser/ArabicReminderParser.kt
+++ b/Rannah/app/src/main/java/com/bal/reminders/parser/ArabicReminderParser.kt
@@ -68,7 +68,7 @@ class ArabicReminderParser @Inject constructor() : ReminderParser {
// A suggestion is offered only when the sentence carried both halves. A
// schedule with no title, or a date with no time, is not something to
- // put in front of the user as «فهمت: …» — the pickers below already say
+ // put in front of the user as «فهمت: …», the pickers below already say
// what رَنّة will do, and a half-guess would only have to be corrected.
return if (schedule != null && title.isNotBlank()) {
ParseResult.Success(title, schedule)
@@ -327,7 +327,7 @@ class ArabicReminderParser @Inject constructor() : ReminderParser {
)
val WEEKLY = w("كل (?:يوم |ايام )?(?:ال)?(?:$DAY_ALT)(?: ?و ?(?:ال)?(?:$DAY_ALT))*")
- // و? — a day may arrive glued to واو العطف («وثلاثاء», «والثلاثا»).
+ // و?: a day may arrive glued to واو العطف («وثلاثاء», «والثلاثا»).
val DAY_TOKEN = Regex("(? Unit
}
/**
- * An occurrence that is finished for today — completed, or deliberately skipped.
+ * An occurrence that is finished for today, completed, or deliberately skipped.
*
* The two outcomes are told apart three ways at once: the mark (a check or a
* skip arrow), the leading word of [meta] («مكتمل» / «تم تخطيه»), and the fill
@@ -483,7 +483,7 @@ internal fun Modifier.forwardShift(x: () -> Float): Modifier = this.then(
/**
* The signature empty state: the «رَنّة» bell drawn calm and large, standing on
- * its own. No stock illustration, and no container behind it — the mark is the
+ * its own. No stock illustration, and no container behind it, the mark is the
* mark, on whatever surface it lands.
*/
@Composable
diff --git a/Rannah/app/src/main/java/com/bal/reminders/ui/components/SlideToConfirm.kt b/Rannah/app/src/main/java/com/bal/reminders/ui/components/SlideToConfirm.kt
index 2960844..a1887d1 100644
--- a/Rannah/app/src/main/java/com/bal/reminders/ui/components/SlideToConfirm.kt
+++ b/Rannah/app/src/main/java/com/bal/reminders/ui/components/SlideToConfirm.kt
@@ -44,7 +44,7 @@ import kotlinx.coroutines.launch
* Slide to confirm «تم الإنجاز» on the alarm screen.
*
* The alarm screen is the one place رَنّة is read by someone half-awake, in the
- * dark, with a sound going. A tap there is not evidence of intent — it is
+ * dark, with a sound going. A tap there is not evidence of intent: it is
* evidence of wanting the noise to stop. So the act that writes down "I did the
* thing in the real world" asks for a movement nobody performs by accident, and
* the track is labelled with the claim it records («اسحب للتأكيد»), not a bare
@@ -67,7 +67,7 @@ import kotlinx.coroutines.launch
*
* The track has a *minimum* height, not a fixed one, and the label is allowed
* two lines. At a 200% font scale «اسحب للتأكيد» no longer runs out of a 76dp
- * box and get clipped — on the one screen where an ambiguous confirmation is
+ * box and get clipped: on the one screen where an ambiguous confirmation is
* unacceptable, the control grows instead.
*/
@Composable
diff --git a/Rannah/app/src/main/java/com/bal/reminders/ui/components/SnoozeSheet.kt b/Rannah/app/src/main/java/com/bal/reminders/ui/components/SnoozeSheet.kt
index 5ee89f5..591c581 100644
--- a/Rannah/app/src/main/java/com/bal/reminders/ui/components/SnoozeSheet.kt
+++ b/Rannah/app/src/main/java/com/bal/reminders/ui/components/SnoozeSheet.kt
@@ -55,7 +55,7 @@ import java.time.ZoneId
*
* The choice made here applies to **this occurrence only** and is never
* remembered. The next ring goes back to «مدة التأجيل الافتراضية», so the big
- * button's label is always telling the truth — a duration silently inherited
+ * button's label is always telling the truth, a duration silently inherited
* from a decision made at 3am is worse than no flexibility at all.
*
* [limit] is the last instant this occurrence may be postponed to. A choice past
@@ -164,7 +164,7 @@ fun SnoozeSheet(
/**
* «حتى وقت محدد». The resolved instant is spelled out in full before anything is
- * confirmed — «اليوم، ٨:٣٠ مساءً» or «غدًا، ٦:٠٠ صباحًا» — so whether the chosen
+ * confirmed: «اليوم، ٨:٣٠ مساءً» or «غدًا، ٦:٠٠ صباحًا», so whether the chosen
* clock time belongs to today or tomorrow is *stated*, never left for the user
* to work out from a bare time and a hope.
*/
@@ -223,7 +223,7 @@ private fun UntilTimePicker(
}
}
-/** The next occurrence of [time] strictly after [now] — today if it is still ahead, else tomorrow. */
+/** The next occurrence of [time] strictly after [now], today if it is still ahead, else tomorrow. */
internal fun resolveNext(time: LocalTime, now: Instant, zone: ZoneId): Instant {
val today = now.atZone(zone).toLocalDate()
val candidate = today.atTime(time).atZone(zone).toInstant()
diff --git a/Rannah/app/src/main/java/com/bal/reminders/ui/details/DetailsScreen.kt b/Rannah/app/src/main/java/com/bal/reminders/ui/details/DetailsScreen.kt
index 10ccd11..0a2d65b 100644
--- a/Rannah/app/src/main/java/com/bal/reminders/ui/details/DetailsScreen.kt
+++ b/Rannah/app/src/main/java/com/bal/reminders/ui/details/DetailsScreen.kt
@@ -70,7 +70,7 @@ import com.bal.reminders.ui.theme.Space
* **«اليوم»** acts on today's occurrence and nothing else: «تم» closes it as
* done, «تخطي اليوم» closes it without pretending it was, and both leave every
* future day exactly where it was. **«التذكير»** acts on the whole thing: edit
- * it, pause it, delete it. Two headings, two scopes — which is the entire
+ * it, pause it, delete it. Two headings, two scopes, which is the entire
* difference between a repeating reminder and today's ring, taught by layout
* rather than by explanation.
*
@@ -201,7 +201,7 @@ fun DetailsScreen(
}
}
// A live postponement can be moved or taken back
- // here — the two things the ringing screen has no
+ // here: the two things the ringing screen has no
// room for, offered where there is daylight and space.
if (state.snoozed) {
OutlinedButton(
@@ -350,7 +350,7 @@ fun DetailsScreen(
private const val HISTORY_LIMIT = 8
-/** Title, kind, cadence and state — the whole reminder in one card. */
+/** Title, kind, cadence and state, the whole reminder in one card. */
@Composable
private fun SummaryCard(
title: String,
diff --git a/Rannah/app/src/main/java/com/bal/reminders/ui/details/DetailsViewModel.kt b/Rannah/app/src/main/java/com/bal/reminders/ui/details/DetailsViewModel.kt
index 781e681..e7df829 100644
--- a/Rannah/app/src/main/java/com/bal/reminders/ui/details/DetailsViewModel.kt
+++ b/Rannah/app/src/main/java/com/bal/reminders/ui/details/DetailsViewModel.kt
@@ -92,7 +92,7 @@ class DetailsViewModel @Inject constructor(
// The occurrence this zone acts on: a live one first, always. Preferring
// an answer given earlier today would hide a *new* occurrence that an
- // edit has put back on the clock — the screen would say «اكتمل اليوم»
+ // edit has put back on the clock: the screen would say «اكتمل اليوم»
// while an alarm it never mentioned was still armed for this evening.
val live = occurrence?.occurrenceAt?.takeIf {
when (occurrence.phase) {
@@ -119,7 +119,7 @@ class DetailsViewModel @Inject constructor(
)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), DetailsState())
- /** «تم» — this occurrence only; a repeating reminder keeps its future. */
+ /** «تم»: this occurrence only; a repeating reminder keeps its future. */
fun complete(occurrenceAt: Instant) {
viewModelScope.launch {
val done = scheduler.complete(id, occurrenceAt) ?: return@launch
@@ -131,7 +131,7 @@ class DetailsViewModel @Inject constructor(
}
}
- /** «تخطي اليوم» — close today without claiming it was done. Repeating only. */
+ /** «تخطي اليوم»: close today without claiming it was done. Repeating only. */
fun skipToday(occurrenceAt: Instant) {
viewModelScope.launch {
val skipped = scheduler.skipOccurrence(id, occurrenceAt) ?: return@launch
@@ -154,7 +154,7 @@ class DetailsViewModel @Inject constructor(
// ------------------------------------------------------------------ تأجيل
- /** «تغيير وقت التأجيل» — the same sheet the alarm screen offers, in daylight. */
+ /** «تغيير وقت التأجيل»: the same sheet the alarm screen offers, in daylight. */
fun openSnoozeOptions() {
val occurrence = state.value.occurrence?.occurrenceAt ?: return
viewModelScope.launch {
@@ -168,7 +168,7 @@ class DetailsViewModel @Inject constructor(
/**
* Moving a live postponement. The current snooze is cleared first so the
- * scheduler sees an unpostponed occurrence — otherwise it would refuse the
+ * scheduler sees an unpostponed occurrence, otherwise it would refuse the
* change as a duplicate of the postponement already in place.
*/
fun changeSnooze(request: SnoozeRequest) {
@@ -190,7 +190,7 @@ class DetailsViewModel @Inject constructor(
}
/**
- * «إلغاء التأجيل»: the occurrence returns to where it was — unresolved and
+ * «إلغاء التأجيل»: the occurrence returns to where it was, unresolved and
* unanswered. It is not completed, not skipped, not paused and not deleted.
*/
fun cancelSnooze() {
@@ -199,7 +199,7 @@ class DetailsViewModel @Inject constructor(
// ------------------------------------------------------------ the reminder
- /** «إيقاف مؤقت» / «استئناف» — the whole reminder. */
+ /** «إيقاف مؤقت» / «استئناف»: the whole reminder. */
fun setEnabled(enabled: Boolean) {
viewModelScope.launch { scheduler.setEnabled(id, enabled) }
}
diff --git a/Rannah/app/src/main/java/com/bal/reminders/ui/editor/EditorScreen.kt b/Rannah/app/src/main/java/com/bal/reminders/ui/editor/EditorScreen.kt
index bc93275..515bbcd 100644
--- a/Rannah/app/src/main/java/com/bal/reminders/ui/editor/EditorScreen.kt
+++ b/Rannah/app/src/main/java/com/bal/reminders/ui/editor/EditorScreen.kt
@@ -69,8 +69,8 @@ import java.time.LocalTime
import java.time.ZoneId
/**
- * Adding a reminder starts with the question that decides everything else —
- * «ما نوع التذكير؟» — and ends with one sentence confirming what رَنّة
+ * Adding a reminder starts with the question that decides everything else,
+ * «ما نوع التذكير؟», and ends with one sentence confirming what رَنّة
* understood before anything is saved.
*
* The three kinds are separate, named choices rather than five equal chips,
@@ -119,7 +119,7 @@ fun EditorScreen(
.padding(horizontal = Space.screen),
verticalArrangement = Arrangement.spacedBy(Space.md),
) {
- // ١ — ماذا
+ // ١: ماذا
OutlinedTextField(
value = state.title,
onValueChange = viewModel::setTitle,
@@ -165,7 +165,7 @@ fun EditorScreen(
}
}
- // ٢ — ما نوع التذكير؟ الاختيار الذي يقرّر بقية الشاشة.
+ // ٢: ما نوع التذكير؟ الاختيار الذي يقرّر بقية الشاشة.
Field(stringResource(R.string.kind_question)) {
ChoiceChips(
options = listOf(
@@ -189,13 +189,13 @@ fun EditorScreen(
WhenDetails(state, viewModel)
- // ٣ — الساعة
+ // ٣: الساعة
Field(stringResource(R.string.editor_time)) {
TimeRow(state.time) { viewModel.setTime(it) }
}
if (state.pastError) ErrorText(stringResource(R.string.editor_error_past))
- // ما الذي سيحدث فعلًا — بجملة واحدة قبل الحفظ.
+ // ما الذي سيحدث فعلًا: بجملة واحدة قبل الحفظ.
state.buildSchedule()?.let { schedule ->
Surface(
shape = MaterialTheme.shapes.medium,
diff --git a/Rannah/app/src/main/java/com/bal/reminders/ui/editor/EditorViewModel.kt b/Rannah/app/src/main/java/com/bal/reminders/ui/editor/EditorViewModel.kt
index c71f09a..04d2f46 100644
--- a/Rannah/app/src/main/java/com/bal/reminders/ui/editor/EditorViewModel.kt
+++ b/Rannah/app/src/main/java/com/bal/reminders/ui/editor/EditorViewModel.kt
@@ -251,8 +251,8 @@ class EditorViewModel @Inject constructor(
// silently starting to ring again.
enabled = base?.enabled ?: true,
createdAt = base?.createdAt ?: clock.instant(),
- // A completed reminder is never editable — details offers
- // «تراجع» or «حذف» instead — so this is always already null.
+ // A completed reminder is never editable, details offers
+ // «تراجع» or «حذف» instead, so this is always already null.
completedAt = null,
),
)
diff --git a/Rannah/app/src/main/java/com/bal/reminders/ui/home/ChecklistGrouping.kt b/Rannah/app/src/main/java/com/bal/reminders/ui/home/ChecklistGrouping.kt
index 0be15d1..8c29cab 100644
--- a/Rannah/app/src/main/java/com/bal/reminders/ui/home/ChecklistGrouping.kt
+++ b/Rannah/app/src/main/java/com/bal/reminders/ui/home/ChecklistGrouping.kt
@@ -13,9 +13,9 @@ import java.time.ZoneId
/**
* Which section of the home list each reminder belongs in.
*
- * This is pure on purpose. The one real defect the list ever had lived here —
+ * This is pure on purpose. The one real defect the list ever had lived here,
* a reminder was hidden from «اليوم» because it *had a completed record today*,
- * regardless of whether another occurrence was still coming — and a rule that
+ * regardless of whether another occurrence was still coming, and a rule that
* subtle deserves to be checkable without a device, a view model, or a clock
* that only moves forwards.
*/
@@ -105,7 +105,7 @@ internal object ChecklistGrouping {
)
}
- /** The real next occurrence — never a fixed phrase that assumes "tomorrow". */
+ /** The real next occurrence: never a fixed phrase that assumes "tomorrow". */
private fun nextAfter(schedule: Schedule, now: Instant, zone: ZoneId): Instant? =
RecurrenceCalculator.nextOccurrence(schedule, now.atZone(zone))?.toInstant()
}
diff --git a/Rannah/app/src/main/java/com/bal/reminders/ui/home/ChecklistScreen.kt b/Rannah/app/src/main/java/com/bal/reminders/ui/home/ChecklistScreen.kt
index a2e9f08..01dfb11 100644
--- a/Rannah/app/src/main/java/com/bal/reminders/ui/home/ChecklistScreen.kt
+++ b/Rannah/app/src/main/java/com/bal/reminders/ui/home/ChecklistScreen.kt
@@ -76,7 +76,7 @@ import java.time.ZoneId
import kotlinx.coroutines.delay
/**
- * The home screen answers one question — «ما الذي عليّ اليوم؟» — and it answers
+ * The home screen answers one question, «ما الذي عليّ اليوم؟», and it answers
* it with one list.
*
* Everything due today sits under «اليوم» in clock order, with what is waiting
@@ -100,7 +100,7 @@ fun ChecklistScreen(
val undoLabel = stringResource(R.string.action_undo)
// The one undo surface. Completing happens here; deleting happens in the
- // details screen, which closes straight after — both arrive through the same
+ // details screen, which closes straight after, both arrive through the same
// channel, so there is exactly one place «تراجع» ever appears. Each offer is
// taken before the snackbar shows, so it is never replayed.
LaunchedEffect(Unit) {
@@ -356,7 +356,7 @@ private fun metaFor(
/**
* «مكتمل · ٩:٠٠ صباحًا» for a one-time reminder, «تم تخطيه · القادمة غدًا، ٦:٠٠
- * صباحًا» for a repeating one — the state first, then the only thing still worth
+ * صباحًا» for a repeating one: the state first, then the only thing still worth
* knowing: when it rings next. The next ring comes from the schedule, never from
* a fixed phrase.
*/
@@ -379,7 +379,7 @@ private fun closedMeta(
return "$state · $tail"
}
-/** «استئناف» — the one action a paused row offers, and it is not destructive. */
+/** «استئناف»: the one action a paused row offers, and it is not destructive. */
@Composable
private fun ResumeButton(title: String, onClick: () -> Unit) {
val label = stringResource(R.string.action_resume)
diff --git a/Rannah/app/src/main/java/com/bal/reminders/ui/home/ChecklistViewModel.kt b/Rannah/app/src/main/java/com/bal/reminders/ui/home/ChecklistViewModel.kt
index ea4aee8..039467b 100644
--- a/Rannah/app/src/main/java/com/bal/reminders/ui/home/ChecklistViewModel.kt
+++ b/Rannah/app/src/main/java/com/bal/reminders/ui/home/ChecklistViewModel.kt
@@ -23,7 +23,7 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
/**
- * An occurrence that is finished for today — confirmed or deliberately skipped.
+ * An occurrence that is finished for today, confirmed or deliberately skipped.
* [returnsAt] is the next time this reminder will ring, so a skipped recurring
* reminder says out loud that tomorrow is untouched.
*/
@@ -38,11 +38,11 @@ data class ClosedItem(
data class ChecklistState(
/**
* One-time reminders whose day is behind us and which were never answered.
- * They ride above the day with their real date — filing a three-week-old
+ * They ride above the day with their real date, filing a three-week-old
* errand under «اليوم» with nothing but a clock time was simply untrue.
*/
val overdue: List = emptyList(),
- /** Everything due today: waiting, postponed, and still to come — in time order. */
+ /** Everything due today: waiting, postponed, and still to come, in time order. */
val today: List = emptyList(),
val upcoming: List = emptyList(),
val closed: List = emptyList(),
@@ -97,7 +97,7 @@ class ChecklistViewModel @Inject constructor(
ChecklistGrouping.group(reminders, records, clock.instant(), clock.zone)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), ChecklistState())
- /** «تم» on a row — the ring, or the swipe. Idempotent and undoable. */
+ /** «تم» on a row: the ring, or the swipe. Idempotent and undoable. */
fun complete(item: ReminderOccurrence) {
val occ = item.occurrenceAt ?: return
viewModelScope.launch {
@@ -114,7 +114,7 @@ class ChecklistViewModel @Inject constructor(
/**
* «تخطي اليوم» on a daily or recurring row: this occurrence is closed without
* claiming it was done, and the reminder keeps every day after it. The undo
- * message names the scope out loud — «لليوم فقط» — because the one thing a
+ * message names the scope out loud: «لليوم فقط», because the one thing a
* person needs to be sure of here is that they did not just end the series.
*/
fun skipToday(item: ReminderOccurrence) {
diff --git a/Rannah/app/src/main/java/com/bal/reminders/ui/onboarding/OnboardingScreen.kt b/Rannah/app/src/main/java/com/bal/reminders/ui/onboarding/OnboardingScreen.kt
index be090fc..eb8489b 100644
--- a/Rannah/app/src/main/java/com/bal/reminders/ui/onboarding/OnboardingScreen.kt
+++ b/Rannah/app/src/main/java/com/bal/reminders/ui/onboarding/OnboardingScreen.kt
@@ -29,7 +29,7 @@ import com.bal.reminders.ui.components.MadeInSaudi
/**
* A single calm welcome: the bell, one line about what رَنّة is, and the one
- * thing setup actually needs — permission to notify. No tour, no carousel.
+ * thing setup actually needs: permission to notify. No tour, no carousel.
*/
@Composable
fun OnboardingScreen(
diff --git a/Rannah/app/src/main/java/com/bal/reminders/ui/settings/SettingsScreen.kt b/Rannah/app/src/main/java/com/bal/reminders/ui/settings/SettingsScreen.kt
index c3677bb..a4b111a 100644
--- a/Rannah/app/src/main/java/com/bal/reminders/ui/settings/SettingsScreen.kt
+++ b/Rannah/app/src/main/java/com/bal/reminders/ui/settings/SettingsScreen.kt
@@ -226,7 +226,7 @@ private fun SettingsLink(
}
// Auto-mirrored: it points the way the next screen comes from, which
// in Arabic is leftwards. The un-mirrored left chevron this replaced
- // was flipping to point right — back, in an RTL layout.
+ // was flipping to point right: back, in an RTL layout.
Icon(
Icons.AutoMirrored.Rounded.KeyboardArrowRight,
contentDescription = null,
diff --git a/Rannah/app/src/main/java/com/bal/reminders/ui/theme/Theme.kt b/Rannah/app/src/main/java/com/bal/reminders/ui/theme/Theme.kt
index 123981e..38654b5 100644
--- a/Rannah/app/src/main/java/com/bal/reminders/ui/theme/Theme.kt
+++ b/Rannah/app/src/main/java/com/bal/reminders/ui/theme/Theme.kt
@@ -116,10 +116,10 @@ val DarkColors = darkColorScheme(
// ---------------------------------------------------------------- الشعار
// لا لوحة ثانية للشعار. كان يرتدي حِبريًّا وأحمرَ لا يظهران في أي شاشة، فبدا
// كأنه علامة منتج آخر ملصقة على هذا. الآن يرتدي ألوان التطبيق نفسها: البحري
-// أرضيةً للأيقونة، والكريمي جرسًا — واللونان مستعملان أصلًا في الواجهة.
+// أرضيةً للأيقونة، والكريمي جرسًا، واللونان مستعملان أصلًا في الواجهة.
// The launcher's ground is @color/brand_teal and the bell on it is
-// @color/brand_cream — the same teal the app uses for its primary action. They
+// @color/brand_cream: the same teal the app uses for its primary action. They
// live in colors.xml because only XML resources need them; nothing in Compose
// draws the mark in anything but a theme colour.
@@ -143,17 +143,17 @@ object Space {
// ---------------------------------------------------------------- الخط
// Tajawal (SIL OFL 1.1): a warm, geometric-humanist Arabic face, the strongest
-// premium-feeling choice for a modern Saudi product — cleaner and more
+// premium-feeling choice for a modern Saudi product, cleaner and more
// distinctive than a neutral corporate sans, with open counters and clear
// Arabic-Indic numerals that stay legible at large sizes for older eyes.
// Four static weights map to a restrained four-step hierarchy; the heaviest
// (ExtraBold) carries the wordmark and the date, the one place identity speaks.
val AppFont = FontFamily(
- Font(R.font.tajawal_regular, FontWeight.Normal), // 400 — reading
- Font(R.font.tajawal_medium, FontWeight.Medium), // 500 — labels
- Font(R.font.tajawal_bold, FontWeight.SemiBold), // 700 — headings, rows
- Font(R.font.tajawal_extrabold, FontWeight.Bold), // 800 — wordmark, date
+ Font(R.font.tajawal_regular, FontWeight.Normal), // 400: reading
+ Font(R.font.tajawal_medium, FontWeight.Medium), // 500: labels
+ Font(R.font.tajawal_bold, FontWeight.SemiBold), // 700: headings, rows
+ Font(R.font.tajawal_extrabold, FontWeight.Bold), // 800: wordmark, date
)
// Arabic script wants taller line-heights than Material's Latin defaults, and
@@ -163,7 +163,7 @@ val AppFont = FontFamily(
//
// Every style sets letterSpacing to zero. Material's defaults track Latin text
// apart by fractions of an em; Arabic is joined script, and tracking pulls the
-// joins apart — the single most common way an Arabic UI ends up looking subtly
+// joins apart: the single most common way an Arabic UI ends up looking subtly
// broken. Titles and body are also separated by a full step now (18/16 against
// 17/15) so hierarchy comes from size, not only from weight.
private fun face(
@@ -197,7 +197,7 @@ val BalTypography = Typography(
// Corners are calmer than before: rows and cards no longer read as large
// bubbles. Tightening the radius is part of making the app feel intentional
-// rather than decorative — not everything needs to be a big pill.
+// rather than decorative: not everything needs to be a big pill.
val BalShapes = Shapes(
extraSmall = RoundedCornerShape(8.dp),
small = RoundedCornerShape(10.dp),
diff --git a/Rannah/app/src/test/java/com/bal/reminders/data/db/Migration5To6Test.kt b/Rannah/app/src/test/java/com/bal/reminders/data/db/Migration5To6Test.kt
index 6b8ceee..6935c14 100644
--- a/Rannah/app/src/test/java/com/bal/reminders/data/db/Migration5To6Test.kt
+++ b/Rannah/app/src/test/java/com/bal/reminders/data/db/Migration5To6Test.kt
@@ -41,7 +41,7 @@ class Migration5To6Test {
reminder(2, "once", snooze = 5)
// The contradiction older builds could race into: one occurrence holding
- // both answers. The unique index allowed it — it is per status.
+ // both answers. The unique index allowed it, it is per status.
record(1, 1, OCCURRENCE_A, "completed")
record(2, 1, OCCURRENCE_A, "skipped")
// A ring that was missed and later answered: two claims about one moment.
diff --git a/Rannah/app/src/test/java/com/bal/reminders/data/db/ReminderMigrationTest.kt b/Rannah/app/src/test/java/com/bal/reminders/data/db/ReminderMigrationTest.kt
index 0aa4a6b..375e5c5 100644
--- a/Rannah/app/src/test/java/com/bal/reminders/data/db/ReminderMigrationTest.kt
+++ b/Rannah/app/src/test/java/com/bal/reminders/data/db/ReminderMigrationTest.kt
@@ -45,9 +45,9 @@ class ReminderMigrationTest {
// ── The legacy fixture ─────────────────────────────────────────────────
private fun seedRepresentativeV4Data() {
- // 1 — an ordinary active daily reminder: the control.
+ // 1: an ordinary active daily reminder: the control.
insertReminder(id = 1, title = "دواء الضغط", recurrenceType = "daily", timeMinutes = 480)
- // 2 — a recurring series stopped by the removed «إنهاء التكرار», which
+ // 2: a recurring series stopped by the removed «إنهاء التكرار», which
// recorded itself as a completion while staying enabled.
insertReminder(
id = 2,
@@ -57,7 +57,7 @@ class ReminderMigrationTest {
timeMinutes = 600,
completedAtMillis = ENDED_AT,
)
- // 3 — a reminder the user paused the other way, with enabled = 0.
+ // 3: a reminder the user paused the other way, with enabled = 0.
insertReminder(
id = 3,
title = "قراءة الورد",
@@ -65,7 +65,7 @@ class ReminderMigrationTest {
timeMinutes = 1_140,
enabled = false,
)
- // 4 — a one-time appointment that genuinely happened. Its completion is
+ // 4: a one-time appointment that genuinely happened. Its completion is
// a fact about the world, not a legacy encoding of "stopped".
insertReminder(
id = 4,
@@ -75,7 +75,7 @@ class ReminderMigrationTest {
timeMinutes = 555,
completedAtMillis = APPOINTMENT_DONE_AT,
)
- // 5 — snoozed at the moment of the update.
+ // 5: snoozed at the moment of the update.
insertReminder(
id = 5,
title = "صلاة الاستخارة",
@@ -83,7 +83,7 @@ class ReminderMigrationTest {
timeMinutes = 300,
snoozedUntilMillis = SNOOZED_UNTIL,
)
- // 6 — Hijri reminder data written before v5.
+ // 6: Hijri reminder data written before v5.
insertReminder(
id = 6,
title = "زكاة الشهر",
@@ -92,7 +92,7 @@ class ReminderMigrationTest {
dayOfMonth = 15,
timeMinutes = 720,
)
- // 7 — both legacy stop encodings at once.
+ // 7: both legacy stop encodings at once.
insertReminder(
id = 7,
title = "فاتورة الكهرباء",
@@ -102,7 +102,7 @@ class ReminderMigrationTest {
enabled = false,
completedAtMillis = BILL_ENDED_AT,
)
- // 8 — every optional field populated, in Arabic, so "content preserved"
+ // 8: every optional field populated, in Arabic, so "content preserved"
// means something stronger than "the row survived".
insertReminder(
id = 8,
@@ -126,7 +126,7 @@ class ReminderMigrationTest {
completionLabel = "هل هنّأتها؟",
snoozeMinutes = 20,
)
- // 9 — a one-time reminder that was disabled but never completed.
+ // 9: a one-time reminder that was disabled but never completed.
insertReminder(
id = 9,
title = "تسليم التقرير",
diff --git a/Rannah/app/src/test/java/com/bal/reminders/data/db/SqliteMigrationHarness.kt b/Rannah/app/src/test/java/com/bal/reminders/data/db/SqliteMigrationHarness.kt
index 53c01c6..aea9896 100644
--- a/Rannah/app/src/test/java/com/bal/reminders/data/db/SqliteMigrationHarness.kt
+++ b/Rannah/app/src/test/java/com/bal/reminders/data/db/SqliteMigrationHarness.kt
@@ -122,7 +122,7 @@ fun Connection.tableNames(): List =
*
* Column *order* is deliberately ignored. `ALTER TABLE ADD COLUMN` appends,
* while a freshly created table places the column where the entity declares it,
- * so a migrated v5 and a fresh v5 differ in order and in nothing else — which
+ * so a migrated v5 and a fresh v5 differ in order and in nothing else, which
* is exactly the distinction Room itself makes when it validates a schema.
*/
fun Connection.describeSchema(): String = buildString {
diff --git a/Rannah/app/src/test/java/com/bal/reminders/domain/OccurrenceStateResolverTest.kt b/Rannah/app/src/test/java/com/bal/reminders/domain/OccurrenceStateResolverTest.kt
index 03f4708..ee795c3 100644
--- a/Rannah/app/src/test/java/com/bal/reminders/domain/OccurrenceStateResolverTest.kt
+++ b/Rannah/app/src/test/java/com/bal/reminders/domain/OccurrenceStateResolverTest.kt
@@ -42,7 +42,7 @@ class OccurrenceStateResolverTest {
fun `a one-time reminder from an earlier day is overdue, with its real date`() {
val v = resolve(reminder(Schedule.Once(LocalDate.of(2026, 7, 10), LocalTime.of(9, 0))))
assertEquals(ReminderPhase.OVERDUE, v.phase)
- // The date it was actually due — not a bare time filed under «اليوم».
+ // The date it was actually due: not a bare time filed under «اليوم».
assertEquals(zdt(LocalDate.of(2026, 7, 10), LocalTime.of(9, 0)), v.displayAt)
}
diff --git a/Rannah/app/src/test/java/com/bal/reminders/domain/ReminderKindTest.kt b/Rannah/app/src/test/java/com/bal/reminders/domain/ReminderKindTest.kt
index a460833..af8973c 100644
--- a/Rannah/app/src/test/java/com/bal/reminders/domain/ReminderKindTest.kt
+++ b/Rannah/app/src/test/java/com/bal/reminders/domain/ReminderKindTest.kt
@@ -13,7 +13,7 @@ import org.junit.Test
/**
* The three kinds the user chooses between, read back off the schedule.
*
- * «يومي» is a preset over [Schedule.Daily], not a separate scheduling path —
+ * «يومي» is a preset over [Schedule.Daily], not a separate scheduling path,
* these tests pin that it is a *label* decision and that the recurrence engine
* underneath is the same one every other repeating reminder uses.
*/
diff --git a/Rannah/app/src/test/java/com/bal/reminders/scheduling/ReminderLifecycleTest.kt b/Rannah/app/src/test/java/com/bal/reminders/scheduling/ReminderLifecycleTest.kt
index a1f8fff..533015f 100644
--- a/Rannah/app/src/test/java/com/bal/reminders/scheduling/ReminderLifecycleTest.kt
+++ b/Rannah/app/src/test/java/com/bal/reminders/scheduling/ReminderLifecycleTest.kt
@@ -22,7 +22,7 @@ import org.junit.Assert.assertTrue
import org.junit.Test
/**
- * The three kinds a user actually chooses — «مرة واحدة», «يومي», «متكرر» — each
+ * The three kinds a user actually chooses, «مرة واحدة», «يومي», «متكرر», each
* followed all the way through the life it has: what completing does, what
* skipping does, what pausing does, what deleting does, and what is left behind.
*
diff --git a/Rannah/app/src/test/java/com/bal/reminders/scheduling/ReminderSchedulerTest.kt b/Rannah/app/src/test/java/com/bal/reminders/scheduling/ReminderSchedulerTest.kt
index e8598af..852d7cd 100644
--- a/Rannah/app/src/test/java/com/bal/reminders/scheduling/ReminderSchedulerTest.kt
+++ b/Rannah/app/src/test/java/com/bal/reminders/scheduling/ReminderSchedulerTest.kt
@@ -61,7 +61,7 @@ class ReminderSchedulerTest {
date.atTime(time).atZone(zone).toInstant()
/**
- * The occurrence a surface would act on — exactly what the resolver hands the
+ * The occurrence a surface would act on, exactly what the resolver hands the
* home and the details screens, so the tests answer the same occurrence the
* app does rather than inventing one.
*/
diff --git a/Rannah/app/src/test/java/com/bal/reminders/scheduling/SchedulerFakes.kt b/Rannah/app/src/test/java/com/bal/reminders/scheduling/SchedulerFakes.kt
index 66938a9..90622d1 100644
--- a/Rannah/app/src/test/java/com/bal/reminders/scheduling/SchedulerFakes.kt
+++ b/Rannah/app/src/test/java/com/bal/reminders/scheduling/SchedulerFakes.kt
@@ -12,8 +12,8 @@ import kotlinx.coroutines.flow.map
/**
* In-memory stand-ins for the three seams [ReminderScheduler] talks through.
- * [FakeRepository] mirrors the DAO's real contracts — including the transactional
- * "one answer per occurrence" rule — so a test that passes here is testing the
+ * [FakeRepository] mirrors the DAO's real contracts, including the transactional
+ * "one answer per occurrence" rule, so a test that passes here is testing the
* behaviour the database actually enforces, not a looser version of it.
*/
internal class FakeRepository : ReminderRepository {
diff --git a/Rannah/app/src/test/java/com/bal/reminders/scheduling/SnoozeModelTest.kt b/Rannah/app/src/test/java/com/bal/reminders/scheduling/SnoozeModelTest.kt
index 768096b..a380b38 100644
--- a/Rannah/app/src/test/java/com/bal/reminders/scheduling/SnoozeModelTest.kt
+++ b/Rannah/app/src/test/java/com/bal/reminders/scheduling/SnoozeModelTest.kt
@@ -116,7 +116,7 @@ class SnoozeModelTest {
@Test
fun `the cap is the next natural occurrence, not just twelve hours`() = runTest {
// Daily 09:00. Answering at 22:00, the next ring is 11 hours away, so the
- // 12-hour ceiling is not what binds — the reminder's own next occurrence is.
+ // 12-hour ceiling is not what binds, the reminder's own next occurrence is.
val (id, _) = dailyAt(LocalTime.of(9, 0))
val todayNine = zdt(LocalDate.of(2026, 7, 15), LocalTime.of(9, 0))
repository.setNextTrigger(id, todayNine)
diff --git a/Rannah/app/src/test/java/com/bal/reminders/ui/home/ChecklistGroupingTest.kt b/Rannah/app/src/test/java/com/bal/reminders/ui/home/ChecklistGroupingTest.kt
index 3518b52..08ed818 100644
--- a/Rannah/app/src/test/java/com/bal/reminders/ui/home/ChecklistGroupingTest.kt
+++ b/Rannah/app/src/test/java/com/bal/reminders/ui/home/ChecklistGroupingTest.kt
@@ -71,7 +71,7 @@ class ChecklistGroupingTest {
@Test
fun `a reminder completed today is still listed when another occurrence rings today`() {
// The defect this pins: complete the 09:00 daily, then edit it to 21:00.
- // The 21:00 occurrence is unresolved and its alarm is armed — the list
+ // The 21:00 occurrence is unresolved and its alarm is armed, the list
// used to hide it and show only «مكتمل», so the app rang for something
// it was telling the user was finished.
val edited = reminder(1, Schedule.Daily(LocalTime.of(21, 0)))
diff --git a/Rannah/app/src/test/java/com/bal/reminders/ui/permissions/ReadinessTest.kt b/Rannah/app/src/test/java/com/bal/reminders/ui/permissions/ReadinessTest.kt
index ad9b320..6e98783 100644
--- a/Rannah/app/src/test/java/com/bal/reminders/ui/permissions/ReadinessTest.kt
+++ b/Rannah/app/src/test/java/com/bal/reminders/ui/permissions/ReadinessTest.kt
@@ -6,7 +6,7 @@ import org.junit.Assert.assertTrue
import org.junit.Test
/**
- * Readiness reports only what is actionable, worst first — and every blocking
+ * Readiness reports only what is actionable, worst first, and every blocking
* issue it reports must have somewhere to go.
*
* `ALARM_CHANNEL` used to fail that second rule: the summary named it in red,
diff --git a/Rannah/docs/ARCHITECTURE.md b/Rannah/docs/ARCHITECTURE.md
new file mode 100644
index 0000000..7bf9fb8
--- /dev/null
+++ b/Rannah/docs/ARCHITECTURE.md
@@ -0,0 +1,75 @@
+# Architecture
+
+Developer notes for رَنّة. The user facing description lives in the [README](../../README.md); this file covers how the app is put together and why.
+
+## Shape
+
+One Gradle module, `:app`, package `com.bal.reminders`. Kotlin with Jetpack Compose for the UI, Room for storage, Hilt for injection, and AlarmManager for delivery. Minimum SDK 26, target and compile SDK 35.
+
+```
+app/src/main/java/com/bal/reminders/
+├── ui/ Compose screens and ViewModels, one package per screen
+├── domain/ models, recurrence, occurrence state, repository interface
+├── data/ Room database, DAO, repository implementation, settings
+├── scheduling/ the lifecycle owner, alarm gateway, receivers, notifications
+├── parser/ Arabic text to a reminder schedule
+├── format/ date and number formatting, Gregorian and Hijri
+├── widget/ home screen widget
+└── di/ Hilt module
+```
+
+## The rule that holds it together
+
+**The database is the source of truth, and every alarm is derivable from it.**
+
+Nothing about a reminder lives only in AlarmManager. `ReminderScheduler` writes the row first, then asks `AlarmGateway` to set the alarm. If the process dies, the device reboots, the clock jumps, or the app is reinstalled, the alarms can be rebuilt from the tables without losing anything. That is what `SystemEventsReceiver` does on `BOOT_COMPLETED`, `MY_PACKAGE_REPLACED`, `TIME_CHANGED` and `TIMEZONE_CHANGED`, and what the periodic `ReconcileWorker` does as a safety net.
+
+## Layers
+
+| Layer | Responsibility | Depends on |
+| --- | --- | --- |
+| `ui` | Compose screens, ViewModels holding screen state | domain, scheduling |
+| `scheduling` | the reminder lifecycle: fire, snooze, complete, skip, reschedule | domain, data |
+| `domain` | models, recurrence maths, occurrence state, repository interface | nothing Android specific |
+| `data` | Room entities and DAO, repository implementation, DataStore settings | domain |
+
+`domain` holds the interface, `data` holds the implementation, and Hilt binds one to the other in `di/AppModule.kt`. The recurrence and occurrence logic is plain Kotlin with an injected `Clock`, which is what makes it testable on the JVM without an emulator.
+
+## Scheduling
+
+`ReminderScheduler` is the single owner of the lifecycle. Every action goes through it, and every action is idempotent, so a replayed broadcast or a double tap cannot produce a second outcome for the same occurrence.
+
+- **تأجيل (snooze)** postpones the current occurrence and can repeat; the occurrence keeps its identity across postponements.
+- **تم (complete)** records the occurrence as done and never touches the series.
+- **إيقاف مؤقت / استئناف (pause and resume)** and **حذف (delete)** act on the series and are explicit and separate. Delete returns a snapshot that restore can put back.
+
+`complete` and `skipOccurrence` both write through the repository's transactional terminal write, so one occurrence can never end up both completed and skipped.
+
+`AlarmGateway` is an interface with one Android implementation. Tests substitute a fake and assert on what would have been scheduled, without touching the platform.
+
+Delivery has two modes. A normal reminder posts a notification. An alarm mode reminder («منبّه مهم») shows a full screen activity over the lock screen and runs `AlarmRingerService` as a foreground service of type `systemExempted`, which is the type Android reserves for a continuing exact alarm.
+
+## Storage
+
+Room, database version 6, two tables:
+
+- `reminders`, the series with its schedule
+- `completions`, the per occurrence outcome log
+
+Schemas are exported to `app/schemas/` and the migration tests build the legacy databases from those exported files, so changing a schema re-runs them. Settings that are not reminders (theme, default snooze, onboarding state) live in a DataStore preferences file rather than in Room.
+
+## Arabic parsing
+
+`ArabicReminderParser` is rule based, not a model. It normalises the text, consumes every date and time expression it recognises, and what is left becomes the title. It handles one time, daily, weekly by weekday, monthly by day number, and relative offsets. Times without an AM or PM marker use a daytime heuristic. Schedules are Gregorian; Hijri dates are a display layer on top, with a user adjustment offset.
+
+## Testing
+
+JVM unit tests under `app/src/test`, run with `./gradlew testDebugUnitTest`. They cover recurrence, occurrence state, the scheduler lifecycle against a fake gateway, snooze limits, Room migrations, Hijri formatting, Arabic parsing, and checklist grouping. There are no instrumented tests in the repository; anything that needs a real device, such as OEM battery behaviour or alarm audio, is verified by hand.
+
+## Privacy by construction
+
+The app declares no `INTERNET` permission and has no network dependency, so reminder data cannot leave the device through the app. It can still leave through Android's own backup, because `allowBackup` is true, which the [privacy statement](PRIVACY.md) says plainly.
+
+## Release builds
+
+Signing credentials are never in the repository. `app/build.gradle.kts` reads them from a properties file outside the tree, by default `~/.keystores/rannah/keystore.properties`, overridable with `-Prannah.keystoreProperties` or the `RANNAH_KEYSTORE_PROPERTIES` environment variable. When that file is absent the release variant still assembles, unsigned, so a machine without the private key can verify compilation, shrinking and lint.
diff --git a/Rannah/docs/assets/header-dark.svg b/Rannah/docs/assets/header-dark.svg
new file mode 100644
index 0000000..08028b2
--- /dev/null
+++ b/Rannah/docs/assets/header-dark.svg
@@ -0,0 +1,34 @@
+
diff --git a/Rannah/docs/assets/header-light.svg b/Rannah/docs/assets/header-light.svg
new file mode 100644
index 0000000..b3d59f7
--- /dev/null
+++ b/Rannah/docs/assets/header-light.svg
@@ -0,0 +1,34 @@
+