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:
+1
-1
@@ -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' },
|
||||
];
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user