feat: add WebDAV file browser with auth improvements

Add a new Files section powered by WebDAV for browsing, uploading,
downloading, renaming, and deleting files and folders.

New features:
- WebDAV file browser with grid/list views and breadcrumb navigation
- File upload (drag-and-drop and button), folder creation, rename, delete
- File preview modals for images and other file types
- WebDAV proxy API route to handle authentication
- Navigation rail entry for Files (auto-hidden when WebDAV is unsupported)

Auth improvements:
- Fix premature redirects on calendar, contacts, and settings pages by
  adding explicit auth check on mount before redirecting to login
- Persist active settings tab in localStorage

Other:
- Expose getAuthHeader() and getServerUrl() on JMAPClient
- Add WebDAV store with connection testing and capability detection
- Add i18n translations for file browser in all 8 locales (de, en, es,
  fr, it, ja, nl, pt)
This commit is contained in:
Linus Rath
2026-03-15 05:24:27 +01:00
parent cb74e3bf73
commit 1f1db8fac8
23 changed files with 4179 additions and 12 deletions
+12 -4
View File
@@ -45,7 +45,8 @@ export default function CalendarPage() {
const router = useRouter();
const t = useTranslations("calendar");
const isMobile = useIsMobile();
const { client, isAuthenticated, logout } = useAuthStore();
const { client, isAuthenticated, logout, checkAuth, isLoading: authLoading } = useAuthStore();
const [initialCheckDone, setInitialCheckDone] = useState(() => useAuthStore.getState().isAuthenticated && !!useAuthStore.getState().client);
const { quota, isPushConnected } = useEmailStore();
const {
calendars, events, selectedDate, viewMode, selectedCalendarIds,
@@ -76,14 +77,21 @@ export default function CalendarPage() {
// Swipe navigation ref (handlers defined after navigatePrev/navigateNext)
const touchStartRef = useRef<{ x: number; y: number; time: number } | null>(null);
// Check auth on mount
useEffect(() => {
if (!isAuthenticated) {
checkAuth().finally(() => {
setInitialCheckDone(true);
});
}, [checkAuth]);
useEffect(() => {
if (initialCheckDone && !isAuthenticated && !authLoading) {
try { sessionStorage.setItem('redirect_after_login', window.location.pathname); } catch { /* ignore */ }
router.push("/login");
} else if (!supportsCalendar) {
} else if (client && !supportsCalendar) {
router.push("/");
}
}, [isAuthenticated, supportsCalendar, router]);
}, [initialCheckDone, isAuthenticated, authLoading, client, supportsCalendar, router]);
useEffect(() => {
if (error) {