Compare commits
2
Commits
589c8cdf6e
...
17dc9719e9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17dc9719e9 | ||
|
|
0f58331654 |
@@ -1,6 +1,7 @@
|
||||
import type { CalendarEvent } from './shared';
|
||||
import { CalendarDays, MapPin, Repeat } from 'lucide-react';
|
||||
import { useCalendarEvents, useSelectedCollection } from './useDavData';
|
||||
import { useTranslation } from '../../locales';
|
||||
|
||||
// Right panel of /calendar. Deliberately a grouped agenda list, not a month grid: the sidecar returns
|
||||
// events as they are stored, with RRULE unexpanded, so a grid would have to invent occurrences the server
|
||||
@@ -26,12 +27,13 @@ const formatTime = (ev: CalendarEvent) => {
|
||||
};
|
||||
|
||||
export const CalendarView = () => {
|
||||
const { t } = useTranslation();
|
||||
const { selected, isLoading: loadingCollections } = useSelectedCollection('calendar');
|
||||
const { events, isLoading, error } = useCalendarEvents(selected?.path ?? null);
|
||||
|
||||
if (loadingCollections) return <Placeholder text="Loading calendars…" />;
|
||||
if (!selected) return <Placeholder text="No calendars yet. Create one from a CalDAV client." />;
|
||||
if (error) return <Placeholder text={`Could not read ${selected.displayName}: ${String(error)}`} />;
|
||||
if (error) return <Placeholder text={t('dav.readFailed', { name: selected.displayName, reason: String(error) })} />;
|
||||
if (isLoading) return <Placeholder text="Loading events…" />;
|
||||
if (events.length === 0) return <Placeholder text={`${selected.displayName} has no events.`} />;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Link, useSearchParams } from 'react-router';
|
||||
import { CalendarDays, Contact as ContactIcon } from 'lucide-react';
|
||||
import { davCollectionPath, COLLECTION_PARAM } from './shared';
|
||||
import { useDavCollections } from './useDavData';
|
||||
import { useTranslation } from '../../locales';
|
||||
|
||||
// Left panel for both /calendar and /contacts: the list of collections belonging to this user.
|
||||
//
|
||||
@@ -22,6 +23,7 @@ type Props = {
|
||||
const ROW = 'group relative flex items-center gap-3 rounded-lg px-3 py-2 text-left text-sm transition-colors';
|
||||
|
||||
const CollectionNav = ({ kind, base, title, icon: Icon }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const [params] = useSearchParams();
|
||||
const { collections, isLoading } = useDavCollections(kind);
|
||||
const fromUrl = params.get(COLLECTION_PARAM);
|
||||
@@ -69,19 +71,19 @@ const CollectionNav = ({ kind, base, title, icon: Icon }: Props) => {
|
||||
);
|
||||
})}
|
||||
{!isLoading && collections.length === 0 && (
|
||||
<p className="px-3 py-2 text-xs text-muted-foreground">
|
||||
Nothing here yet. Add one from a phone or desktop client over CalDAV/CardDAV.
|
||||
</p>
|
||||
<p className="px-3 py-2 text-xs text-muted-foreground">{t('dav.empty')}</p>
|
||||
)}
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CalendarNav = () => (
|
||||
<CollectionNav kind="calendar" base="/calendar" title="Calendar" icon={CalendarDays} />
|
||||
);
|
||||
export const CalendarNav = () => {
|
||||
const { t } = useTranslation();
|
||||
return <CollectionNav kind="calendar" base="/calendar" title={t('dav.calendar')} icon={CalendarDays} />;
|
||||
};
|
||||
|
||||
export const ContactsNav = () => (
|
||||
<CollectionNav kind="addressbook" base="/contacts" title="Contacts" icon={ContactIcon} />
|
||||
);
|
||||
export const ContactsNav = () => {
|
||||
const { t } = useTranslation();
|
||||
return <CollectionNav kind="addressbook" base="/contacts" title={t('dav.contacts')} icon={ContactIcon} />;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Building2, Mail, Phone, User } from 'lucide-react';
|
||||
import { useDavContacts, useSelectedCollection } from './useDavData';
|
||||
import { useTranslation } from '../../locales';
|
||||
|
||||
// Right panel of /contacts: the selected address book as cards, sorted by name.
|
||||
//
|
||||
@@ -16,12 +17,13 @@ const initials = (name: string) =>
|
||||
.join('') || '?';
|
||||
|
||||
export const ContactsView = () => {
|
||||
const { t } = useTranslation();
|
||||
const { selected, isLoading: loadingCollections } = useSelectedCollection('addressbook');
|
||||
const { contacts, isLoading, error } = useDavContacts(selected?.path ?? null);
|
||||
|
||||
if (loadingCollections) return <Placeholder text="Loading address books…" />;
|
||||
if (!selected) return <Placeholder text="No address books yet. Create one from a CardDAV client." />;
|
||||
if (error) return <Placeholder text={`Could not read ${selected.displayName}: ${String(error)}`} />;
|
||||
if (error) return <Placeholder text={t('dav.readFailed', { name: selected.displayName, reason: String(error) })} />;
|
||||
if (isLoading) return <Placeholder text="Loading contacts…" />;
|
||||
if (contacts.length === 0) return <Placeholder text={`${selected.displayName} has no contacts.`} />;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import jsQR from 'jsqr';
|
||||
import { Camera, Download, RotateCcw } from 'lucide-react';
|
||||
import { crc32, decodeFrame, reassemble, type MetaFrame } from './protocol';
|
||||
import { useTranslation } from '../../locales';
|
||||
|
||||
// Receiver: camera → jsQR → collect frames until every index is present.
|
||||
//
|
||||
@@ -15,6 +16,7 @@ import { crc32, decodeFrame, reassemble, type MetaFrame } from './protocol';
|
||||
type Progress = { meta: MetaFrame; have: number };
|
||||
|
||||
export const QrReceiver = () => {
|
||||
const { t } = useTranslation();
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const chunksRef = useRef(new Map<number, Uint8Array>());
|
||||
@@ -130,7 +132,7 @@ export const QrReceiver = () => {
|
||||
className="flex cursor-pointer items-center gap-2 rounded-md border border-duck-dark/20 px-3 py-2 text-sm text-duck-dark/80 hover:border-duck-teal hover:text-duck-teal"
|
||||
>
|
||||
<Camera className="h-4 w-4" />
|
||||
Start scanning
|
||||
{t('qrTransfer.startScanning')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -172,18 +174,14 @@ export const QrReceiver = () => {
|
||||
className="flex cursor-pointer items-center gap-1 text-xs text-duck-dark/50 hover:text-duck-teal"
|
||||
>
|
||||
<RotateCcw className="h-3.5 w-3.5" />
|
||||
Receive another
|
||||
{t('qrTransfer.receiveAnother')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{scanning && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={stop}
|
||||
className="cursor-pointer text-xs text-duck-dark/50 hover:text-red-500"
|
||||
>
|
||||
Stop
|
||||
<button type="button" onClick={stop} className="cursor-pointer text-xs text-duck-dark/50 hover:text-red-500">
|
||||
{t('qrTransfer.stop')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import QRCode from 'qrcode';
|
||||
import { Upload, Play, Pause } from 'lucide-react';
|
||||
import {
|
||||
chunkFile,
|
||||
crc32,
|
||||
encodeData,
|
||||
encodeMeta,
|
||||
newSessionId,
|
||||
DEFAULT_CHUNK_BYTES,
|
||||
} from './protocol';
|
||||
import { chunkFile, crc32, encodeData, encodeMeta, newSessionId, DEFAULT_CHUNK_BYTES } from './protocol';
|
||||
import { useTranslation } from '../../locales';
|
||||
|
||||
// Sender: a file becomes a looping carousel of QR frames.
|
||||
//
|
||||
@@ -24,6 +18,7 @@ const META_EVERY = 12;
|
||||
const SPEEDS = [4, 6, 8, 12] as const;
|
||||
|
||||
export const QrSender = () => {
|
||||
const { t } = useTranslation();
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const [file, setFile] = useState<{ name: string; mime: string; bytes: Uint8Array } | null>(null);
|
||||
const [frames, setFrames] = useState<string[]>([]);
|
||||
@@ -82,7 +77,7 @@ export const QrSender = () => {
|
||||
<div className="flex h-full w-full flex-col items-center gap-3 overflow-y-auto p-3">
|
||||
<label className="flex w-full cursor-pointer items-center justify-center gap-2 rounded-md border border-dashed border-duck-dark/25 py-3 text-sm text-duck-dark/70 hover:border-duck-teal hover:text-duck-teal">
|
||||
<Upload className="h-4 w-4" />
|
||||
{file ? file.name : 'Choose a file to send'}
|
||||
{file ? file.name : t('qrTransfer.chooseFile')}
|
||||
<input
|
||||
type="file"
|
||||
className="hidden"
|
||||
@@ -129,8 +124,8 @@ export const QrSender = () => {
|
||||
</div>
|
||||
|
||||
<p className="text-center text-[10px] leading-relaxed text-duck-dark/40">
|
||||
Keep this on screen until the receiver reports 100%. It loops forever — a missed frame is
|
||||
picked up on the next pass.
|
||||
Keep this on screen until the receiver reports 100%. It loops forever — a missed frame is picked up on the
|
||||
next pass.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -11,5 +11,17 @@
|
||||
"unknownMessage": "The VNC server sent an unknown message type ({{type}}).",
|
||||
"unknownEncoding": "The VNC server used encoding {{encoding}}, which this client did not ask for."
|
||||
}
|
||||
},
|
||||
"qrTransfer": {
|
||||
"chooseFile": "Choose a file to send",
|
||||
"startScanning": "Start scanning",
|
||||
"receiveAnother": "Receive another",
|
||||
"stop": "Stop"
|
||||
},
|
||||
"dav": {
|
||||
"empty": "Nothing here yet. Add one from a phone or desktop client over CalDAV/CardDAV.",
|
||||
"calendar": "Calendar",
|
||||
"contacts": "Contacts",
|
||||
"readFailed": "Could not read {{name}}: {{reason}}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,5 +11,17 @@
|
||||
"unknownMessage": "O servidor VNC enviou um tipo de mensagem desconhecido ({{type}}).",
|
||||
"unknownEncoding": "O servidor VNC usou a codificação {{encoding}}, que este cliente não pediu."
|
||||
}
|
||||
},
|
||||
"qrTransfer": {
|
||||
"chooseFile": "Escolha um ficheiro para enviar",
|
||||
"startScanning": "Começar a digitalizar",
|
||||
"receiveAnother": "Receber outro",
|
||||
"stop": "Parar"
|
||||
},
|
||||
"dav": {
|
||||
"empty": "Ainda não há nada aqui. Adicione um a partir de um cliente de telemóvel ou computador por CalDAV/CardDAV.",
|
||||
"calendar": "Calendário",
|
||||
"contacts": "Contactos",
|
||||
"readFailed": "Não foi possível ler {{name}}: {{reason}}"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user