Merge pull request 'Codeberg sources, a Play button, and current project descriptions' (#8) from codeberg-sources-play-store into main
Reviewed-on: #8
This commit was merged in pull request #8.
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