Add NextSnap PWA with photo gallery viewer and continuous capture

Offline-first photo capture app for Nextcloud with:
- Camera capture with continuous mode (auto-reopens after each photo)
- File browser with fullscreen image gallery, swipe navigation, and rename
- Upload queue with background sync engine
- Admin panel for Nextcloud user management
- Service worker for offline-first caching (v13)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-07 04:53:13 -06:00
commit cad4118f72
55 changed files with 9038 additions and 0 deletions

208
app/templates/login.html Normal file
View File

@@ -0,0 +1,208 @@
{% extends "base.html" %}
{% block title %}Login - NextSnap{% endblock %}
{% block content %}
<div class="container">
<div class="login-container">
<div class="login-header">
<div class="app-icon">📷</div>
<h1 class="login-title">NextSnap</h1>
<p class="login-subtitle">Offline-first photo capture for Nextcloud</p>
</div>
<form id="login-form" class="login-form">
<div class="form-group">
<label for="username">Nextcloud Username</label>
<input
type="text"
id="username"
name="username"
class="form-input"
placeholder="Enter your username"
autocomplete="username"
autocapitalize="none"
autocorrect="off"
autofocus
required
>
<span class="field-error" id="username-error"></span>
</div>
<div class="form-group">
<label for="password">Password</label>
<input
type="password"
id="password"
name="password"
class="form-input"
placeholder="Enter your password"
autocomplete="current-password"
required
>
<span class="field-error" id="password-error"></span>
</div>
<div id="error-message" class="error-message hidden"></div>
<button type="submit" class="btn btn-primary btn-login" id="login-btn">
<span id="login-btn-text">Login</span>
<span id="login-btn-loading" class="hidden">
<span class="spinner"></span> Logging in...
</span>
</button>
</form>
<div class="login-footer">
<p class="help-text">
<strong>Tip:</strong> Use your Nextcloud credentials to login
</p>
</div>
</div>
</div>
{% endblock %}
{% block extra_css %}
<style>
body {
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
}
.login-container {
max-width: 400px;
margin: 2rem auto;
padding: 2rem;
background: var(--bg-secondary);
border-radius: 16px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.login-header {
text-align: center;
margin-bottom: 2rem;
}
.app-icon {
font-size: 4rem;
margin-bottom: 1rem;
}
.login-title {
font-size: 2rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.login-subtitle {
color: var(--text-secondary);
font-size: 0.9rem;
}
.login-form {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-group label {
font-weight: 600;
font-size: 0.9rem;
}
.form-input {
width: 100%;
padding: 0.75rem;
font-size: 1rem;
border: 2px solid var(--bg-tertiary);
border-radius: 8px;
background: var(--bg-primary);
color: var(--text-primary);
transition: border-color 0.2s;
}
.form-input:focus {
outline: none;
border-color: var(--accent);
}
.form-input.error {
border-color: var(--error);
}
.field-error {
color: var(--error);
font-size: 0.85rem;
min-height: 1.2rem;
}
.error-message {
padding: 0.75rem;
background: var(--error);
color: white;
border-radius: 8px;
font-size: 0.9rem;
}
.btn-login {
margin-top: 0.5rem;
position: relative;
}
.btn-login:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.spinner {
display: inline-block;
width: 14px;
height: 14px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.login-footer {
margin-top: 2rem;
padding-top: 1.5rem;
border-top: 1px solid var(--bg-tertiary);
}
.help-text {
text-align: center;
color: var(--text-secondary);
font-size: 0.85rem;
}
/* Mobile responsiveness */
@media (max-width: 480px) {
.login-container {
margin: 1rem;
padding: 1.5rem;
}
.app-icon {
font-size: 3rem;
}
.login-title {
font-size: 1.75rem;
}
}
</style>
{% endblock %}
{% block extra_js %}
<script src="{{ url_for('static', filename='js/auth.js') }}"></script>
{% endblock %}