feat: Initialize XtremFlow project with core structure, IPTV features, authentication, and video player.

This commit is contained in:
Michael committed 2025-12-05 09:17:11 +01:00
commit 2a28eb44d8
28 files changed
+3279

No files matched your search

+119
View File
@@ -0,0 +1,119 @@
import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:uuid/uuid.dart';
import '../models/app_user.dart';
import '../models/playlist_config.dart';
class HiveService {
static const String _usersBoxName = 'users';
static const String _playlistsBoxName = 'playlists';
static const String _encryptionKeyName = 'hive_encryption_key';
static const _storage = FlutterSecureStorage();
static bool _initialized = false;
/// Initialize Hive for Web with encryption
static Future<void> init() async {
if (_initialized) return;
// Initialize Hive for Web (uses IndexedDB)
await Hive.initFlutter();
// Generate or retrieve encryption key
final encryptionKey = await _getOrCreateEncryptionKey();
// Register adapters
Hive.registerAdapter(AppUserAdapter());
Hive.registerAdapter(PlaylistConfigAdapter());
// Open encrypted boxes
await Hive.openBox<AppUser>(
_usersBoxName,
encryptionCipher: HiveAesCipher(encryptionKey),
);
await Hive.openBox<PlaylistConfig>(
_playlistsBoxName,
encryptionCipher: HiveAesCipher(encryptionKey),
);
// Seed default admin if no users exist
await _seedDefaultAdmin();
_initialized = true;
}
/// Get or create 256-bit encryption key
static Future<List<int>> _getOrCreateEncryptionKey() async {
try {
final existingKey = await _storage.read(key: _encryptionKeyName);
if (existingKey != null) {
return base64Decode(existingKey);
}
} catch (e) {
// Storage might not be available in some web contexts
// Fall back to session-based key (not persisted)
}
// Generate new 256-bit key
final key = Hive.generateSecureKey();
try {
await _storage.write(
key: _encryptionKeyName,
value: base64Encode(key),
);
} catch (e) {
// Ignore if storage fails - key will be regenerated next session
}
return key;
}
/// Seed default admin user (admin/admin)
static Future<void> _seedDefaultAdmin() async {
final usersBox = Hive.box<AppUser>(_usersBoxName);
if (usersBox.isEmpty) {
final adminId = const Uuid().v4();
final passwordHash = _hashPassword('admin');
final admin = AppUser(
id: adminId,
username: 'admin',
passwordHash: passwordHash,
isAdmin: true,
assignedPlaylistIds: [],
createdAt: DateTime.now(),
);
await usersBox.put(adminId, admin);
}
}
/// Hash password using SHA-256
static String _hashPassword(String password) {
final bytes = utf8.encode(password);
final digest = sha256.convert(bytes);
return digest.toString();
}
/// Public method to hash passwords (used by auth)
static String hashPassword(String password) => _hashPassword(password);
/// Get users box
static Box<AppUser> get usersBox => Hive.box<AppUser>(_usersBoxName);
/// Get playlists box
static Box<PlaylistConfig> get playlistsBox =>
Hive.box<PlaylistConfig>(_playlistsBoxName);
/// Close all boxes (cleanup)
static Future<void> dispose() async {
await Hive.close();
_initialized = false;
}
}
+51
View File
@@ -0,0 +1,51 @@
import 'package:hive/hive.dart';
part 'app_user.g.dart';
@HiveType(typeId: 0)
class AppUser {
@HiveField(0)
final String id;
@HiveField(1)
final String username;
@HiveField(2)
final String passwordHash; // SHA-256 hash
@HiveField(3)
final bool isAdmin;
@HiveField(4)
final List<String> assignedPlaylistIds;
@HiveField(5)
final DateTime createdAt;
const AppUser({
required this.id,
required this.username,
required this.passwordHash,
required this.isAdmin,
required this.assignedPlaylistIds,
required this.createdAt,
});
AppUser copyWith({
String? id,
String? username,
String? passwordHash,
bool? isAdmin,
List<String>? assignedPlaylistIds,
DateTime? createdAt,
}) {
return AppUser(
id: id ?? this.id,
username: username ?? this.username,
passwordHash: passwordHash ?? this.passwordHash,
isAdmin: isAdmin ?? this.isAdmin,
assignedPlaylistIds: assignedPlaylistIds ?? this.assignedPlaylistIds,
createdAt: createdAt ?? this.createdAt,
);
}
}
+56
View File
@@ -0,0 +1,56 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'app_user.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class AppUserAdapter extends TypeAdapter<AppUser> {
@override
final int typeId = 0;
@override
AppUser read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return AppUser(
id: fields[0] as String,
username: fields[1] as String,
passwordHash: fields[2] as String,
isAdmin: fields[3] as bool,
assignedPlaylistIds: (fields[4] as List).cast<String>(),
createdAt: fields[5] as DateTime,
);
}
@override
void write(BinaryWriter writer, AppUser obj) {
writer
..writeByte(6)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.username)
..writeByte(2)
..write(obj.passwordHash)
..writeByte(3)
..write(obj.isAdmin)
..writeByte(4)
..write(obj.assignedPlaylistIds)
..writeByte(5)
..write(obj.createdAt);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is AppUserAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
+60
View File
@@ -0,0 +1,60 @@
import 'package:hive/hive.dart';
part 'playlist_config.g.dart';
@HiveType(typeId: 1)
class PlaylistConfig {
@HiveField(0)
final String id;
@HiveField(1)
final String name;
@HiveField(2)
final String dns; // Xtream server URL (without trailing slash)
@HiveField(3)
final String username;
@HiveField(4)
final String password; // Will be encrypted by Hive
@HiveField(5)
final DateTime createdAt;
@HiveField(6)
final bool isActive;
const PlaylistConfig({
required this.id,
required this.name,
required this.dns,
required this.username,
required this.password,
required this.createdAt,
this.isActive = true,
});
PlaylistConfig copyWith({
String? id,
String? name,
String? dns,
String? username,
String? password,
DateTime? createdAt,
bool? isActive,
}) {
return PlaylistConfig(
id: id ?? this.id,
name: name ?? this.name,
dns: dns ?? this.dns,
username: username ?? this.username,
password: password ?? this.password,
createdAt: createdAt ?? this.createdAt,
isActive: isActive ?? this.isActive,
);
}
/// Generate Xtream API base URL
String get apiBaseUrl => '$dns/player_api.php';
}
+59
View File
@@ -0,0 +1,59 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'playlist_config.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class PlaylistConfigAdapter extends TypeAdapter<PlaylistConfig> {
@override
final int typeId = 1;
@override
PlaylistConfig read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return PlaylistConfig(
id: fields[0] as String,
name: fields[1] as String,
dns: fields[2] as String,
username: fields[3] as String,
password: fields[4] as String,
createdAt: fields[5] as DateTime,
isActive: fields[6] as bool,
);
}
@override
void write(BinaryWriter writer, PlaylistConfig obj) {
writer
..writeByte(7)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.name)
..writeByte(2)
..write(obj.dns)
..writeByte(3)
..write(obj.username)
..writeByte(4)
..write(obj.password)
..writeByte(5)
..write(obj.createdAt)
..writeByte(6)
..write(obj.isActive);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is PlaylistConfigAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
+69
View File
@@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../features/auth/providers/auth_provider.dart';
import '../../features/auth/screens/login_screen.dart';
import '../../features/iptv/screens/playlist_selection_screen.dart';
import '../../features/iptv/screens/dashboard_screen.dart';
import '../../features/admin/screens/admin_panel.dart';
import '../models/playlist_config.dart';
final routerProvider = Provider<GoRouter>((ref) {
final authNotifier = ref.watch(authProvider.notifier);
final authState = ref.watch(authProvider);
return GoRouter(
initialLocation: '/login',
redirect: (context, state) {
final isLoggedIn = authState.isAuthenticated;
final isLoginRoute = state.matchedLocation == '/login';
// Redirect to login if not authenticated
if (!isLoggedIn && !isLoginRoute) {
return '/login';
}
// Redirect to playlists if already logged in and trying to access login
if (isLoggedIn && isLoginRoute) {
return '/playlists';
}
// Admin guard for /admin route
if (state.matchedLocation == '/admin' && !authState.isAdmin) {
return '/playlists';
}
return null;
},
routes: [
GoRoute(
path: '/login',
builder: (context, state) => const LoginScreen(),
),
GoRoute(
path: '/playlists',
builder: (context, state) => const PlaylistSelectionScreen(),
),
GoRoute(
path: '/dashboard',
builder: (context, state) {
final playlist = state.extra as PlaylistConfig?;
if (playlist == null) {
// Redirect back if no playlist provided
WidgetsBinding.instance.addPostFrameCallback((_) {
context.go('/playlists');
});
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
return DashboardScreen(playlist: playlist);
},
),
GoRoute(
path: '/admin',
builder: (context, state) => const AdminPanel(),
),
],
);
});