GingerCal 1.3: month view, Galician interface, Nginx Proxy Manager bridge option

This commit is contained in:
xocko committed 2026-10-15 17:42:46 +02:00
1 parent f44ecf4871
commit 42c3d6da91
39 files changed
+1409 -220

No files matched your search

+15 -22
View File
@@ -109,41 +109,34 @@ public class Dav {
continue;
}
if (r.code == 401) {
throw new DavException(401, "Login refused (401). Check the user name and use a "
+ "Nextcloud app password (Settings > Security > Devices & sessions).");
throw new DavException(401, L.t("Login refused (401). Check the user name and use a Nextcloud app password (Settings > Security > Devices & sessions)."));
}
if (r.code == 403 && body != null && method.equals("PUT")) {
throw new DavException(403, "Not allowed to write to this calendar (403). It may be read-only/shared.");
throw new DavException(403, L.t("Not allowed to write to this calendar (403). It may be read-only/shared."));
}
return r;
}
throw new DavException(DavException.REDIRECT, "Too many redirects from the server.");
throw new DavException(DavException.REDIRECT, L.t("Too many redirects from the server."));
}
static void checkRedirect(String from, String to, List<String> visited, int code) throws DavException {
String a = originKey(from), b = originKey(to);
if (a.startsWith("http:") && b.startsWith("https:")) {
throw new DavException(DavException.REDIRECT, "The server redirected http:// to "
+ origin(to) + " (Nextcloud forces HTTPS / HSTS). Android 2.3 can't follow that: "
+ "use the legacy-TLS bridge address instead (see README, 'Forced HTTPS / HSTS').");
throw new DavException(DavException.REDIRECT, L.f("The server redirected http:// to %s (Nextcloud forces HTTPS / HSTS). Android 2.3 can't follow that: use the legacy-TLS bridge address instead (see README, 'Forced HTTPS / HSTS').", origin(to)));
}
if (!a.equals(b)) {
throw new DavException(DavException.REDIRECT, "The server redirected to " + origin(to)
+ ", not the address set in the app. Nextcloud is sending its public URL; check the "
+ "bridge's proxy_redirect line (README, 'Forced HTTPS / HSTS').");
throw new DavException(DavException.REDIRECT, L.f("The server redirected to %s, not the address set in the app. Nextcloud is sending its public URL; check the bridge's proxy_redirect line (README, 'Forced HTTPS / HSTS').", origin(to)));
}
if (visited.contains(to)) {
throw new DavException(DavException.REDIRECT, "Redirect loop (HTTP " + code + " back to the same "
+ "address). Nextcloud forces HTTPS but the bridge reaches it over plain HTTP: switch the "
+ "bridge to proxy_pass https:// (README, 'Forced HTTPS / HSTS').");
throw new DavException(DavException.REDIRECT, L.f("Redirect loop (HTTP %d back to the same address). Nextcloud forces HTTPS but the bridge reaches it over plain HTTP: switch the bridge to proxy_pass https:// (README, 'Forced HTTPS / HSTS').", code));
}
}
List<Response> multistatus(String method, String target, String depth, String body) throws IOException {
Http.Resp r = send(method, target, depth, body, null);
if (r.code != 207) {
throw new DavException(r.code, method + " " + URI.create(target).getPath() + " failed: HTTP " + r.code
+ (r.code == 404 ? " (not found)" : r.code == 405 ? " (not a WebDAV URL?)" : ""));
throw new DavException(r.code, L.f("%s %s failed: HTTP %d%s", method, URI.create(target).getPath(), r.code,
r.code == 404 ? L.t(" (not found)") : r.code == 405 ? L.t(" (not a WebDAV URL?)") : ""));
}
Node ms = Xml.parse(r.body);
List<Response> out = new ArrayList<Response>();
@@ -206,7 +199,7 @@ public class Dav {
}
}
if (principal == null) {
throw last != null ? last : new DavException(0, "Server did not report a CalDAV principal");
throw last != null ? last : new DavException(0, L.t("Server did not report a CalDAV principal"));
}
List<Response> rs = multistatus("PROPFIND", principal, "0", propfind("<c:calendar-home-set/>"));
for (Response r : rs) {
@@ -326,7 +319,7 @@ public class Dav {
/** GET a single object. Returns {ics, etag}. */
public String[] get(String href) throws IOException {
Http.Resp r = send("GET", href, null, null, null);
if (r.code != 200) throw new DavException(r.code, "Could not load event: HTTP " + r.code);
if (r.code != 200) throw new DavException(r.code, L.f("Could not load event: HTTP %d", r.code));
return new String[] {r.body, r.etag};
}
@@ -334,25 +327,25 @@ public class Dav {
String href = abs(calHref.endsWith("/") ? calHref : calHref + "/", uid + ".ics");
Http.Resp r = send("PUT", href, null, ics, new String[] {"If-None-Match", "*"});
if (r.code != 201 && r.code != 204 && r.code != 200) {
throw new DavException(r.code, "Saving failed: HTTP " + r.code);
throw new DavException(r.code, L.f("Saving failed: HTTP %d", r.code));
}
}
public void update(String href, String etag, String ics) throws IOException {
String[] h = etag == null || etag.length() == 0 ? null : new String[] {"If-Match", etag};
Http.Resp r = send("PUT", href, null, ics, h);
if (r.code == 412) throw new DavException(412, "Event was changed on the server meanwhile. Sync and try again.");
if (r.code == 412) throw new DavException(412, L.t("Event was changed on the server meanwhile. Sync and try again."));
if (r.code != 201 && r.code != 204 && r.code != 200) {
throw new DavException(r.code, "Saving failed: HTTP " + r.code);
throw new DavException(r.code, L.f("Saving failed: HTTP %d", r.code));
}
}
public void delete(String href, String etag) throws IOException {
String[] h = etag == null || etag.length() == 0 ? null : new String[] {"If-Match", etag};
Http.Resp r = send("DELETE", href, null, null, h);
if (r.code == 412) throw new DavException(412, "Event was changed on the server meanwhile. Sync and try again.");
if (r.code == 412) throw new DavException(412, L.t("Event was changed on the server meanwhile. Sync and try again."));
if (r.code != 204 && r.code != 200 && r.code != 404) {
throw new DavException(r.code, "Delete failed: HTTP " + r.code);
throw new DavException(r.code, L.f("Delete failed: HTTP %d", r.code));
}
}
+2 -2
View File
@@ -20,8 +20,8 @@ public class DavException extends IOException {
public UntrustedCert(String fingerprint, String subject, String issuer, String expires,
boolean pinMismatch) {
super(pinMismatch ? "Server certificate changed (does not match the pinned fingerprint)"
: "Server certificate is not trusted by this phone");
super(pinMismatch ? L.t("Server certificate changed (does not match the pinned fingerprint)")
: L.t("Server certificate is not trusted by this phone"));
this.fingerprint = fingerprint;
this.subject = subject;
this.issuer = issuer;
+7
View File
@@ -296,6 +296,13 @@ public class ICal {
return c.getTimeInMillis();
}
/** Whether [start, end) covers part of the local day starting at `day`; zero-length counts at its start. */
public static boolean onDay(long start, long end, long day) {
long next = addDays(day, 1);
if (end <= start) return start >= day && start < next;
return start < next && end > day;
}
public static long startOfDay(long t) {
Calendar c = Calendar.getInstance();
c.setTimeInMillis(t);
+231
View File
@@ -0,0 +1,231 @@
package org.gingercal;
import java.text.DateFormatSymbols;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
/**
* Interface language. English text is the key; Galician comes from the table below.
* Android 2.3 ships no Galician locale data, so month and weekday names live here too.
* tools/check_i18n.py checks that every L.t()/L.f() key has a translation with matching
* placeholders.
*/
public final class L {
private L() {}
/** Galician on (the app default); the desktop tests run in English. */
public static volatile boolean gl;
private static final Map<String, String> GL = new HashMap<String, String>();
public static String t(String en) {
if (!gl) return en;
String v = GL.get(en);
return v == null ? en : v;
}
public static String f(String en, Object... args) {
return String.format(Locale.US, t(en), args);
}
// ------------------------------------------------------------------ dates
static final String[] GL_MONTHS = {"xaneiro", "febreiro", "marzo", "abril", "maio", "xuño", "xullo",
"agosto", "setembro", "outubro", "novembro", "decembro"};
static final String[] GL_MONTHS_SHORT = {"xan", "feb", "mar", "abr", "mai", "xuñ", "xul", "ago", "set",
"out", "nov", "dec"};
static final String[] GL_DAYS = {"", "domingo", "luns", "martes", "mércores", "xoves", "venres", "sábado"};
static final String[] GL_DAYS_SHORT = {"", "dom", "lun", "mar", "mér", "xov", "ven", "sáb"};
public static DateFormatSymbols symbols() {
if (!gl) return new DateFormatSymbols(Locale.getDefault());
DateFormatSymbols s = new DateFormatSymbols(Locale.US);
s.setMonths(GL_MONTHS);
s.setShortMonths(GL_MONTHS_SHORT);
s.setWeekdays(GL_DAYS);
s.setShortWeekdays(GL_DAYS_SHORT);
s.setAmPmStrings(new String[] {"a.m.", "p.m."});
return s;
}
public static SimpleDateFormat df(String pattern) {
return new SimpleDateFormat(pattern, symbols());
}
/** "Outubro de 2026" / "October 2026". */
public static String monthTitle(long t) {
String s = df(gl ? "MMMM 'de' yyyy" : "MMMM yyyy").format(new Date(t));
return s.length() > 0 ? Character.toUpperCase(s.charAt(0)) + s.substring(1) : s;
}
public static int firstDayOfWeek() {
return gl ? Calendar.MONDAY : Calendar.getInstance().getFirstDayOfWeek();
}
// ------------------------------------------------------------ translations
private static void put(String en, String gal) {
GL.put(en, gal);
}
static {
// main screen
put("Not set up yet: press Menu > Settings.", "Aínda sen configurar: preme Menú > Configuración.");
put("Syncing…", "Sincronizando…");
put("Loading %s from the server…", "Cargando %s do servidor…");
put("Couldn't load this month: %s", "Non se puido cargar este mes: %s");
put("never", "nunca");
put("Sync failed: %s\nLast good sync: %s", "Erro de sincronización: %s\nÚltima sincronización correcta: %s");
put("Synced %s", "Sincronizado: %s");
put("No upcoming events. Menu > New event, or Menu > Sync.", "Non hai eventos próximos. Menú > Novo evento ou Menú > Sincronizar.");
put("Nothing on %s. Long-press a day to add an event.", "Non hai nada o %s. Mantén premido un día para engadir un evento.");
put("View", "Vista");
put("Agenda", "Axenda");
put("Month", "Mes");
put("New event", "Novo evento");
put("Sync", "Sincronizar");
put("Today", "Hoxe");
put("Calendars", "Calendarios");
put("Settings", "Configuración");
put("Sync first: no writable calendar yet.", "Sincroniza primeiro: aínda non hai ningún calendario con permiso de escritura.");
put("Calendar: %s", "Calendario: %s");
put("Repeating event: edit the series in Nextcloud. Delete removes the whole series.", "Evento periódico: edita a serie en Nextcloud. Eliminar borra a serie completa.");
put("(no title)", "(sen título)");
put("(repeats)", "(repítese)");
put("Close", "Pechar");
put("Edit", "Editar");
put("Delete", "Eliminar");
put("Delete event?", "Eliminar o evento?");
put("This deletes EVERY occurrence of \"%s\".", "Isto elimina TODAS as repeticións de «%s».");
put("\"%s\" will be deleted on the server.", "«%s» eliminarase no servidor.");
put("Cancel", "Cancelar");
put("Deleting…", "Eliminando…");
put("Delete failed", "Non se puido eliminar");
put("OK", "Aceptar");
// dates and times
put("Today · %s", "Hoxe · %s");
put("Tomorrow · %s", "Mañá · %s");
put("All day", "Todo o día");
put("All day, until %s", "Todo o día, ata o %s");
put("No reminder", "Sen aviso");
put("At start time", "Á hora de comezo");
put("%d min before", "%d min antes");
put("%d h before", "%d h antes");
put("%d day before", "%d día antes");
// event editor
put("Edit event", "Editar evento");
put("Title", "Título");
put("Starts", "Comeza");
put("Ends", "Remata");
put("Location", "Lugar");
put("Notes", "Notas");
put("Save", "Gardar");
put("Alarm: %s", "Aviso: %s");
put("Alarm (saved in the event)", "Aviso (gárdase no evento)");
put("Calendar", "Calendario");
put("The end is before the start.", "O final é anterior ao comezo.");
put("No writable calendar. Sync first.", "Non hai ningún calendario con permiso de escritura. Sincroniza primeiro.");
put("Loading…", "Cargando…");
put("Can't load event", "Non se puido cargar o evento");
put("Repeating event", "Evento periódico");
put("Repeating events can't be edited on the phone yet. Edit the series in Nextcloud.", "Os eventos periódicos aínda non se poden editar no teléfono. Edita a serie en Nextcloud.");
put("Can't edit", "Non se pode editar");
put("The server returned no event data.", "O servidor non devolveu os datos do evento.");
put("Saving…", "Gardando…");
put("Not saved", "Non se gardou");
put("Saved", "Gardado");
// calendars
put("No calendars yet. Set up the account in Settings and sync.", "Aínda non hai calendarios. Configura a conta en Configuración e sincroniza.");
put("(read-only)", "(só lectura)");
put("first writable", "o primeiro con escritura");
put("New events go to: %s", "Os eventos novos gárdanse en: %s");
put("Default calendar", "Calendario predeterminado");
// settings
put("GingerCal settings", "Configuración de GingerCal");
put("Nextcloud server. Android 2.3 can't speak TLS 1.2, so use either your legacy-TLS bridge (https://cloud.example.com:8443) or http:// to the server on your home Wi-Fi. Use an app password, not your real one.",
"Servidor Nextcloud. Android 2.3 non admite TLS 1.2, así que usa a túa ponte TLS antiga (https://cloud.example.com:8443) ou http:// ao servidor na wifi da casa. Usa un contrasinal de aplicación, non o teu contrasinal real.");
put("Server address", "Enderezo do servidor");
put("User name", "Usuario");
put("App password", "Contrasinal de aplicación");
put("Pinned certificate SHA-256 (filled in for you)", "SHA-256 do certificado fixado (énchese automaticamente)");
put("optional", "opcional");
put("Days ahead to sync", "Días futuros que sincronizar");
put("Save and test connection", "Gardar e probar a conexión");
put("Re-download everything", "Descargar todo de novo");
put("Background sync", "Sincronización automática");
put("Background sync: %s", "Sincronización automática: %s");
put("Manual only", "Só manual");
put("Every hour", "Cada hora");
put("Every %d hours", "Cada %d horas");
put("Phone notification", "Notificación no teléfono");
put("Notification: %s", "Notificación: %s");
put("Off", "Desactivada");
put("off", "desactivada");
put("Language: %s", "Lingua: %s");
put("Re-downloading in the background", "Descargando todo de novo en segundo plano");
put("Server address and user name are required.", "O enderezo do servidor e o usuario son obrigatorios.");
put("Unencrypted connection", "Conexión sen cifrar");
put("http:// sends your password in clear text over the internet. Use the legacy-TLS bridge instead, or only use http:// to a LAN address.",
"Con http:// o contrasinal viaxa sen cifrar por internet. Usa a ponte TLS antiga ou usa http:// só cun enderezo da rede local.");
put("Use anyway", "Usar igualmente");
put("Connecting…", "Conectando…");
put("Connection failed", "Erro de conexión");
put("Connected. Calendars found: %d\n", "Conectado. Calendarios atopados: %d\n");
put("It works", "Funciona");
put("Sync now", "Sincronizar agora");
// certificate trust
put("WARNING: the server certificate changed since you trusted it. That happens when you regenerate the bridge certificate - or when someone is intercepting the connection.\n\n",
"AVISO: o certificado do servidor cambiou desde que confiaches nel. Isto pasa se rexeneras o certificado da ponte, ou se alguén está a interceptar a conexión.\n\n");
put("This phone doesn't trust the server certificate (normal for the self-signed legacy bridge, or for Let's Encrypt on Android 2.3).\n\n",
"Este teléfono non confía no certificado do servidor (é normal coa ponte antiga autoasinada ou con Let's Encrypt en Android 2.3).\n\n");
put("Subject: %s\nIssuer: %s\nExpires: %s\n\nSHA-256:\n%s\n\nOnly trust it if this matches the fingerprint printed on your server.",
"Suxeito: %s\nEmisor: %s\nCaduca: %s\n\nSHA-256:\n%s\n\nConfía nel só se coincide coa pegada dixital que mostra o teu servidor.");
put("Certificate changed!", "O certificado cambiou!");
put("Trust this certificate?", "Confiar neste certificado?");
put("Trust", "Confiar");
put("Server certificate changed (does not match the pinned fingerprint)", "O certificado do servidor cambiou (non coincide coa pegada fixada)");
put("Server certificate is not trusted by this phone", "Este teléfono non confía no certificado do servidor");
// errors
put("Can't resolve %s. No connection, or a typo in the server address.", "Non se pode resolver %s. Non hai conexión ou hai un erro no enderezo do servidor.");
put("Can't connect to the server (%s). Wrong port, or the bridge is down?", "Non se pode conectar co servidor (%s). Porto incorrecto ou ponte caída?");
put("The server took too long to answer.", "O servidor tardou de máis en responder.");
put("TLS error: %s", "Erro TLS: %s");
put("Network error: %s", "Erro de rede: %s");
put("Out of memory. Lower 'Days ahead' in Settings.", "Sen memoria. Reduce «Días futuros» na configuración.");
put("Error: %s", "Erro: %s");
put("Login refused (401). Check the user name and use a Nextcloud app password (Settings > Security > Devices & sessions).",
"Acceso rexeitado (401). Comproba o usuario e usa un contrasinal de aplicación de Nextcloud (Configuración > Seguranza > Dispositivos e sesións).");
put("Not allowed to write to this calendar (403). It may be read-only/shared.", "Non tes permiso para escribir neste calendario (403). Pode ser de só lectura ou compartido.");
put("Too many redirects from the server.", "O servidor fixo demasiadas redireccións.");
put("The server redirected http:// to %s (Nextcloud forces HTTPS / HSTS). Android 2.3 can't follow that: use the legacy-TLS bridge address instead (see README, 'Forced HTTPS / HSTS').",
"O servidor redirixiu http:// a %s (Nextcloud obriga a usar HTTPS / HSTS). Android 2.3 non pode seguir esa redirección: usa o enderezo da ponte TLS antiga (consulta o README, «Forced HTTPS / HSTS»).");
put("The server redirected to %s, not the address set in the app. Nextcloud is sending its public URL; check the bridge's proxy_redirect line (README, 'Forced HTTPS / HSTS').",
"O servidor redirixiu a %s, que non é o enderezo configurado na aplicación. Nextcloud está a enviar o seu URL público; revisa a liña proxy_redirect da ponte (README, «Forced HTTPS / HSTS»).");
put("Redirect loop (HTTP %d back to the same address). Nextcloud forces HTTPS but the bridge reaches it over plain HTTP: switch the bridge to proxy_pass https:// (README, 'Forced HTTPS / HSTS').",
"Bucle de redirección (HTTP %d ao mesmo enderezo). Nextcloud obriga a usar HTTPS pero a ponte conecta con el por HTTP: cambia a ponte a proxy_pass https:// (README, «Forced HTTPS / HSTS»).");
put("%s %s failed: HTTP %d%s", "%s %s fallou: HTTP %d%s");
put(" (not found)", " (non atopado)");
put(" (not a WebDAV URL?)", " (non é un URL WebDAV?)");
put("Server did not report a CalDAV principal", "O servidor non informou de ningún principal CalDAV");
put("Could not load event: HTTP %d", "Non se puido cargar o evento: HTTP %d");
put("Saving failed: HTTP %d", "Non se puido gardar: HTTP %d");
put("Event was changed on the server meanwhile. Sync and try again.", "O evento cambiou no servidor mentres tanto. Sincroniza e téntao de novo.");
put("Delete failed: HTTP %d", "Non se puido eliminar: HTTP %d");
put("TLS handshake failed before any certificate was sent. The server most likely requires TLS 1.2+, which Android 2.3 cannot do. Point the app at the legacy-TLS bridge (see README) or use http:// on your LAN. (%s)",
"A negociación TLS fallou antes de recibir ningún certificado. Seguramente o servidor esixe TLS 1.2 ou superior, que Android 2.3 non admite. Configura a aplicación coa ponte TLS antiga (consulta o README) ou usa http:// na rede local. (%s)");
put("Bad XML from server: %s", "XML incorrecto do servidor: %s");
put("Empty XML from server", "XML baleiro do servidor");
put("%d calendars: %d refreshed (%d events)", "Calendarios: %d, actualizados: %d (eventos: %d)");
put(", %d unchanged", ", sen cambios: %d");
}
}
+6 -2
View File
@@ -16,10 +16,12 @@ public class Syncer {
public static class Result {
public int calendars, fetched, skipped, events;
/** The time window now cached locally. */
public long from, to;
public String summary() {
return calendars + " calendars: " + fetched + " refreshed (" + events + " events)"
+ (skipped > 0 ? ", " + skipped + " unchanged" : "");
return L.f("%d calendars: %d refreshed (%d events)", calendars, fetched, events)
+ (skipped > 0 ? L.f(", %d unchanged", skipped) : "");
}
}
@@ -32,6 +34,8 @@ public class Syncer {
Set<String> seen = new HashSet<String>();
long from = ICal.addDays(ICal.startOfDay(now), -pastDays);
long to = ICal.addDays(ICal.startOfDay(now), futureDays + 1);
res.from = from;
res.to = to;
for (Cal r : remote) {
seen.add(r.href);
+1 -3
View File
@@ -101,9 +101,7 @@ public final class Tls {
return new DavException.UntrustedCert(seenFp, seenSubject, seenIssuer, seenExpiry, mismatch);
}
if (e instanceof SSLException && seenFp == null) {
return new DavException(0, "TLS handshake failed before any certificate was sent. The server "
+ "most likely requires TLS 1.2+, which Android 2.3 cannot do. Point the app at the "
+ "legacy-TLS bridge (see README) or use http:// on your LAN. (" + e.getMessage() + ")");
return new DavException(0, L.f("TLS handshake failed before any certificate was sent. The server most likely requires TLS 1.2+, which Android 2.3 cannot do. Point the app at the legacy-TLS bridge (see README) or use http:// on your LAN. (%s)", e.getMessage()));
}
return e;
}
+2 -2
View File
@@ -106,9 +106,9 @@ public class Xml {
} catch (IOException e) {
throw e;
} catch (Exception e) {
throw new IOException("Bad XML from server: " + e.getMessage());
throw new IOException(L.f("Bad XML from server: %s", e.getMessage()));
}
if (root.kids.isEmpty()) throw new IOException("Empty XML from server");
if (root.kids.isEmpty()) throw new IOException(L.t("Empty XML from server"));
return root.kids.get(0);
}