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

186
app/routes/webdav_proxy.py Normal file
View File

@@ -0,0 +1,186 @@
from flask import Blueprint, request, jsonify, send_file
from app.services.auth import require_auth, get_current_user
from app.services.nextcloud import NextcloudClient
from config import Config
from io import BytesIO
from PIL import Image
bp = Blueprint('webdav', __name__, url_prefix='/api/files')
def _get_nc_client():
"""Get Nextcloud client for current user."""
user = get_current_user()
if not user:
return None
return NextcloudClient(Config.NEXTCLOUD_URL, user['username'], user['password'])
@bp.route('/list', methods=['GET'])
@require_auth
def list_files():
"""List files and directories at the given path."""
path = request.args.get('path', '/')
nc_client = _get_nc_client()
if not nc_client:
return jsonify({'error': 'Not authenticated'}), 401
try:
result = nc_client.propfind(path, depth=1)
return jsonify(result), 200
except Exception as e:
return jsonify({'error': f'Failed to list files: {str(e)}'}), 500
@bp.route('/upload', methods=['PUT', 'POST'])
@require_auth
def upload_file():
"""Upload a file to Nextcloud."""
nc_client = _get_nc_client()
if not nc_client:
return jsonify({'error': 'Not authenticated'}), 401
# Get target path from query parameter or form data
target_path = request.args.get('path') or request.form.get('path')
if not target_path:
return jsonify({'error': 'Target path is required'}), 400
# Get file data
if 'file' in request.files:
# Multipart upload
file = request.files['file']
file_data = file.read()
filename = file.filename
else:
# Raw binary upload
file_data = request.get_data()
filename = request.args.get('filename', 'upload.jpg')
if not file_data:
return jsonify({'error': 'No file data provided'}), 400
# Construct full path
full_path = f"{target_path.rstrip('/')}/{filename}"
try:
result = nc_client.put_file(full_path, file_data)
return jsonify({
'success': True,
'path': full_path,
'url': result.get('url', '')
}), 200
except Exception as e:
return jsonify({'error': f'Upload failed: {str(e)}'}), 500
@bp.route('/verify', methods=['HEAD', 'GET'])
@require_auth
def verify_file():
"""Verify that a file exists on Nextcloud."""
path = request.args.get('path')
if not path:
return jsonify({'error': 'Path is required'}), 400
nc_client = _get_nc_client()
if not nc_client:
return jsonify({'error': 'Not authenticated'}), 401
try:
exists = nc_client.head(path)
if exists:
return jsonify({'exists': True}), 200
else:
return jsonify({'exists': False}), 404
except Exception as e:
return jsonify({'error': f'Verification failed: {str(e)}'}), 500
@bp.route('/mkdir', methods=['POST'])
@require_auth
def make_directory():
"""Create a new directory on Nextcloud."""
data = request.get_json()
if not data or 'path' not in data:
return jsonify({'error': 'Path is required'}), 400
path = data['path']
nc_client = _get_nc_client()
if not nc_client:
return jsonify({'error': 'Not authenticated'}), 401
try:
nc_client.mkcol(path)
return jsonify({'success': True, 'path': path}), 201
except Exception as e:
return jsonify({'error': f'Failed to create directory: {str(e)}'}), 500
@bp.route('/rename', methods=['POST'])
@require_auth
def rename_file():
"""Rename or move a file on Nextcloud."""
data = request.get_json()
if not data or 'source' not in data or 'destination' not in data:
return jsonify({'error': 'Source and destination paths are required'}), 400
source = data['source']
destination = data['destination']
nc_client = _get_nc_client()
if not nc_client:
return jsonify({'error': 'Not authenticated'}), 401
try:
nc_client.move(source, destination)
return jsonify({
'success': True,
'source': source,
'destination': destination
}), 200
except Exception as e:
return jsonify({'error': f'Failed to rename file: {str(e)}'}), 500
@bp.route('/thumbnail', methods=['GET'])
@require_auth
def get_thumbnail():
"""Generate and return a thumbnail of an image file."""
path = request.args.get('path')
size = request.args.get('size', '256')
if not path:
return jsonify({'error': 'Path is required'}), 400
try:
size = int(size)
if size < 32 or size > 1024:
size = 256
except ValueError:
size = 256
nc_client = _get_nc_client()
if not nc_client:
return jsonify({'error': 'Not authenticated'}), 401
try:
# Download the file from Nextcloud
file_data = nc_client.get_file(path)
# Open image and create thumbnail
image = Image.open(BytesIO(file_data))
# Convert to RGB if necessary (for PNG with transparency, etc.)
if image.mode not in ('RGB', 'L'):
image = image.convert('RGB')
# Create thumbnail maintaining aspect ratio
image.thumbnail((size, size), Image.Resampling.LANCZOS)
# Save to BytesIO
output = BytesIO()
image.save(output, format='JPEG', quality=85)
output.seek(0)
return send_file(
output,
mimetype='image/jpeg',
as_attachment=False,
download_name=f'thumbnail_{size}.jpg'
)
except Exception as e:
return jsonify({'error': f'Failed to generate thumbnail: {str(e)}'}), 500