feat: implement IPTV settings management with streaming and display preferences

This commit is contained in:
Michael committed 2025-12-08 21:40:46 +01:00
1 parent 62fc19598a
commit 63846c090a
8 files changed
+265 -10

No files matched your search

+62
View File
@@ -0,0 +1,62 @@
import 'dart:convert';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
import '../database/database.dart';
import '../models/user.dart';
class SettingsHandler {
final AppDatabase db;
SettingsHandler(this.db);
Router get router {
final router = Router();
// GET /api/settings
router.get('/', (Request req) {
final user = req.context['user'] as User?;
if (user == null) {
return Response.forbidden(jsonEncode({'error': 'Authentication required'}));
}
final settingsJson = db.getUserSettings(user.id);
// If no settings exist, return empty object or default
return Response.ok(
settingsJson ?? '{}',
headers: {'content-type': 'application/json'},
);
});
// POST /api/settings
router.post('/', (Request req) async {
final user = req.context['user'] as User?;
if (user == null) {
return Response.forbidden(jsonEncode({'error': 'Authentication required'}));
}
try {
final body = await req.readAsString();
// Validate JSON
try {
jsonDecode(body);
} catch (e) {
return Response.badRequest(body: jsonEncode({'error': 'Invalid JSON'}));
}
db.updateUserSettings(user.id, body);
return Response.ok(
body,
headers: {'content-type': 'application/json'},
);
} catch (e) {
return Response.internalServerError(
body: jsonEncode({'error': 'Failed to save settings: $e'}),
);
}
});
return router;
}
}
+37
View File
@@ -68,6 +68,16 @@ class AppDatabase {
)
''');
// User Settings table
_db.execute('''
CREATE TABLE IF NOT EXISTS user_settings (
user_id TEXT PRIMARY KEY,
settings_json TEXT NOT NULL,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
''');
// Indexes
_db.execute('CREATE INDEX IF NOT EXISTS idx_sessions_token ON sessions(token)');
_db.execute('CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at)');
@@ -312,6 +322,33 @@ class AppDatabase {
_db.execute('DELETE FROM playlists WHERE id = ?', [playlistId]);
}
// ==================== User Settings ====================
/// Get user settings as JSON string
String? getUserSettings(String userId) {
final result = _db.select(
'SELECT settings_json FROM user_settings WHERE user_id = ?',
[userId],
);
if (result.isEmpty) return null;
return result.first['settings_json'] as String;
}
/// Update user settings
void updateUserSettings(String userId, String settingsJson) {
final now = DateTime.now().toIso8601String();
// UPSERT (Insert or Replace)
_db.execute('''
INSERT INTO user_settings (user_id, settings_json, updated_at)
VALUES (?, ?, ?)
ON CONFLICT(user_id) DO UPDATE SET
settings_json = excluded.settings_json,
updated_at = excluded.updated_at
''', [userId, settingsJson, now]);
}
/// Close database connection
void close() {
_db.dispose();
+10 -2
View File
@@ -11,7 +11,10 @@ import 'database/database.dart';
import 'models/user.dart';
import 'api/auth_handler.dart';
import 'api/users_handler.dart';
import 'api/auth_handler.dart';
import 'api/users_handler.dart';
import 'api/playlists_handler.dart';
import 'api/settings_handler.dart';
import 'middleware/auth_middleware.dart';
import 'middleware/security_middleware.dart';
import 'services/cleanup_service.dart';
@@ -35,6 +38,7 @@ void main(List<String> args) async {
final authHandler = AuthHandler(db);
final playlistsHandler = PlaylistsHandler(db);
final usersHandler = UsersHandler(db);
final settingsHandler = SettingsHandler(db);
// Setup router
final apiRouter = Router()
@@ -43,11 +47,15 @@ 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))
// Settings endpoints (with auth middleware)
..mount('/api/settings', Pipeline()
.addMiddleware(authMiddleware(db))
.addHandler(settingsHandler.router.call));
// Initialize Cleanup Service
final cleanupService = CleanupService();
@@ -0,0 +1,51 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'dart:html' as html;
import '../models/app_user.dart';
class SettingsApiService {
final String _baseUrl = html.window.location.origin;
Future<Map<String, dynamic>?> getSettings() async {
final token = html.window.localStorage['auth_token'];
if (token == null) return null;
try {
final response = await http.get(
Uri.parse('$_baseUrl/api/settings'),
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
);
if (response.statusCode == 200) {
if (response.body.isEmpty) return {};
return jsonDecode(response.body) as Map<String, dynamic>;
}
return null;
} catch (e) {
return null;
}
}
Future<bool> saveSettings(Map<String, dynamic> settings) async {
final token = html.window.localStorage['auth_token'];
if (token == null) return false;
try {
final response = await http.post(
Uri.parse('$_baseUrl/api/settings'),
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
body: jsonEncode(settings),
);
return response.statusCode == 200;
} catch (e) {
return false;
}
}
}
@@ -1,5 +1,7 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../../core/services/settings_api_service.dart';
import '../../auth/providers/auth_provider.dart';
/// Streaming quality presets for Live TV
enum StreamQuality {
@@ -225,28 +227,29 @@ class IptvSettings {
bool matchesFilter(String categoryName) => matchesLiveTvFilter(categoryName);
}
/// IPTV Settings notifier with SharedPreferences persistence
/// IPTV Settings notifier with SharedPreferences and API persistence
class IptvSettingsNotifier extends StateNotifier<IptvSettings> {
SharedPreferences? _prefs;
final SettingsApiService _apiService = SettingsApiService();
final Ref _ref;
bool _initialized = false;
IptvSettingsNotifier() : super(const IptvSettings()) {
IptvSettingsNotifier(this._ref) : super(const IptvSettings()) {
_loadSettings();
}
/// Load settings from SharedPreferences
/// Load settings from SharedPreferences and then API
Future<void> _loadSettings() async {
if (_initialized) return;
try {
_prefs = await SharedPreferences.getInstance();
// Load filters
// Load local settings first (fastest)
final liveTv = _prefs?.getString(_SettingsKeys.liveTvFilter) ?? '';
final movies = _prefs?.getString(_SettingsKeys.moviesFilter) ?? '';
final series = _prefs?.getString(_SettingsKeys.seriesFilter) ?? '';
// Load streaming settings
final quality = StreamQuality.values[
_prefs?.getInt(_SettingsKeys.streamQuality) ?? StreamQuality.medium.index
];
@@ -265,7 +268,6 @@ class IptvSettingsNotifier extends StateNotifier<IptvSettings> {
];
final directPlay = _prefs?.getBool(_SettingsKeys.preferDirectPlay) ?? false;
// Load Player Display Settings
final showClock = _prefs?.getBool(_SettingsKeys.showClock) ?? false;
final aspectRatio = _prefs?.getString(_SettingsKeys.preferredAspectRatio) ?? 'contain';
@@ -285,18 +287,84 @@ class IptvSettingsNotifier extends StateNotifier<IptvSettings> {
);
_initialized = true;
print('Settings loaded: Clock=$showClock, Ratio=$aspectRatio');
// Sync from API if logged in
final authState = _ref.read(authProvider);
if (authState.isAuthenticated) {
await _fetchFromApi();
}
} catch (e) {
print('Error loading settings: $e');
}
}
Future<void> _fetchFromApi() async {
try {
final remoteSettings = await _apiService.getSettings();
if (remoteSettings != null && remoteSettings.isNotEmpty) {
// Update state with remote settings if they exist
T? getValue<T>(String key) => remoteSettings[key] as T?;
state = state.copyWith(
liveTvCategoryFilter: getValue<String>(_SettingsKeys.liveTvFilter),
moviesCategoryFilter: getValue<String>(_SettingsKeys.moviesFilter),
seriesCategoryFilter: getValue<String>(_SettingsKeys.seriesFilter),
streamQuality: remoteSettings[_SettingsKeys.streamQuality] != null
? StreamQuality.values[remoteSettings[_SettingsKeys.streamQuality] as int] : null,
bufferSize: remoteSettings[_SettingsKeys.bufferSize] != null
? BufferSize.values[remoteSettings[_SettingsKeys.bufferSize] as int] : null,
connectionTimeout: remoteSettings[_SettingsKeys.connectionTimeout] != null
? ConnectionTimeout.values[remoteSettings[_SettingsKeys.connectionTimeout] as int] : null,
autoReconnect: getValue<bool>(_SettingsKeys.autoReconnect),
epgCacheDuration: remoteSettings[_SettingsKeys.epgCacheDuration] != null
? EpgCacheDuration.values[remoteSettings[_SettingsKeys.epgCacheDuration] as int] : null,
transcodingMode: remoteSettings[_SettingsKeys.transcodingMode] != null
? TranscodingMode.values[remoteSettings[_SettingsKeys.transcodingMode] as int] : null,
preferDirectPlay: getValue<bool>(_SettingsKeys.preferDirectPlay),
showClock: getValue<bool>(_SettingsKeys.showClock),
preferredAspectRatio: getValue<String>(_SettingsKeys.preferredAspectRatio),
);
// Save to local prefs to keep in sync
_saveToPrefs();
}
} catch (e) {
print('Error syncing settings from API: $e');
}
}
Future<void> _saveToApi() async {
final authState = _ref.read(authProvider);
if (!authState.isAuthenticated) return;
// Create map from state
final settingsMap = {
_SettingsKeys.liveTvFilter: state.liveTvCategoryFilter,
_SettingsKeys.moviesFilter: state.moviesCategoryFilter,
_SettingsKeys.seriesFilter: state.seriesCategoryFilter,
_SettingsKeys.streamQuality: state.streamQuality.index,
_SettingsKeys.bufferSize: state.bufferSize.index,
_SettingsKeys.connectionTimeout: state.connectionTimeout.index,
_SettingsKeys.autoReconnect: state.autoReconnect,
_SettingsKeys.epgCacheDuration: state.epgCacheDuration.index,
_SettingsKeys.transcodingMode: state.transcodingMode.index,
_SettingsKeys.preferDirectPlay: state.preferDirectPlay,
_SettingsKeys.showClock: state.showClock,
_SettingsKeys.preferredAspectRatio: state.preferredAspectRatio,
};
await _apiService.saveSettings(settingsMap);
}
// ===== Save Helpers =====
Future<void> _saveString(String key, String value) async {
try {
_prefs ??= await SharedPreferences.getInstance();
await _prefs?.setString(key, value);
await _saveToApi();
} catch (e) {
print('Error saving setting $key: $e');
}
@@ -306,6 +374,7 @@ class IptvSettingsNotifier extends StateNotifier<IptvSettings> {
try {
_prefs ??= await SharedPreferences.getInstance();
await _prefs?.setInt(key, value);
await _saveToApi();
} catch (e) {
print('Error saving setting $key: $e');
}
@@ -315,11 +384,31 @@ class IptvSettingsNotifier extends StateNotifier<IptvSettings> {
try {
_prefs ??= await SharedPreferences.getInstance();
await _prefs?.setBool(key, value);
await _saveToApi();
} catch (e) {
print('Error saving setting $key: $e');
}
}
Future<void> _saveToPrefs() async {
// Helper to bulk save state to prefs (after API fetch)
_prefs ??= await SharedPreferences.getInstance();
await _prefs?.setString(_SettingsKeys.liveTvFilter, state.liveTvCategoryFilter);
await _prefs?.setString(_SettingsKeys.moviesFilter, state.moviesCategoryFilter);
await _prefs?.setString(_SettingsKeys.seriesFilter, state.seriesCategoryFilter);
await _prefs?.setInt(_SettingsKeys.streamQuality, state.streamQuality.index);
await _prefs?.setInt(_SettingsKeys.bufferSize, state.bufferSize.index);
await _prefs?.setInt(_SettingsKeys.connectionTimeout, state.connectionTimeout.index);
await _prefs?.setBool(_SettingsKeys.autoReconnect, state.autoReconnect);
await _prefs?.setInt(_SettingsKeys.epgCacheDuration, state.epgCacheDuration.index);
await _prefs?.setInt(_SettingsKeys.transcodingMode, state.transcodingMode.index);
await _prefs?.setBool(_SettingsKeys.preferDirectPlay, state.preferDirectPlay);
await _prefs?.setBool(_SettingsKeys.showClock, state.showClock);
await _prefs?.setString(_SettingsKeys.preferredAspectRatio, state.preferredAspectRatio);
}
// ===== Filter Setters =====
void setLiveTvFilter(String filter) {
@@ -404,5 +493,13 @@ class IptvSettingsNotifier extends StateNotifier<IptvSettings> {
/// Provider for IPTV settings
final iptvSettingsProvider =
StateNotifierProvider<IptvSettingsNotifier, IptvSettings>((ref) {
return IptvSettingsNotifier();
// Watch auth provider to re-fetch settings on login/logout
final authState = ref.watch(authProvider);
final notifier = IptvSettingsNotifier(ref);
// Note: Since we watch authProvider, this provider will be disposed and recreated
// whenever authState changes. This automatically handles fetching settings for
// the new user on creation.
return notifier;
});
Binary file not shown.
Binary file not shown.
Binary file not shown.