3 Commits
Author SHA1 Message Date
Jean-Luc Makiola c0e0d6269d Merge pull request 'Move to floret-kit 0.7.0' (#9) from chore/floret-kit-0.7.0 into main
Release — F-Droid repo + Gitea/Codeberg release + Play / detect (push) Successful in 6s
Release — F-Droid repo + Gitea/Codeberg release + Play / release (push) Successful in 19m22s
Release — F-Droid repo + Gitea/Codeberg release + Play / play (push) Successful in 7s
Reviewed-on: https://codeberg.org/jlmakiola/clockula/pulls/9
2026-10-07 10:17:01 +02:00
makiolaj 4290892cb2 chore: move to floret-kit 0.7.0 and its toolchain
floret-kit main now needs Gradle >= 9.6, so bump to Gradle 9.8.0,
AGP 9.4.1, Kotlin 2.4.20, and Hilt 2.60.1 / KSP 2.3.11 to read
Kotlin 2.4 metadata.
2026-10-07 09:54:01 +02:00
makiolaj 24098e8845 docs(readme): restructure along Calendula and Agendula's READMEs 2026-10-06 22:53:28 +02:00
7 changed files with 214 additions and 88 deletions

No files matched your search

+149 -40
View File
@@ -1,66 +1,132 @@
<div align="center"> <div align="center">
<img src="fastlane/metadata/android/en-US/images/icon.png" width="112" alt="Clockula icon">
<h1>Clockula</h1> <h1>Clockula</h1>
<p><strong>A modern Material 3 Expressive clock app for Android.</strong><br> <p><strong>A Material 3 Expressive clock app for Android.</strong><br>
Alarms, timers, stopwatch and world clock. Open standards, no account required.</p> Alarms, timers, stopwatch and world clock. No account and no network access.</p>
<p>
<a href="https://codeberg.org/jlmakiola/clockula/actions"><img src="https://codeberg.org/jlmakiola/clockula/actions/workflows/ci.yaml/badge.svg?branch=main" alt="CI"></a> <a href="https://codeberg.org/jlmakiola/clockula/actions"><img src="https://codeberg.org/jlmakiola/clockula/actions/workflows/ci.yaml/badge.svg?branch=main" alt="CI"></a>
<img src="https://img.shields.io/badge/Android-10%2B-3DDC84?logo=android&logoColor=white" alt="Android 10+"> <img src="https://img.shields.io/badge/Android-10%2B-3DDC84?logo=android&logoColor=white" alt="Android 10+">
<img src="https://img.shields.io/badge/Kotlin-Compose-7F52FF?logo=kotlin&logoColor=white" alt="Kotlin + Compose"> <img src="https://img.shields.io/badge/Kotlin-Compose-7F52FF?logo=kotlin&logoColor=white" alt="Kotlin + Compose">
<img src="https://img.shields.io/badge/Material%203-Expressive-4285F4" alt="Material 3 Expressive"> <img src="https://img.shields.io/badge/Material%203-Expressive-4285F4" alt="Material 3 Expressive">
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-green" alt="MIT License"></a> <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-green" alt="MIT License"></a>
</p>
<p>
<a href="https://apps.obtainium.imranr.dev/redirect?r=obtainium://add/https://codeberg.org/jlmakiola/clockula"><img src="https://github.com/ImranR98/Obtainium/blob/main/assets/graphics/badge_obtainium.png?raw=true" alt="Get it on Obtainium" height="56"></a>
&nbsp;
<a href="https://ko-fi.com/jeanlucmakiola"><img src="https://storage.ko-fi.com/cdn/brandasset/v2/support_me_on_kofi_badge_beige.png" alt="Support me on Ko-fi" height="56"></a>
</p>
</div> </div>
Clockula is the clock sibling to [Calendula](https://codeberg.org/jlmakiola/calendula) Clockula is the clock sibling to [Calendula](https://codeberg.org/jlmakiola/calendula)
(calendar) and [Agendula](https://codeberg.org/jlmakiola/agendula) (tasks). The (calendar) and [Agendula](https://codeberg.org/jlmakiola/agendula) (tasks). The
name follows theirs: *clock*, given Calendula's `-ula` ending. A Calendula flower name follows theirs: *clock* with Calendula's `-ula` ending. A Calendula flower
head is botanically a cluster of many small *florets*, so the three apps are head is botanically a cluster of many small *florets*, so the three apps are
florets of one bloom — and they share one, [floret-kit](https://codeberg.org/jlmakiola/floret-kit). florets of one bloom. They share one library,
[floret-kit](https://codeberg.org/jlmakiola/floret-kit).
> 🚧 **Early development.** The spec is locked; the app is being built. See 1.0.0 is in its last betas before the stable release.
> [`docs/ROADMAP.md`](docs/ROADMAP.md) for where it actually is.
## The honest difference from its siblings ## Features
Calendula is a pure front-end over Android's `CalendarContract`. Agendula speaks ### Alarms
the dmfs `TaskContract`. Both put a good interface on a standard someone else
already maintains.
**There is no open provider behind a clock.** Alarms, timers and laps are - Repeat schedules, snooze, your own ringtones and per-alarm settings
Clockula's own data, so Clockula is the first app in the family to own its - Scheduled as real alarm clocks: exempt from Doze, and shown on the lock
storage — by design, not as a fallback. What that commitment is replaced with is screen and in the status bar
the first section of [`docs/PLAN.md`](docs/PLAN.md), and it comes down to three - Re-resolved against your current time zone whenever it changes, so DST and
things: travel don't shift them
- A self-check screen that reads your device's actual settings. If one of them
would stop an alarm ringing, Clockula names it and opens that settings page,
instead of failing quietly at 7am.
- **Clockula speaks Android's standard alarm contract.** It implements ### Timers, stopwatch and world clock
`android.provider.AlarmClock` in full, so your assistant and your automation
apps can set alarms and timers in it exactly as they would in any stock clock,
and the system status bar and lock screen show its next alarm.
- **World clocks are IANA time zones**, not a bespoke city list that goes stale.
- **Your data exports** to a documented, versioned JSON file whenever you want
it, because data you can't take with you isn't really yours.
## Alarms that actually ring - Several timers at once, controllable from the notification shade
- A stopwatch with laps
- World clocks from the IANA time-zone database built into Android, so the
city list doesn't go stale
- A running timer or stopwatch stays reachable from every tab
An alarm app is only worth having if you can trust it, so Clockula treats that as ### Widgets
the feature rather than the plumbing. Alarms are scheduled as real alarm clocks —
doze-exempt, visible to the system — and re-resolved against your current time
zone whenever anything moves underneath them, so DST and travel don't silently
shift them.
When something on your device *would* stop an alarm ringing, Clockula tells you - Three home-screen widgets: next alarm, timer and clock
which setting it is and takes you there, instead of failing quietly at 7am. - Each widget picks its own theme, or takes its colours from your wallpaper
## Documentation ### Works with other apps
Start with [`docs/`](docs/): [`PLAN.md`](docs/PLAN.md) is the spec and the - Clockula implements Android's standard `AlarmClock` intents in full. Your
reasoning, [`ROADMAP.md`](docs/ROADMAP.md) is the status, voice assistant and automation apps can set alarms and timers in it the same
[`ARCHITECTURE.md`](docs/ARCHITECTURE.md) is how the code is put together, and way they would in the stock clock.
[`RELEASING.md`](docs/RELEASING.md) is the release runbook. - Alarms, timers, world clocks and settings export to a JSON file whose format
is documented in [`docs/BACKUP.md`](docs/BACKUP.md), and import back from it.
## Building ### Design and privacy
- Material 3 Expressive throughout, with dynamic colour on Android 12+, light
and dark themes, and expressive motion and shapes
- A per-app language setting. Clockula ships in English for now;
[help translate](#translations).
- No telemetry, no analytics and no internet permission
## Where your data lives
Calendula works on top of Android's `CalendarContract`, and Agendula can work on
top of the dmfs `TaskContract`. Both put an interface on a standard someone else
maintains. Clocks have no such provider, so Clockula keeps alarms, timers and
laps in its own database, the first app in the family to do so. The first
section of [`docs/PLAN.md`](docs/PLAN.md) explains how it makes up for the
missing standard: the `AlarmClock` intents, IANA time zones and the documented
backup format listed above.
## Install
| Channel | Available | Notes |
| --- | --- | --- |
| [Codeberg release / Obtainium](#codeberg-release--obtainium) | Now, as betas | Plain APK download, or automatic with Obtainium |
| [Self-hosted F-Droid repo](#self-hosted-f-droid-repo) | From 1.0.0 | Add the repo once |
| [Build from source](#build-from-source) | Now | Full control |
### Codeberg release / Obtainium
Every release is on
[Codeberg](https://codeberg.org/jlmakiola/clockula/releases) with the signed
APK and a `.sha256` checksum. Betas are published as pre-releases. Download the
APK and install it directly.
For automatic updates, use [Obtainium](https://github.com/ImranR98/Obtainium).
On your phone,
[add Clockula in one tap](https://apps.obtainium.imranr.dev/redirect?r=obtainium://add/https://codeberg.org/jlmakiola/clockula),
or add it by hand: *Add App*, paste `https://codeberg.org/jlmakiola/clockula`,
then *Add*. While Clockula is in beta, switch on *Include prereleases* for it,
or Obtainium won't find anything. A beta updates in place to later betas and to
the stable release.
### Self-hosted F-Droid repo
From the stable 1.0.0 on, every release is also published to the self-hosted
F-Droid repository that serves Calendula and Agendula. If you already have it,
Clockula will show up there. Otherwise add it once:
1. In your F-Droid client, open *Settings → Repositories → Add* (or open the
link below on your phone):
```
https://apps.dev.jeanlucmakiola.de/dev/fdroid/repo?fingerprint=C2C0640402BF458FC0ED957AF0B37AA4C14022E72F89CE90B5965B458CF73425
```
<sub>Repo: `https://apps.dev.jeanlucmakiola.de/dev/fdroid/repo` ·
fingerprint (SHA-256):
`C2C0 6404 02BF 458F C0ED 957A F0B3 7AA4 C140 22E7 2F89 CE90 B596 5B45 8CF7 3425`</sub>
2. Refresh, search for Clockula and install it.
### Build from source
```sh ```sh
git clone --recurse-submodules https://codeberg.org/jlmakiola/clockula.git git clone --recurse-submodules https://codeberg.org/jlmakiola/clockula.git
@@ -69,10 +135,53 @@ cd clockula
``` ```
floret-kit is a submodule built from source as a Gradle composite build, so floret-kit is a submodule built from source as a Gradle composite build, so
`--recurse-submodules` is not optional. The included build needs to find the `--recurse-submodules` is required. The included build has to find the Android
Android SDK on its own: either export `ANDROID_HOME`, or drop a `sdk.dir=…` line SDK on its own: either export `ANDROID_HOME`, or put a `sdk.dir=…` line in a
in a (gitignored) `floret-kit/local.properties`. (gitignored) `floret-kit/local.properties`. [CONTRIBUTING.md](CONTRIBUTING.md)
covers tests and lint.
<sub>The F-Droid repo and the Codeberg releases share the same signing key, so
you can switch between them without reinstalling.</sub>
## Documentation
- [Contributing](CONTRIBUTING.md): the workflow, the build and the
architectural rule changes are reviewed against
- [Plan](docs/PLAN.md): the spec and the reasoning behind it
- [Roadmap](docs/ROADMAP.md): what has shipped and what's next
- [Architecture](docs/ARCHITECTURE.md): how the code is put together
- [Backup format](docs/BACKUP.md): the JSON export schema
- [Releasing](docs/RELEASING.md): the release runbook
- [Privacy policy](docs/PRIVACY.md): the source of the page at
[jeanlucmakiola.de/clockula/privacy](https://jeanlucmakiola.de/clockula/privacy)
## Contributing
Bug reports, ideas and patches are welcome on
[Codeberg](https://codeberg.org/jlmakiola/clockula/issues). Translations go
through [Weblate](#translations), not pull requests. Read
[CONTRIBUTING.md](CONTRIBUTING.md) before writing code.
## Translations
Clockula is written in English. Translations are managed on a self-hosted
Weblate, and partial ones are fine: untranslated strings fall back to English.
[Help translate Clockula](https://weblate.dev.jeanlucmakiola.de/engage/clockula/)
You don't need to code. Register on the Weblate server, pick a language (or
request one) and translate in your browser.
## Privacy
Clockula has no analytics, tracking, ads, crash-reporting SDK or third-party
services. It doesn't have the internet permission, so it can't send anything
anywhere. Your alarms, timers and world clocks stay in the app's private
storage on your device.
[Privacy policy](https://jeanlucmakiola.de/clockula/privacy), written in
[`docs/PRIVACY.md`](docs/PRIVACY.md), which is what that page renders.
## Licence ## Licence
MIT — see [`LICENSE`](LICENSE). [MIT](LICENSE), Jean-Luc Makiola, 2026
+4 -4
View File
@@ -1,8 +1,8 @@
[versions] [versions]
agp = "9.2.1" agp = "9.4.1"
kotlin = "2.3.21" kotlin = "2.4.20"
ksp = "2.3.9" ksp = "2.3.11"
hilt = "2.59.2" hilt = "2.60.1"
coreKtx = "1.19.0" coreKtx = "1.19.0"
appcompat = "1.7.1" appcompat = "1.7.1"
lifecycleRuntime = "2.10.0" lifecycleRuntime = "2.10.0"
Binary file not shown.
+4 -2
View File
@@ -1,8 +1,10 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip distributionSha256Sum=bafd5ce9cfaea0fbccfdc8439a1ac42fbd4cd9c89dc9a988228d8a2639a58e6c
distributionSha256Sum=bafc141b619ad6350fd975fc903156dd5c151998cc8b058e8c1044ab5f7b031f distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.0-bin.zip
networkTimeout=10000 networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
Vendored
+4 -7
View File
@@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
# #
# Copyright © 2015-2021 the original authors. # Copyright © 2015 the original authors.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -20,7 +20,7 @@
############################################################################## ##############################################################################
# #
# Gradle start up script for POSIX generated by Gradle. # gradlew start up script for POSIX generated by Gradle.
# #
# Important for running: # Important for running:
# #
@@ -29,7 +29,7 @@
# bash, then to run this script, type that shell name before the whole # bash, then to run this script, type that shell name before the whole
# command line, like: # command line, like:
# #
# ksh Gradle # ksh gradlew
# #
# Busybox and similar reduced shells will NOT work, because this script # Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features: # requires all of these POSIX shell features:
@@ -57,7 +57,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.
@@ -114,7 +114,6 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;; NONSTOP* ) nonstop=true ;;
esac esac
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM. # Determine the Java command to use to start the JVM.
@@ -172,7 +171,6 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java # For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" ) JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -212,7 +210,6 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@" "$@"
Vendored
+52 -34
View File
@@ -19,12 +19,39 @@
@if "%DEBUG%"=="" @echo off @if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################
@rem @rem
@rem Gradle startup script for Windows @rem gradlew startup script for Windows
@rem @rem
@rem ########################################################################## @rem ##########################################################################
@rem Set local scope for the variables with windows NT shell @rem Set local scope for the variables, and ensure extensions are enabled
if "%OS%"=="Windows_NT" setlocal setlocal EnableExtensions
@rem Catch executions from older scripts and ensure they exit cleanly.
@rem This can be removed once we can be reasonably confident that few people
@rem will be migrating directly to this new wrapper.
goto afterSafetyNet
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
goto exitWithErrorLevel
:afterSafetyNet
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=. if "%DIRNAME%"=="" set DIRNAME=.
@@ -45,13 +72,14 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2 1>&2 echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo. 1>&2 1>&2 echo.
echo Please set the JAVA_HOME variable in your environment to match the 1>&2 1>&2 echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation. 1>&2 1>&2 echo location of your Java installation.
goto fail "%COMSPEC%" /c exit 1
goto exitWithErrorLevel
:findJavaFromJavaHome :findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=% set JAVA_HOME=%JAVA_HOME:"=%
@@ -59,36 +87,26 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. 1>&2 1>&2 echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo. 1>&2 1>&2 echo.
echo Please set the JAVA_HOME variable in your environment to match the 1>&2 1>&2 echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation. 1>&2 1>&2 echo location of your Java installation.
goto fail "%COMSPEC%" /c exit 1
goto exitWithErrorLevel
:execute :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=
@rem Execute Gradle @rem Execute gradlew
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* @rem endlocal doesn't take effect until after the line is parsed and variables are expanded
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel & goto exitWithErrorLevel
:end @rem This label must not be changed. We rely on old scripts being able to jump to this point.
@rem End local scope for the variables with windows NT shell :exitWithErrorLevel
if %ERRORLEVEL% equ 0 goto mainEnd @rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
"%COMSPEC%" /c exit %ERRORLEVEL%
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega