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.
|
// Social / external links shown in the footer. `icon` is an Iconify name.
|
||||||
export const SOCIALS: { label: string; href: string; icon: string }[] = [
|
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: 'Ko-fi', href: 'https://ko-fi.com/jeanlucmakiola', icon: 'simple-icons:kofi' },
|
||||||
{ label: 'Email', href: `mailto:${LEGAL.email}`, icon: 'mdi:email-outline' },
|
{ label: 'Email', href: `mailto:${LEGAL.email}`, icon: 'mdi:email-outline' },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -39,8 +39,10 @@ const projects = defineCollection({
|
|||||||
.default([]),
|
.default([]),
|
||||||
links: z
|
links: z
|
||||||
.object({
|
.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(),
|
fdroid: z.string().url().optional(),
|
||||||
|
play: z.string().url().optional(),
|
||||||
donate: z.string().url().optional(),
|
donate: z.string().url().optional(),
|
||||||
translate: z.string().url().optional(),
|
translate: z.string().url().optional(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,37 +6,51 @@ released: false
|
|||||||
platform: Android 10+
|
platform: Android 10+
|
||||||
license: MIT
|
license: MIT
|
||||||
summary: >-
|
summary: >-
|
||||||
The task-list sibling to Calendula — a pure front-end over the OpenTasks
|
The task-list sibling to Calendula. Keeps your tasks in its own store built
|
||||||
provider (CalDAV VTODOs), with no reinvented sync stack.
|
around iCalendar VTODOs — or on top of a tasks provider you already sync —
|
||||||
|
with no account required.
|
||||||
tech:
|
tech:
|
||||||
- Kotlin
|
- Kotlin
|
||||||
- Jetpack Compose
|
- Jetpack Compose
|
||||||
- Material 3 Expressive
|
- Material 3 Expressive
|
||||||
|
- Room
|
||||||
features:
|
features:
|
||||||
- title: Provider-native
|
- title: Two places to keep tasks
|
||||||
body: >-
|
body: >-
|
||||||
A pure front-end over the OpenTasks TaskContract provider. DAVx5,
|
By default, Agendula's own database, designed around RFC 5545's VTODO —
|
||||||
SmoothSync, and DecSync sync your CalDAV VTODOs in; Agendula reads and
|
no account, no permission, no other app needed. Or a provider you already
|
||||||
writes them — no own database.
|
have (OpenTasks, tasks.org), left to whatever syncs it for you.
|
||||||
- title: Data layer done
|
- title: Coexists, never replaces
|
||||||
body: >-
|
body: >-
|
||||||
Provider resolution, live-updating reads, writes, smart-list filtering,
|
Agendula's store is an ordinary app database, published to nothing, so
|
||||||
and a self-scheduled reminder engine are built and unit-tested.
|
installing it never breaks OpenTasks — and if you already sync through a
|
||||||
- title: UI in progress
|
provider, that keeps working exactly as it did.
|
||||||
|
- title: Nothing quietly lost
|
||||||
body: >-
|
body: >-
|
||||||
The Material 3 Expressive screens are being built on top of the tested
|
Recurring tasks are expanded per RFC 5545, and anything the schema doesn't
|
||||||
data layer, one at a time.
|
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
|
- title: Open standards only
|
||||||
body: >-
|
body: >-
|
||||||
CalDAV, iCalendar, and DecSync are the lane. Proprietary task services are
|
CalDAV, iCalendar, and DecSync are the lane; a CalDAV sync engine of
|
||||||
out of scope by design — they would mean owning a sync stack.
|
Agendula's own is being built now. Google Tasks and Microsoft To Do are
|
||||||
|
out of scope by design.
|
||||||
links:
|
links:
|
||||||
gitea: https://gitea.jeanlucmakiola.de/makiolaj/agendula
|
codeberg: https://codeberg.org/jlmakiola/agendula
|
||||||
donate: https://ko-fi.com/jeanlucmakiola
|
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
|
Where Calendula is a pure front-end over Android's `CalendarContract`, Agendula
|
||||||
is a pure front-end over the **OpenTasks `TaskContract` provider**. The name
|
carries **its own store** — a database designed against `VTODO`, the same shape
|
||||||
rhymes with its sibling on purpose: *Agendula* is *agenda* — Latin for "things
|
DAVx5 (and SmoothSync, DecSync, …) syncs out of a CalDAV server. Using a tasks
|
||||||
to be done" — given Calendula's `-ula` ending. A Calendula flower head is a
|
provider you already have is a choice rather than a requirement. The name rhymes
|
||||||
cluster of many small *florets*, so the two apps are florets of one bloom.
|
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
|
- title: Private by default
|
||||||
body: >-
|
body: >-
|
||||||
Zero telemetry, zero analytics, no internet permission — your data never
|
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:
|
links:
|
||||||
gitea: https://gitea.jeanlucmakiola.de/makiolaj/calendula
|
codeberg: https://codeberg.org/jlmakiola/calendula
|
||||||
fdroid: https://f-droid.org/packages/de.jeanlucmakiola.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
|
donate: https://ko-fi.com/jeanlucmakiola
|
||||||
translate: https://weblate.dev.jeanlucmakiola.de/engage/calendula/
|
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
|
(CalDAV via DAVx5, Google, local, WebCal subscriptions, …) simply appears, and
|
||||||
everything you create or edit syncs back the same way. **No own database, no
|
everything you create or edit syncs back the same way. **No own database, no
|
||||||
sync stack reinvented.**
|
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.
|
app family — so each app draws from one bloom instead of reinventing it.
|
||||||
tech:
|
tech:
|
||||||
- Kotlin
|
- Kotlin
|
||||||
|
- Jetpack Compose
|
||||||
- Gradle composite build
|
- Gradle composite build
|
||||||
features:
|
features:
|
||||||
- title: Built from source
|
- title: Built from source
|
||||||
body: >-
|
body: >-
|
||||||
Embedded as a git submodule and wired in with a Gradle composite build —
|
Embedded as a git submodule and wired in with a Gradle composite build —
|
||||||
no published artifacts, which keeps every app reproducible for F-Droid.
|
no published artifacts, which keeps every app reproducible for F-Droid.
|
||||||
- title: core-time
|
- title: Identity and components
|
||||||
body: >-
|
body: >-
|
||||||
Pure-Kotlin date/time helpers: local-day windows for smart-list logic and
|
The M3 Expressive theme factory, navigation motion and predictive-back
|
||||||
locale/zone-aware display formatting. No Android, no dependencies.
|
peek, plus the shared Compose vocabulary each screen is written out of —
|
||||||
- title: Growing
|
grouped rows, inline fields, option cards, full-screen pickers,
|
||||||
|
collapsing scaffolds.
|
||||||
|
- title: Plumbing, not looks
|
||||||
body: >-
|
body: >-
|
||||||
Identity/theme, components, screen recipes, and provider / prefs /
|
core-time (local-day windows, locale-aware formatting), core-reminders
|
||||||
reminders / crash plumbing land as they're extracted from the apps.
|
(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:
|
links:
|
||||||
gitea: https://gitea.jeanlucmakiola.de/makiolaj/floret-kit
|
codeberg: https://codeberg.org/jlmakiola/floret-kit
|
||||||
donate: https://ko-fi.com/jeanlucmakiola
|
donate: https://ko-fi.com/jeanlucmakiola
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+17
-6
@@ -46,23 +46,34 @@ import { LEGAL } from '../consts';
|
|||||||
<ul>
|
<ul>
|
||||||
<li><strong>Kotlin</strong> and <strong>Jetpack Compose</strong>, designed in <strong>Material 3 Expressive</strong>.</li>
|
<li><strong>Kotlin</strong> and <strong>Jetpack Compose</strong>, designed in <strong>Material 3 Expressive</strong>.</li>
|
||||||
<li>
|
<li>
|
||||||
No reinvented sync stack — each app is a front-end over a platform
|
Open standards over proprietary services — CalDAV, iCalendar, DecSync.
|
||||||
provider (<code>CalendarContract</code>, the OpenTasks{' '}
|
Calendula is a pure front-end over Android's <code>CalendarContract</code>;
|
||||||
<code>TaskContract</code>) and open standards like CalDAV, iCalendar, and
|
Agendula keeps its own <code>VTODO</code> store, or rides a tasks
|
||||||
DecSync.
|
provider you already sync.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
A shared design system, <a href="/work/floret-kit">floret-kit</a>, wired
|
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
|
in as a git submodule via a Gradle composite build — no published
|
||||||
artifacts, so every app stays reproducible.
|
artifacts, so every app stays reproducible.
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
|
|
||||||
<h2>Self-hosted infrastructure</h2>
|
<h2>Self-hosted infrastructure</h2>
|
||||||
<p>Code, translations, builds, and this site all run on infrastructure I host myself.</p>
|
<p>Code, translations, builds, and this site all run on infrastructure I host myself.</p>
|
||||||
<ul>
|
<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://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><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>
|
<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';
|
import type { GetStaticPaths } from 'astro';
|
||||||
|
|
||||||
export const getStaticPaths = (async () => {
|
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.
|
// Never throws — returns null on 404/offline so the build can't break.
|
||||||
// (Defined inside getStaticPaths: Astro extracts this fn into its own scope.)
|
// (Defined inside getStaticPaths: Astro extracts this fn into its own scope.)
|
||||||
const fetchLatestRelease = async (repoUrl?: string): Promise<string | null> => {
|
const fetchLatestRelease = async (repoUrl?: string): Promise<string | null> => {
|
||||||
if (!repoUrl) return 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 {
|
try {
|
||||||
const u = new URL(repoUrl);
|
const u = new URL(repoUrl);
|
||||||
const [, owner, repo] = u.pathname.split('/');
|
const [, owner, repo] = u.pathname.split('/');
|
||||||
if (!owner || !repo) return null;
|
if (!owner || !repo) return null;
|
||||||
const api = `${u.origin}/api/v1/repos/${owner}/${repo}/releases/latest`;
|
const base = `${u.origin}/api/v1/repos/${owner}/${repo}/releases`;
|
||||||
const ctrl = new AbortController();
|
return (
|
||||||
const timer = setTimeout(() => ctrl.abort(), 8000);
|
(await tagFrom(`${base}/latest`)) ??
|
||||||
const res = await fetch(api, {
|
(await tagFrom(`${base}?draft=false&limit=1`))
|
||||||
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;
|
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -34,7 +49,7 @@ export const getStaticPaths = (async () => {
|
|||||||
return Promise.all(
|
return Promise.all(
|
||||||
projects.map(async (project) => ({
|
projects.map(async (project) => ({
|
||||||
params: { slug: project.id },
|
params: { slug: project.id },
|
||||||
props: { project, version: await fetchLatestRelease(project.data.links.gitea) },
|
props: { project, version: await fetchLatestRelease(project.data.links.codeberg) },
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
}) satisfies GetStaticPaths;
|
}) satisfies GetStaticPaths;
|
||||||
@@ -67,9 +82,16 @@ const { Content } = await render(project);
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
links.gitea && (
|
links.play && (
|
||||||
<a class="btn btn--outlined" href={links.gitea} rel="noopener">
|
<a class="btn btn--tonal" href={links.play} rel="noopener">
|
||||||
<Icon name="simple-icons:gitea" /> Source on Gitea
|
<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>
|
</a>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -212,6 +212,11 @@ a:hover { text-decoration-color: currentColor; }
|
|||||||
.btn:active { transform: scale(0.97); }
|
.btn:active { transform: scale(0.97); }
|
||||||
.btn--filled { background: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary); }
|
.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--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 {
|
.btn--outlined {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--md-sys-color-on-surface);
|
color: var(--md-sys-color-on-surface);
|
||||||
|
|||||||
Reference in New Issue
Block a user