Point sources at Codeberg, add the Play button, refresh descriptions

Codeberg is where the apps live now, so the source link, its icon and the
release lookup follow. Forgejo serves Gitea's /api/v1, so the version badge
keeps working — with a fallback to the newest pre-release, since a pre-1.0
app publishes nothing else (Agendula showed a stale v0.2.0 before).

Calendula is on Google Play as well as F-Droid; adds a tonal button for it,
and its fifteen community translations replace "German and English".

Agendula's entry described the app it stopped being: it now carries its own
VTODO store, with an external provider as a choice rather than a requirement,
plus export and its own CalDAV sync in progress. floret-kit's modules have
grown past core-time. Footer forge link and the /uses infrastructure list
separate the two roles: Codeberg for source, self-hosted Gitea for builds.
This commit is contained in:
2026-09-09 16:09:30 +02:00
parent 6311baccb7
commit ff3c0fa437
8 changed files with 122 additions and 52 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ export const LEGAL = {
// Social / external links shown in the footer. `icon` is an Iconify name.
export const SOCIALS: { label: string; href: string; icon: string }[] = [
{ label: 'Gitea', href: 'https://gitea.jeanlucmakiola.de/makiolaj', icon: 'simple-icons:gitea' },
{ label: 'Codeberg', href: 'https://codeberg.org/jlmakiola', icon: 'simple-icons:codeberg' },
{ label: 'Ko-fi', href: 'https://ko-fi.com/jeanlucmakiola', icon: 'simple-icons:kofi' },
{ label: 'Email', href: `mailto:${LEGAL.email}`, icon: 'mdi:email-outline' },
];
+3 -1
View File
@@ -39,8 +39,10 @@ const projects = defineCollection({
.default([]),
links: z
.object({
gitea: z.string().url().optional(),
// Canonical source is Codeberg; the self-hosted Gitea is build infra.
codeberg: z.string().url().optional(),
fdroid: z.string().url().optional(),
play: z.string().url().optional(),
donate: z.string().url().optional(),
translate: z.string().url().optional(),
})
+33 -19
View File
@@ -6,37 +6,51 @@ released: false
platform: Android 10+
license: MIT
summary: >-
The task-list sibling to Calendula — a pure front-end over the OpenTasks
provider (CalDAV VTODOs), with no reinvented sync stack.
The task-list sibling to Calendula. Keeps your tasks in its own store built
around iCalendar VTODOs — or on top of a tasks provider you already sync —
with no account required.
tech:
- Kotlin
- Jetpack Compose
- Material 3 Expressive
- Room
features:
- title: Provider-native
- title: Two places to keep tasks
body: >-
A pure front-end over the OpenTasks TaskContract provider. DAVx5,
SmoothSync, and DecSync sync your CalDAV VTODOs in; Agendula reads and
writes them — no own database.
- title: Data layer done
By default, Agendula's own database, designed around RFC 5545's VTODO —
no account, no permission, no other app needed. Or a provider you already
have (OpenTasks, tasks.org), left to whatever syncs it for you.
- title: Coexists, never replaces
body: >-
Provider resolution, live-updating reads, writes, smart-list filtering,
and a self-scheduled reminder engine are built and unit-tested.
- title: UI in progress
Agendula's store is an ordinary app database, published to nothing, so
installing it never breaks OpenTasks — and if you already sync through a
provider, that keeps working exactly as it did.
- title: Nothing quietly lost
body: >-
The Material 3 Expressive screens are being built on top of the tested
data layer, one at a time.
Recurring tasks are expanded per RFC 5545, and anything the schema doesn't
model is round-tripped verbatim rather than dropped. Export to standard
.ics files is built in, because data you can't take with you isn't yours.
- title: Open standards only
body: >-
CalDAV, iCalendar, and DecSync are the lane. Proprietary task services are
out of scope by design — they would mean owning a sync stack.
CalDAV, iCalendar, and DecSync are the lane; a CalDAV sync engine of
Agendula's own is being built now. Google Tasks and Microsoft To Do are
out of scope by design.
links:
gitea: https://gitea.jeanlucmakiola.de/makiolaj/agendula
codeberg: https://codeberg.org/jlmakiola/agendula
donate: https://ko-fi.com/jeanlucmakiola
translate: https://weblate.dev.jeanlucmakiola.de/engage/agendula/
---
Where Calendula is a pure front-end over Android's `CalendarContract`, Agendula
is a pure front-end over the **OpenTasks `TaskContract` provider**. The name
rhymes with its sibling on purpose: *Agendula* is *agenda* — Latin for "things
to be done" — given Calendula's `-ula` ending. A Calendula flower head is a
cluster of many small *florets*, so the two apps are florets of one bloom.
carries **its own store** — a database designed against `VTODO`, the same shape
DAVx5 (and SmoothSync, DecSync, …) syncs out of a CalDAV server. Using a tasks
provider you already have is a choice rather than a requirement. The name rhymes
with its sibling on purpose: *Agendula* is *agenda* — Latin for "things to be
done" — given Calendula's `-ula` ending. A Calendula flower head is a cluster of
many small *florets*, so the two apps are florets of one bloom.
**Where it stands:** the store, reminders, export, and the Material 3 Expressive
screens through task detail, editing and settings are built; German and
Brazilian Portuguese are the first community translations. Still ahead: a Glance
widget, the first F-Droid release, and Agendula's own CalDAV sync. Its
[privacy policy](/agendula/privacy) is already published.
+9 -2
View File
@@ -32,10 +32,12 @@ features:
- title: Private by default
body: >-
Zero telemetry, zero analytics, no internet permission — your data never
leaves the device. Dynamic color on Android 12+, German and English UI.
leaves the device. Dynamic color on Android 12+, and fifteen community
translations alongside English.
links:
gitea: https://gitea.jeanlucmakiola.de/makiolaj/calendula
codeberg: https://codeberg.org/jlmakiola/calendula
fdroid: https://f-droid.org/packages/de.jeanlucmakiola.calendula/
play: https://play.google.com/store/apps/details?id=de.jeanlucmakiola.calendula
donate: https://ko-fi.com/jeanlucmakiola
translate: https://weblate.dev.jeanlucmakiola.de/engage/calendula/
---
@@ -46,3 +48,8 @@ on top of Android's `CalendarContract`: any calendar synced to your device
(CalDAV via DAVx5, Google, local, WebCal subscriptions, …) simply appears, and
everything you create or edit syncs back the same way. **No own database, no
sync stack reinvented.**
Install it from F-Droid or Google Play — same app, same MIT source on Codeberg,
and still no internet permission in either build. There is also a
[privacy policy](/calendula/privacy) covering what the app does and doesn't do
with your data.
+16 -7
View File
@@ -10,22 +10,31 @@ summary: >-
app family — so each app draws from one bloom instead of reinventing it.
tech:
- Kotlin
- Jetpack Compose
- Gradle composite build
features:
- title: Built from source
body: >-
Embedded as a git submodule and wired in with a Gradle composite build —
no published artifacts, which keeps every app reproducible for F-Droid.
- title: core-time
- title: Identity and components
body: >-
Pure-Kotlin date/time helpers: local-day windows for smart-list logic and
locale/zone-aware display formatting. No Android, no dependencies.
- title: Growing
The M3 Expressive theme factory, navigation motion and predictive-back
peek, plus the shared Compose vocabulary each screen is written out of —
grouped rows, inline fields, option cards, full-screen pickers,
collapsing scaffolds.
- title: Plumbing, not looks
body: >-
Identity/theme, components, screen recipes, and provider / prefs /
reminders / crash plumbing land as they're extracted from the apps.
core-time (local-day windows, locale-aware formatting), core-reminders
(lead-time model and codec), core-locale (per-app language), core-crash
(on-device capture and issue hand-off) — each app layers its own storage
and strings on top.
- title: Mechanics shared, look per-app
body: >-
The principle the kit is held to: a component moves here once two apps
need the same behaviour, while palette and personality stay with the app.
links:
gitea: https://gitea.jeanlucmakiola.de/makiolaj/floret-kit
codeberg: https://codeberg.org/jlmakiola/floret-kit
donate: https://ko-fi.com/jeanlucmakiola
---
+17 -6
View File
@@ -46,23 +46,34 @@ import { LEGAL } from '../consts';
<ul>
<li><strong>Kotlin</strong> and <strong>Jetpack Compose</strong>, designed in <strong>Material 3 Expressive</strong>.</li>
<li>
No reinvented sync stack — each app is a front-end over a platform
provider (<code>CalendarContract</code>, the OpenTasks{' '}
<code>TaskContract</code>) and open standards like CalDAV, iCalendar, and
DecSync.
Open standards over proprietary services — CalDAV, iCalendar, DecSync.
Calendula is a pure front-end over Android's <code>CalendarContract</code>;
Agendula keeps its own <code>VTODO</code> store, or rides a tasks
provider you already sync.
</li>
<li>
A shared design system, <a href="/work/floret-kit">floret-kit</a>, wired
in as a git submodule via a Gradle composite build — no published
artifacts, so every app stays reproducible.
</li>
<li>Released on <a href="https://f-droid.org" rel="noopener">F-Droid</a>, MIT-licensed, zero telemetry.</li>
<li>
Released on <a href="https://f-droid.org" rel="noopener">F-Droid</a> and
Google Play, MIT-licensed, zero telemetry.
</li>
</ul>
<h2>Self-hosted infrastructure</h2>
<p>Code, translations, builds, and this site all run on infrastructure I host myself.</p>
<ul>
<li><a href="https://gitea.jeanlucmakiola.de/makiolaj" rel="noopener">Gitea</a> for source hosting and as the home for smaller experiments.</li>
<li>
<a href="https://codeberg.org/jlmakiola" rel="noopener">Codeberg</a> is
where the apps live — source, issues, releases.
</li>
<li>
Self-hosted <a href="https://gitea.jeanlucmakiola.de/makiolaj" rel="noopener">Gitea</a>{' '}
runs the build side of that — signing, the F-Droid repo, release
pipelines — and is home to smaller experiments.
</li>
<li><a href="https://weblate.org" rel="noopener">Weblate</a> for community translations of the apps.</li>
<li><a href="https://coolify.io" rel="noopener">Coolify</a> to build and deploy this site.</li>
<li>Umami for the privacy-respecting analytics above.</li>
+38 -16
View File
@@ -5,26 +5,41 @@ import BaseLayout from '../../layouts/BaseLayout.astro';
import type { GetStaticPaths } from 'astro';
export const getStaticPaths = (async () => {
// Build-time: resolve the latest release tag from the project's Gitea repo.
// Build-time: resolve the latest release tag from the project's Codeberg
// repo. Forgejo serves Gitea's /api/v1, so the same call fits both.
// `releases/latest` skips pre-releases, and a pre-1.0 app publishes nothing
// else — so fall back to the newest release of any kind before giving up.
// Never throws — returns null on 404/offline so the build can't break.
// (Defined inside getStaticPaths: Astro extracts this fn into its own scope.)
const fetchLatestRelease = async (repoUrl?: string): Promise<string | null> => {
if (!repoUrl) return null;
const tagFrom = async (url: string): Promise<string | null> => {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), 8000);
try {
const res = await fetch(url, {
signal: ctrl.signal,
headers: { Accept: 'application/json' },
});
if (!res.ok) return null;
const data = await res.json();
const release = Array.isArray(data) ? data[0] : data;
return typeof release?.tag_name === 'string' ? release.tag_name : null;
} catch {
return null;
} finally {
clearTimeout(timer);
}
};
try {
const u = new URL(repoUrl);
const [, owner, repo] = u.pathname.split('/');
if (!owner || !repo) return null;
const api = `${u.origin}/api/v1/repos/${owner}/${repo}/releases/latest`;
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), 8000);
const res = await fetch(api, {
signal: ctrl.signal,
headers: { Accept: 'application/json' },
});
clearTimeout(timer);
if (!res.ok) return null;
const data = await res.json();
return typeof data.tag_name === 'string' ? data.tag_name : null;
const base = `${u.origin}/api/v1/repos/${owner}/${repo}/releases`;
return (
(await tagFrom(`${base}/latest`)) ??
(await tagFrom(`${base}?draft=false&limit=1`))
);
} catch {
return null;
}
@@ -34,7 +49,7 @@ export const getStaticPaths = (async () => {
return Promise.all(
projects.map(async (project) => ({
params: { slug: project.id },
props: { project, version: await fetchLatestRelease(project.data.links.gitea) },
props: { project, version: await fetchLatestRelease(project.data.links.codeberg) },
}))
);
}) satisfies GetStaticPaths;
@@ -67,9 +82,16 @@ const { Content } = await render(project);
)
}
{
links.gitea && (
<a class="btn btn--outlined" href={links.gitea} rel="noopener">
<Icon name="simple-icons:gitea" /> Source on Gitea
links.play && (
<a class="btn btn--tonal" href={links.play} rel="noopener">
<Icon name="simple-icons:googleplay" /> Get it on Google Play
</a>
)
}
{
links.codeberg && (
<a class="btn btn--outlined" href={links.codeberg} rel="noopener">
<Icon name="simple-icons:codeberg" /> Source on Codeberg
</a>
)
}
+5
View File
@@ -212,6 +212,11 @@ a:hover { text-decoration-color: currentColor; }
.btn:active { transform: scale(0.97); }
.btn--filled { background: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary); }
.btn--filled:hover { box-shadow: var(--md-sys-elevation-2); }
.btn--tonal {
background: var(--md-sys-color-secondary-container);
color: var(--md-sys-color-on-secondary-container);
}
.btn--tonal:hover { box-shadow: var(--md-sys-elevation-1); }
.btn--outlined {
background: transparent;
color: var(--md-sys-color-on-surface);