feat: add new UI tabs for movies and series, including mobile-specific versions with search and pagination.

This commit is contained in:
Michael committed 2025-12-08 20:43:41 +01:00
1 parent d48d25f65e
commit 6a71f58a36
29 files changed
+78 -78

No files matched your search

+9 -9
View File
@@ -29,7 +29,7 @@ class AuthHandler {
return Response(400, body: jsonEncode({
'success': false,
'error': 'Username and password are required',
}), headers: {'Content-Type': 'application/json'});
}), headers: {'Content-Type': 'application/json'},);
}
// Verify credentials
@@ -38,7 +38,7 @@ class AuthHandler {
return Response(401, body: jsonEncode({
'success': false,
'error': 'Invalid credentials',
}), headers: {'Content-Type': 'application/json'});
}), headers: {'Content-Type': 'application/json'},);
}
// Create session
@@ -48,7 +48,7 @@ class AuthHandler {
'success': true,
'user': user.toJson(),
'token': session.token,
}), headers: {'Content-Type': 'application/json'});
}), headers: {'Content-Type': 'application/json'},);
} catch (e) {
return Response.internalServerError(
body: jsonEncode({'success': false, 'error': e.toString()}),
@@ -65,14 +65,14 @@ class AuthHandler {
return Response(401, body: jsonEncode({
'success': false,
'error': 'Unauthorized',
}), headers: {'Content-Type': 'application/json'});
}), headers: {'Content-Type': 'application/json'},);
}
db.deleteSession(token);
return Response.ok(jsonEncode({
'success': true,
}), headers: {'Content-Type': 'application/json'});
}), headers: {'Content-Type': 'application/json'},);
} catch (e) {
return Response.internalServerError(
body: jsonEncode({'success': false, 'error': e.toString()}),
@@ -89,7 +89,7 @@ class AuthHandler {
return Response(401, body: jsonEncode({
'success': false,
'error': 'Unauthorized',
}), headers: {'Content-Type': 'application/json'});
}), headers: {'Content-Type': 'application/json'},);
}
final session = db.findSessionByToken(token);
@@ -97,7 +97,7 @@ class AuthHandler {
return Response(401, body: jsonEncode({
'success': false,
'error': 'Invalid or expired session',
}), headers: {'Content-Type': 'application/json'});
}), headers: {'Content-Type': 'application/json'},);
}
final user = db.findUserById(session.userId);
@@ -105,12 +105,12 @@ class AuthHandler {
return Response(404, body: jsonEncode({
'success': false,
'error': 'User not found',
}), headers: {'Content-Type': 'application/json'});
}), headers: {'Content-Type': 'application/json'},);
}
return Response.ok(jsonEncode({
'user': user.toJson(),
}), headers: {'Content-Type': 'application/json'});
}), headers: {'Content-Type': 'application/json'},);
} catch (e) {
return Response.internalServerError(
body: jsonEncode({'success': false, 'error': e.toString()}),
+13 -13
View File
@@ -27,14 +27,14 @@ class PlaylistsHandler {
return Response(401, body: jsonEncode({
'success': false,
'error': 'Unauthorized',
}), headers: {'Content-Type': 'application/json'});
}), headers: {'Content-Type': 'application/json'},);
}
final playlists = db.getPlaylists(userId);
return Response.ok(jsonEncode({
'playlists': playlists.map((p) => p.toJson()).toList(),
}), headers: {'Content-Type': 'application/json'});
}), headers: {'Content-Type': 'application/json'},);
} catch (e) {
return Response.internalServerError(
body: jsonEncode({'success': false, 'error': e.toString()}),
@@ -51,7 +51,7 @@ class PlaylistsHandler {
return Response(401, body: jsonEncode({
'success': false,
'error': 'Unauthorized',
}), headers: {'Content-Type': 'application/json'});
}), headers: {'Content-Type': 'application/json'},);
}
final payload = jsonDecode(await request.readAsString()) as Map<String, dynamic>;
@@ -65,7 +65,7 @@ class PlaylistsHandler {
return Response(400, body: jsonEncode({
'success': false,
'error': 'Missing required fields',
}), headers: {'Content-Type': 'application/json'});
}), headers: {'Content-Type': 'application/json'},);
}
final playlist = db.createPlaylist(
@@ -80,7 +80,7 @@ class PlaylistsHandler {
return Response.ok(jsonEncode({
'success': true,
'playlist': playlist.toJson(),
}), headers: {'Content-Type': 'application/json'});
}), headers: {'Content-Type': 'application/json'},);
} catch (e) {
return Response.internalServerError(
body: jsonEncode({'success': false, 'error': e.toString()}),
@@ -97,7 +97,7 @@ class PlaylistsHandler {
return Response(401, body: jsonEncode({
'success': false,
'error': 'Unauthorized',
}), headers: {'Content-Type': 'application/json'});
}), headers: {'Content-Type': 'application/json'},);
}
// Verify playlist belongs to user
@@ -106,14 +106,14 @@ class PlaylistsHandler {
return Response(404, body: jsonEncode({
'success': false,
'error': 'Playlist not found',
}), headers: {'Content-Type': 'application/json'});
}), headers: {'Content-Type': 'application/json'},);
}
if (existing.userId != userId) {
return Response(403, body: jsonEncode({
'success': false,
'error': 'Forbidden',
}), headers: {'Content-Type': 'application/json'});
}), headers: {'Content-Type': 'application/json'},);
}
final payload = jsonDecode(await request.readAsString()) as Map<String, dynamic>;
@@ -135,7 +135,7 @@ class PlaylistsHandler {
return Response.ok(jsonEncode({
'success': true,
'playlist': playlist.toJson(),
}), headers: {'Content-Type': 'application/json'});
}), headers: {'Content-Type': 'application/json'},);
} catch (e) {
return Response.internalServerError(
body: jsonEncode({'success': false, 'error': e.toString()}),
@@ -152,7 +152,7 @@ class PlaylistsHandler {
return Response(401, body: jsonEncode({
'success': false,
'error': 'Unauthorized',
}), headers: {'Content-Type': 'application/json'});
}), headers: {'Content-Type': 'application/json'},);
}
// Verify playlist belongs to user
@@ -161,21 +161,21 @@ class PlaylistsHandler {
return Response(404,body: jsonEncode({
'success': false,
'error': 'Playlist not found',
}), headers: {'Content-Type': 'application/json'});
}), headers: {'Content-Type': 'application/json'},);
}
if (existing.userId != userId) {
return Response(403, body: jsonEncode({
'success': false,
'error': 'Forbidden',
}), headers: {'Content-Type': 'application/json'});
}), headers: {'Content-Type': 'application/json'},);
}
db.deletePlaylist(id);
return Response.ok(jsonEncode({
'success': true,
}), headers: {'Content-Type': 'application/json'});
}), headers: {'Content-Type': 'application/json'},);
} catch (e) {
return Response.internalServerError(
body: jsonEncode({'success': false, 'error': e.toString()}),
+6 -6
View File
@@ -37,7 +37,7 @@ class UsersHandler {
return Response.ok(jsonEncode({
'success': true,
'users': users.map((u) => u.toJson()).toList(),
}), headers: {'Content-Type': 'application/json'});
}), headers: {'Content-Type': 'application/json'},);
} catch (e) {
return Response.internalServerError(
body: jsonEncode({'success': false, 'error': e.toString()}),
@@ -62,21 +62,21 @@ class UsersHandler {
return Response.badRequest(body: jsonEncode({
'success': false,
'error': 'Missing required fields',
}), headers: {'Content-Type': 'application/json'});
}), headers: {'Content-Type': 'application/json'},);
}
if (db.findUserByUsername(username) != null) {
return Response.badRequest(body: jsonEncode({
'success': false,
'error': 'Username already exists',
}), headers: {'Content-Type': 'application/json'});
}), headers: {'Content-Type': 'application/json'},);
}
final user = db.createUser(username, password, isAdmin: isAdmin);
return Response.ok(jsonEncode({
'success': true,
'user': user.toJson(),
}), headers: {'Content-Type': 'application/json'});
}), headers: {'Content-Type': 'application/json'},);
} catch (e) {
return Response.internalServerError(
@@ -100,7 +100,7 @@ class UsersHandler {
return Response.badRequest(body: jsonEncode({
'success': false,
'error': 'Missing password',
}), headers: {'Content-Type': 'application/json'});
}), headers: {'Content-Type': 'application/json'},);
}
db.updateUserPassword(id, password);
@@ -151,7 +151,7 @@ class UsersHandler {
return Response.badRequest(body: jsonEncode({
'success': false,
'error': 'Cannot delete yourself',
}), headers: {'Content-Type': 'application/json'});
}), headers: {'Content-Type': 'application/json'},);
}
db.deleteUser(id);
+5 -5
View File
@@ -86,7 +86,7 @@ class AppDatabase {
_db.execute('''
INSERT INTO users (id, username, password_hash, is_admin)
VALUES (?, ?, ?, 1)
''', [adminId, 'admin', passwordHash]);
''', [adminId, 'admin', passwordHash],);
print('Default admin user created (username: admin, password: admin)');
}
@@ -141,7 +141,7 @@ class AppDatabase {
_db.execute('''
INSERT INTO users (id, username, password_hash, is_admin)
VALUES (?, ?, ?, ?)
''', [userId, username, passwordHash, isAdmin ? 1 : 0]);
''', [userId, username, passwordHash, isAdmin ? 1 : 0],);
return User(
id: userId,
@@ -190,7 +190,7 @@ class AppDatabase {
_db.execute('''
INSERT INTO sessions (id, user_id, token, expires_at)
VALUES (?, ?, ?, ?)
''', [sessionId, userId, token, expiresAt.toIso8601String()]);
''', [sessionId, userId, token, expiresAt.toIso8601String()],);
return models.Session(
id: sessionId,
@@ -272,7 +272,7 @@ class AppDatabase {
_db.execute('''
INSERT INTO playlists (id, user_id, name, server_url, username, password, dns, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
''', [playlistId, userId, name, serverUrl, username, password, dns, now, now]);
''', [playlistId, userId, name, serverUrl, username, password, dns, now, now],);
return Playlist(
id: playlistId,
@@ -302,7 +302,7 @@ class AppDatabase {
UPDATE playlists
SET name = ?, server_url = ?, username = ?, password = ?, dns = ?, updated_at = ?
WHERE id = ?
''', [name, serverUrl, username, password, dns, now, playlistId]);
''', [name, serverUrl, username, password, dns, now, playlistId],);
return getPlaylistById(playlistId)!;
}
+1 -1
View File
@@ -26,7 +26,7 @@ Middleware authMiddleware(AppDatabase db) {
final updatedRequest = request.change(context: {
...request.context,
'userId': session.userId,
});
},);
return handler(updatedRequest);
};
+2 -2
View File
@@ -23,7 +23,7 @@ Middleware securityHeadersMiddleware() {
'Referrer-Policy': 'strict-origin-when-cross-origin',
// Note: CSP is tricky with Flutter Web (requires 'unsafe-eval' for Dart),
// so we omit it here to avoid breaking the app, or use a permissive one.
});
},);
};
};
}
@@ -42,7 +42,7 @@ Middleware honeypotMiddleware() {
'/config.php',
'/api/.env',
'/console',
'/actuator/health'
'/actuator/health',
];
return (Handler handler) {
+7 -7
View File
@@ -43,11 +43,11 @@ void main(List<String> args) async {
// Playlists endpoints (with auth middleware)
..mount('/api/playlists', Pipeline()
.addMiddleware(authMiddleware(db))
.addHandler(playlistsHandler.router.call))
.addHandler(playlistsHandler.router.call),)
// Users endpoints (with auth middleware)
..mount('/api/users', Pipeline()
.addMiddleware(authMiddleware(db))
.addHandler(usersHandler.router.call));
.addHandler(usersHandler.router.call),);
// Initialize Cleanup Service
final cleanupService = CleanupService();
@@ -75,12 +75,12 @@ void main(List<String> args) async {
final result = await cleanupService.runCleanup();
return Response.ok(
jsonEncode(result),
headers: {'content-type': 'application/json'}
headers: {'content-type': 'application/json'},
);
});
return router(request);
}));
}),);
// Create handlers
final staticHandler = createStaticHandler(
@@ -532,14 +532,14 @@ Handler _createStreamHandler() {
'-vf', 'scale=-2:480',
'-b:v', '800k',
'-maxrate', '800k',
'-bufsize', '1600k'
'-bufsize', '1600k',
]);
} else {
// Medium: Original res (or cap at 720p?), 2500k
ffmpegArgs.addAll([
'-b:v', '2500k',
'-maxrate', '2500k',
'-bufsize', '5000k'
'-bufsize', '5000k',
]);
}
}
@@ -556,7 +556,7 @@ Handler _createStreamHandler() {
'-fflags', '+genpts',
// Output to stdout pipe
'pipe:1'
'pipe:1',
]);
// Start the process
+1 -1
View File
@@ -66,7 +66,7 @@ class CleanupService {
'deleted_files': deletedCount,
'freed_bytes': freedBytes,
'timestamp': DateTime.now().toIso8601String(),
'errors': errors
'errors': errors,
};
print('CleanupService: Cleanup complete. Deleted $deletedCount files, freed ${(freedBytes / 1024 / 1024).toStringAsFixed(2)} MB');