mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-11 17:30:00 +02:00
feat: implement IPTV settings management with streaming and display preferences
This commit is contained in:
1 parent
62fc19598a
commit
63846c090a
8 files changed
+265
-10
No files matched your search
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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.
Reference in new issue
Block a user