Files
Oxicloud/static/profile.html
T
DioCrafts 81a93a489b feat: photo/video capture-date pipeline + premium UI/UX overhaul
Backend — Photos timeline now groups by real capture date instead of upload time. New MediaMetadataService (FileLifecycleHook) extracts EXIF DateTimeOriginal from images and container creation_time from videos (mov/mp4/mkv) via nom-exif, timezone-correct (OffsetTimeOriginal), persisting captured_at so the existing media_sort_date trigger takes over. Adds POST /admin/photos/metadata/reextract to backfill existing media. Falls back to upload date when no embedded date exists.

Frontend — premium grid cards: combined metadata line (relative date · size, owner avatar when shared), custom selection checkbox with a clear checked state, uniform full-width 4:3 thumbnail tiles independent of filename length, centered file-type icons, and a hit-test fix so checkbox/star/kebab clicks reach the controls (the decorative thumbnail no longer captures pointer events). Notification messages internationalised across all 16 locales. Broader polish: design tokens, a11y/focus-visible states, brand + PWA assets.

Chore — bump semver-compatible dependencies (cargo upgrade); add nom-exif 3.6.1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 00:24:27 +02:00

250 lines
13 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Declares the page supports both modes. Switching between them is
driven by the CSS `color-scheme` property (via the `data-color-scheme`
attribute on <html> set by theme-init.js), not by mutating this tag. -->
<meta name="color-scheme" content="light dark">
<title>OxiCloud — My Profile</title>
<link rel="icon" type="image/svg+xml" href="/logo/logo-plain.svg">
<script src="/js/core/theme-init.js"></script>
<link rel="stylesheet" href="/css/main.css">
<link rel="stylesheet" href="/css/views/profile.css">
</head>
<body>
<div class="profile-header">
<div class="profile-header-left">
<a href="/" class="profile-logo-link link-reset-flex">
<div class="profile-logo">
<svg viewBox="0 0 500 500">
<path d="M345 310c32 0 58-26 58-58s-26-58-58-58c-6.2 0-12 0.9-17.5 2.7C318 166 289 143 255 143c-34.3 0-63.1 22.6-73 53.7C176.9 195.7 171 195 165 195c-32 0-58 26-58 58s26 58 58 58h180z"/>
</svg>
</div>
<span class="profile-title-text">OxiCloud</span>
</a>
<span class="profile-title-separator">· <span data-i18n="profile.page_title">Profile</span></span>
</div>
<div class="profile-header-right">
<a href="/"><i class="fas fa-arrow-left"></i> <span data-i18n="profile.back_to_app">Back to OxiCloud</span></a>
</div>
</div>
<div class="profile-container" role="main">
<div id="loading"><i class="fas fa-circle-notch"></i> <span data-i18n="profile.loading">Loading…</span></div>
<div id="auth-error" class="hidden">
<div class="err-icon"><i class="fas fa-lock"></i></div>
<h1 data-i18n="profile.not_authenticated">Not Authenticated</h1>
<p data-i18n="profile.not_authenticated_desc">Please sign in to view your profile.</p>
<a href="/login"><i class="fas fa-sign-in-alt"></i> <span data-i18n="profile.sign_in">Sign in</span></a>
</div>
<div id="main-content" class="hidden">
<div class="profile-card">
<div class="avatar-section">
<div class="avatar-large-wrap">
<div class="avatar-large" id="p-avatar">—</div>
<button class="avatar-edit-btn hidden" id="p-avatar-edit-btn" data-i18n-title="profile.edit_photo" title="Edit photo">
<i class="fas fa-pencil-alt"></i>
</button>
</div>
<div class="avatar-info">
<h1 id="p-username">—</h1>
<div class="email" id="p-email">—</div>
<span class="role-badge" id="p-role-badge">—</span>
<p class="avatar-oidc-note hidden" id="p-avatar-oidc-note" data-i18n="profile.photo_managed_by_oidc">Photo managed by your identity provider.</p>
</div>
</div>
<!-- Photo edit panel (local accounts only) -->
<div class="avatar-edit-panel hidden" id="p-avatar-edit-panel">
<div class="avatar-edit-tabs">
<button class="avatar-tab active" id="p-tab-url" data-i18n="profile.photo_tab_url">URL</button>
<button class="avatar-tab" id="p-tab-upload" data-i18n="profile.photo_tab_upload">Upload</button>
</div>
<!-- URL input mode -->
<div class="avatar-tab-pane" id="p-pane-url">
<input type="url" id="p-image-url" class="avatar-url-input"
data-i18n-placeholder="profile.photo_url_placeholder"
placeholder="https://example.com/photo.jpg">
<small class="avatar-hint" data-i18n="profile.photo_url_hint">https://, http://, or data:image/…;base64,… accepted</small>
</div>
<!-- Upload mode -->
<div class="avatar-tab-pane hidden" id="p-pane-upload">
<label class="avatar-file-label" for="p-image-file">
<i class="fas fa-cloud-upload-alt"></i>
<span data-i18n="profile.photo_choose_file">Choose a photo (PNG, JPEG, WebP)</span>
</label>
<input type="file" id="p-image-file" class="avatar-file-input"
accept="image/png,image/jpeg,image/webp">
<img class="avatar-preview hidden" id="p-image-preview" alt="Preview" src="">
<small class="avatar-hint" data-i18n="profile.photo_resize_note">Images larger than 512 × 512 px are automatically resized.</small>
</div>
<div class="avatar-edit-actions">
<button class="btn btn-primary" id="p-avatar-save" data-i18n="profile.photo_save">Save</button>
<button class="btn btn-danger-sm" id="p-avatar-remove" data-i18n="profile.photo_remove">Remove photo</button>
<button class="btn btn-secondary" id="p-avatar-cancel" data-i18n="profile.photo_cancel">Cancel</button>
</div>
<div id="p-avatar-status"></div>
</div>
</div>
<div class="profile-card">
<h2><i class="fas fa-id-card"></i> <span data-i18n="profile.account_details">Account Details</span></h2>
<div class="info-grid">
<div class="info-item">
<div class="info-label"><i class="fas fa-user"></i> <span data-i18n="profile.username">Username</span></div>
<div class="info-value" id="p-detail-username">—</div>
</div>
<div class="info-item">
<div class="info-label"><i class="fas fa-envelope"></i> <span data-i18n="profile.email">Email</span></div>
<div class="info-value" id="p-detail-email">—</div>
</div>
<div class="info-item">
<div class="info-label"><i class="fas fa-shield-alt"></i> <span data-i18n="profile.role">Role</span></div>
<div class="info-value" id="p-detail-role">—</div>
</div>
<div class="info-item">
<div class="info-label"><i class="fas fa-clock"></i> <span data-i18n="profile.last_login">Last Login</span></div>
<div class="info-value" id="p-detail-login">—</div>
</div>
</div>
</div>
<div class="profile-card" id="profile-edit-section">
<h2><i class="fas fa-id-badge"></i> <span data-i18n="profile.edit_profile">Edit Profile</span></h2>
<div id="profile-edit-oidc-note" class="alert alert-info hidden">
<i class="fas fa-info-circle"></i>
<span data-i18n="profile.edit_oidc_managed">To change your information (name, first name, profile picture, …), please update it at your identity provider. Your changes will appear on your next sign-in.</span>
</div>
<form id="profile-edit-form">
<div class="form-group">
<label for="profile-edit-username" data-i18n="profile.username">Username</label>
<input type="text" id="profile-edit-username" maxlength="64" autocomplete="username"
data-i18n-placeholder="profile.username_placeholder" placeholder="e.g. alice">
<small id="profile-edit-username-hint" data-i18n="profile.username_claim_hint">2–64 characters, letters/digits/dot/dash/underscore. Once chosen, the username can't be changed (DAV/NextCloud clients depend on it).</small>
</div>
<div class="form-group">
<label for="profile-edit-given-name" data-i18n="profile.given_name">First name</label>
<input type="text" id="profile-edit-given-name" maxlength="128" autocomplete="given-name">
</div>
<div class="form-group">
<label for="profile-edit-family-name" data-i18n="profile.family_name">Last name</label>
<input type="text" id="profile-edit-family-name" maxlength="128" autocomplete="family-name">
</div>
<div class="form-group form-group--checkbox">
<label for="profile-edit-notify-on-share" class="checkbox-label">
<input type="checkbox" id="profile-edit-notify-on-share">
<span data-i18n="profile.notify_on_share">Email me when someone shares with me</span>
</label>
<small data-i18n="profile.notify_on_share_hint">When unchecked, shares still appear in your account — you just won't get an email about them.</small>
</div>
<button type="submit" class="btn btn-primary" id="profile-edit-submit">
<i class="fas fa-save"></i> <span data-i18n="profile.save_profile">Save changes</span>
</button>
<div id="profile-edit-status"></div>
</form>
</div>
<div class="profile-card">
<h2><i class="fas fa-hdd"></i> <span data-i18n="profile.storage">Storage</span></h2>
<div class="storage-stats">
<div class="storage-stat">
<div class="stat-value" id="p-storage-used">—</div>
<div class="stat-label" data-i18n="profile.used">Used</div>
</div>
<div class="storage-stat">
<div class="stat-value" id="p-storage-quota">—</div>
<div class="stat-label" data-i18n="profile.quota">Quota</div>
</div>
<div class="storage-stat">
<div class="stat-value" id="p-storage-pct">—</div>
<div class="stat-label" data-i18n="profile.usage">Usage</div>
</div>
</div>
<div class="storage-bar-wrap">
<div class="storage-bar"><div class="storage-fill green width-zero" id="p-storage-bar"></div></div>
<div class="storage-text" id="p-storage-text">—</div>
</div>
</div>
<div class="profile-card" id="app-passwords-section">
<h2><i class="fas fa-key"></i> <span data-i18n="profile.app_passwords">App Passwords</span></h2>
<p class="app-pw-desc" data-i18n="profile.app_pw_desc">Generate passwords for WebDAV, CalDAV, and CardDAV clients. Each password is shown only once.</p>
<div class="app-pw-create">
<input type="text" id="app-pw-label" data-i18n-placeholder="profile.app_pw_label_placeholder" placeholder="Label (e.g. Thunderbird, macOS)" maxlength="128">
<button class="btn btn-primary" id="app-pw-generate"><i class="fas fa-plus"></i> <span data-i18n="profile.generate">Generate</span></button>
</div>
<div id="app-pw-created" class="app-pw-created hidden">
<div class="app-pw-created-label"><span data-i18n="profile.new_password_for">New password for</span> <strong id="app-pw-created-label"></strong>:</div>
<div class="app-pw-created-value">
<code id="app-pw-created-password"></code>
<button class="btn btn-copy" id="app-pw-copy-btn" data-i18n-title="profile.copy_to_clipboard" title="Copy to clipboard"><i class="fas fa-copy"></i></button>
</div>
<small data-i18n="profile.copy_warning">Copy this password now. You won't be able to see it again.</small>
</div>
<div id="app-pw-status"></div>
<table class="app-pw-table" id="app-pw-table">
<thead>
<tr><th data-i18n="profile.col_label">Label</th><th data-i18n="profile.col_created">Created</th><th data-i18n="profile.col_last_used">Last Used</th><th data-i18n="profile.col_status">Status</th><th></th></tr>
</thead>
<tbody id="app-pw-tbody"></tbody>
</table>
<div id="app-pw-empty" class="app-pw-empty hidden" data-i18n="profile.no_app_passwords">No app passwords yet.</div>
<div id="app-pw-auto-section" class="app-pw-auto-section hidden">
<button class="app-pw-auto-toggle" id="app-pw-auto-toggle">
<i class="fas fa-chevron-right" id="app-pw-auto-chevron"></i>
<span data-i18n="profile.client_sessions">Client sessions</span>
<span class="app-pw-auto-count" id="app-pw-auto-count">0</span>
</button>
<div id="app-pw-auto-body" class="hidden">
<p class="app-pw-auto-desc" data-i18n="profile.client_sessions_desc">Auto-generated when you connect a Nextcloud-compatible client.</p>
<table class="app-pw-table" id="app-pw-auto-table">
<thead>
<tr><th data-i18n="profile.col_client">Client</th><th data-i18n="profile.col_created">Created</th><th data-i18n="profile.col_last_used">Last Used</th><th></th></tr>
</thead>
<tbody id="app-pw-auto-tbody"></tbody>
</table>
</div>
</div>
</div>
<div class="profile-card" id="password-section">
<h2><i class="fas fa-key"></i> <span data-i18n="profile.change_password">Change Password</span></h2>
<form id="password-form">
<div class="form-group">
<label for="current-password" data-i18n="profile.current_password">Current Password</label>
<input type="password" id="current-password" required autocomplete="current-password">
</div>
<div class="form-group">
<label for="new-password" data-i18n="profile.new_password">New Password</label>
<input type="password" id="new-password" required minlength="8" autocomplete="new-password">
<small data-i18n="profile.min_8_chars">At least 8 characters</small>
</div>
<div class="form-group">
<label for="confirm-password" data-i18n="profile.confirm_password">Confirm New Password</label>
<input type="password" id="confirm-password" required minlength="8" autocomplete="new-password">
</div>
<button type="submit" class="btn btn-primary" id="pw-submit"><i class="fas fa-save"></i> <span data-i18n="profile.update_password">Update Password</span></button>
<div id="pw-status"></div>
</form>
</div>
</div>
</div>
<script type="module" src="/js/views/profile/profile.js"></script>
</body>
</html>