mirror of
https://github.com/R0m1k3/xtremobile.git
synced 2026-10-21 22:28:16 +02:00
feat: add XtreamServiceMobile for Xtream Codes API communication with caching and DNS resolution for mobile.
This commit is contained in:
1 parent
80320d0eae
commit
5dd83019d3
8 files changed
+107
-177
No files matched your search
@@ -20,8 +20,13 @@ class HiveService {
|
|||||||
await Hive.initFlutter();
|
await Hive.initFlutter();
|
||||||
|
|
||||||
// Register adapters
|
// Register adapters
|
||||||
Hive.registerAdapter(AppUserAdapter());
|
// Register adapters
|
||||||
Hive.registerAdapter(PlaylistConfigAdapter());
|
if (!Hive.isAdapterRegistered(0)) {
|
||||||
|
Hive.registerAdapter(AppUserAdapter());
|
||||||
|
}
|
||||||
|
if (!Hive.isAdapterRegistered(1)) {
|
||||||
|
Hive.registerAdapter(PlaylistConfigAdapter());
|
||||||
|
}
|
||||||
|
|
||||||
// Open boxes WITHOUT encryption on Web
|
// Open boxes WITHOUT encryption on Web
|
||||||
// Note: On Web, IndexedDB is already isolated by origin,
|
// Note: On Web, IndexedDB is already isolated by origin,
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ class PlaylistApiService {
|
|||||||
/// Get all playlists for current user
|
/// Get all playlists for current user
|
||||||
Future<List<PlaylistConfig>> getPlaylists() async {
|
Future<List<PlaylistConfig>> getPlaylists() async {
|
||||||
try {
|
try {
|
||||||
if (!HiveService.playlistsBox.isOpen) {
|
await HiveService.init();
|
||||||
await HiveService.init();
|
|
||||||
}
|
|
||||||
return HiveService.playlistsBox.values.toList();
|
return HiveService.playlistsBox.values.toList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error fetching playlists from Hive: $e');
|
print('Error fetching playlists from Hive: $e');
|
||||||
@@ -25,6 +23,7 @@ class PlaylistApiService {
|
|||||||
required String password,
|
required String password,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
|
await HiveService.init();
|
||||||
final id = const Uuid().v4();
|
final id = const Uuid().v4();
|
||||||
final playlist = PlaylistConfig(
|
final playlist = PlaylistConfig(
|
||||||
id: id,
|
id: id,
|
||||||
@@ -52,6 +51,7 @@ class PlaylistApiService {
|
|||||||
required String password,
|
required String password,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
|
await HiveService.init();
|
||||||
if (!HiveService.playlistsBox.containsKey(id)) {
|
if (!HiveService.playlistsBox.containsKey(id)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -79,6 +79,7 @@ class PlaylistApiService {
|
|||||||
/// Delete a playlist
|
/// Delete a playlist
|
||||||
Future<bool> deletePlaylist(String id) async {
|
Future<bool> deletePlaylist(String id) async {
|
||||||
try {
|
try {
|
||||||
|
await HiveService.init();
|
||||||
await HiveService.playlistsBox.delete(id);
|
await HiveService.playlistsBox.delete(id);
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -241,20 +241,25 @@ class XtreamServiceMobile {
|
|||||||
final Map<String, List<Channel>> groupedChannels = {};
|
final Map<String, List<Channel>> groupedChannels = {};
|
||||||
|
|
||||||
for (final streamData in streams) {
|
for (final streamData in streams) {
|
||||||
final data = streamData as Map<String, dynamic>;
|
try {
|
||||||
// Get category name from our mapping using category_id
|
final data = streamData as Map<String, dynamic>;
|
||||||
final categoryId = data['category_id']?.toString() ?? '';
|
// Get category name from our mapping using category_id
|
||||||
final categoryName = categoryMap[categoryId] ?? 'Uncategorized';
|
final categoryId = data['category_id']?.toString() ?? '';
|
||||||
|
final categoryName = categoryMap[categoryId] ?? 'Uncategorized';
|
||||||
|
|
||||||
// Inject category_name into data before parsing
|
// Inject category_name into data before parsing
|
||||||
data['category_name'] = categoryName;
|
data['category_name'] = categoryName;
|
||||||
|
|
||||||
final channel = Channel.fromJson(data);
|
final channel = Channel.fromJson(data);
|
||||||
|
|
||||||
if (!groupedChannels.containsKey(categoryName)) {
|
if (!groupedChannels.containsKey(categoryName)) {
|
||||||
groupedChannels[categoryName] = [];
|
groupedChannels[categoryName] = [];
|
||||||
|
}
|
||||||
|
groupedChannels[categoryName]!.add(channel);
|
||||||
|
} catch (e) {
|
||||||
|
// Skip malformed channel
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
groupedChannels[categoryName]!.add(channel);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return groupedChannels;
|
return groupedChannels;
|
||||||
@@ -371,13 +376,19 @@ class XtreamServiceMobile {
|
|||||||
|
|
||||||
final paginatedMovies = allMovies.sublist(offset, endIndex);
|
final paginatedMovies = allMovies.sublist(offset, endIndex);
|
||||||
|
|
||||||
return paginatedMovies.map((movieData) {
|
final List<xm.Movie> movies = [];
|
||||||
final data =
|
for (final movieData in paginatedMovies) {
|
||||||
Map<String, dynamic>.from(movieData as Map<String, dynamic>);
|
try {
|
||||||
final categoryId = data['category_id']?.toString() ?? '';
|
final data =
|
||||||
data['category_name'] = categoryMap[categoryId] ?? 'Uncategorized';
|
Map<String, dynamic>.from(movieData as Map<String, dynamic>);
|
||||||
return xm.Movie.fromJson(data);
|
final categoryId = data['category_id']?.toString() ?? '';
|
||||||
}).toList();
|
data['category_name'] = categoryMap[categoryId] ?? 'Uncategorized';
|
||||||
|
movies.add(xm.Movie.fromJson(data));
|
||||||
|
} catch (e) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return movies;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw Exception('Failed to fetch movies: $e');
|
throw Exception('Failed to fetch movies: $e');
|
||||||
}
|
}
|
||||||
@@ -403,14 +414,20 @@ class XtreamServiceMobile {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (response.data is! List) return [];
|
if (response.data is! List) return [];
|
||||||
final List<dynamic> movies = response.data as List<dynamic>;
|
final List<dynamic> moviesRaw = response.data as List<dynamic>;
|
||||||
|
|
||||||
return movies.map((movieData) {
|
final List<xm.Movie> movies = [];
|
||||||
final data =
|
for (final movieData in moviesRaw) {
|
||||||
Map<String, dynamic>.from(movieData as Map<String, dynamic>);
|
try {
|
||||||
data['category_name'] = categoryName;
|
final data =
|
||||||
return xm.Movie.fromJson(data);
|
Map<String, dynamic>.from(movieData as Map<String, dynamic>);
|
||||||
}).toList();
|
data['category_name'] = categoryName;
|
||||||
|
movies.add(xm.Movie.fromJson(data));
|
||||||
|
} catch (e) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return movies;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Return empty list instead of throwing to prevent UI crash
|
// Return empty list instead of throwing to prevent UI crash
|
||||||
return [];
|
return [];
|
||||||
@@ -440,19 +457,21 @@ class XtreamServiceMobile {
|
|||||||
final queryLower = query.toLowerCase();
|
final queryLower = query.toLowerCase();
|
||||||
|
|
||||||
// Filter by search query
|
// Filter by search query
|
||||||
return allMovies
|
final List<xm.Movie> results = [];
|
||||||
.where((m) {
|
for (final movieData in allMovies.where((m) {
|
||||||
final name = (m['name']?.toString() ?? '').toLowerCase();
|
final name = (m['name']?.toString() ?? '').toLowerCase();
|
||||||
return name.contains(queryLower);
|
return name.contains(queryLower);
|
||||||
})
|
}).take(100)) {
|
||||||
.take(100) // Limit results
|
try {
|
||||||
.map((movieData) {
|
final data = movieData as Map<String, dynamic>;
|
||||||
final data = movieData as Map<String, dynamic>;
|
final categoryId = data['category_id']?.toString() ?? '';
|
||||||
final categoryId = data['category_id']?.toString() ?? '';
|
data['category_name'] = categoryMap[categoryId] ?? 'Uncategorized';
|
||||||
data['category_name'] = categoryMap[categoryId] ?? 'Uncategorized';
|
results.add(xm.Movie.fromJson(data));
|
||||||
return xm.Movie.fromJson(data);
|
} catch (e) {
|
||||||
})
|
continue;
|
||||||
.toList();
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -502,13 +521,19 @@ class XtreamServiceMobile {
|
|||||||
|
|
||||||
final paginatedSeries = allSeries.sublist(offset, endIndex);
|
final paginatedSeries = allSeries.sublist(offset, endIndex);
|
||||||
|
|
||||||
return paginatedSeries.map((seriesData) {
|
final List<xm.Series> seriesList = [];
|
||||||
final data =
|
for (final seriesData in paginatedSeries) {
|
||||||
Map<String, dynamic>.from(seriesData as Map<String, dynamic>);
|
try {
|
||||||
final categoryId = data['category_id']?.toString() ?? '';
|
final data =
|
||||||
data['category_name'] = categoryMap[categoryId] ?? 'Uncategorized';
|
Map<String, dynamic>.from(seriesData as Map<String, dynamic>);
|
||||||
return xm.Series.fromJson(data);
|
final categoryId = data['category_id']?.toString() ?? '';
|
||||||
}).toList();
|
data['category_name'] = categoryMap[categoryId] ?? 'Uncategorized';
|
||||||
|
seriesList.add(xm.Series.fromJson(data));
|
||||||
|
} catch (e) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return seriesList;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw Exception('Failed to fetch series: $e');
|
throw Exception('Failed to fetch series: $e');
|
||||||
}
|
}
|
||||||
@@ -537,19 +562,21 @@ class XtreamServiceMobile {
|
|||||||
final queryLower = query.toLowerCase();
|
final queryLower = query.toLowerCase();
|
||||||
|
|
||||||
// Filter by search query
|
// Filter by search query
|
||||||
return allSeries
|
final List<xm.Series> results = [];
|
||||||
.where((s) {
|
for (final seriesData in allSeries.where((s) {
|
||||||
final name = (s['name']?.toString() ?? '').toLowerCase();
|
final name = (s['name']?.toString() ?? '').toLowerCase();
|
||||||
return name.contains(queryLower);
|
return name.contains(queryLower);
|
||||||
})
|
}).take(100)) {
|
||||||
.take(100) // Limit results
|
try {
|
||||||
.map((seriesData) {
|
final data = seriesData as Map<String, dynamic>;
|
||||||
final data = seriesData as Map<String, dynamic>;
|
final categoryId = data['category_id']?.toString() ?? '';
|
||||||
final categoryId = data['category_id']?.toString() ?? '';
|
data['category_name'] = categoryMap[categoryId] ?? 'Uncategorized';
|
||||||
data['category_name'] = categoryMap[categoryId] ?? 'Uncategorized';
|
results.add(xm.Series.fromJson(data));
|
||||||
return xm.Series.fromJson(data);
|
} catch (e) {
|
||||||
})
|
continue;
|
||||||
.toList();
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-13
@@ -7,6 +7,7 @@ import 'core/providers/theme_provider.dart';
|
|||||||
import 'mobile/features/iptv/screens/mobile_playlist_screen.dart';
|
import 'mobile/features/iptv/screens/mobile_playlist_screen.dart';
|
||||||
import 'mobile/features/iptv/screens/mobile_dashboard_screen.dart';
|
import 'mobile/features/iptv/screens/mobile_dashboard_screen.dart';
|
||||||
import 'core/models/playlist_config.dart';
|
import 'core/models/playlist_config.dart';
|
||||||
|
import 'core/database/hive_service.dart';
|
||||||
|
|
||||||
/// Mobile-optimized entry point for XtremFlow IPTV
|
/// Mobile-optimized entry point for XtremFlow IPTV
|
||||||
/// No authentication - direct access to playlist selection
|
/// No authentication - direct access to playlist selection
|
||||||
@@ -16,29 +17,18 @@ void main() async {
|
|||||||
// Initialize MediaKit (required for video playback)
|
// Initialize MediaKit (required for video playback)
|
||||||
MediaKit.ensureInitialized();
|
MediaKit.ensureInitialized();
|
||||||
|
|
||||||
// Initialize Hive for local storage
|
// Initialize Hive (Centralized)
|
||||||
await Hive.initFlutter();
|
await HiveService.init();
|
||||||
|
|
||||||
// CLEAR CACHE ON STARTUP (Requested by User)
|
// CLEAR CACHE ON STARTUP (Requested by User)
|
||||||
try {
|
try {
|
||||||
// Clear API/EPG Cache (using default box name 'dio_cache')
|
// Clear API/EPG Cache (using default box name 'dio_cache')
|
||||||
await Hive.deleteBoxFromDisk('dio_cache');
|
await Hive.deleteBoxFromDisk('dio_cache');
|
||||||
debugPrint('XtremFlow: API Cache cleared');
|
debugPrint('XtremFlow: API Cache cleared');
|
||||||
|
|
||||||
// Clear Image Cache (if using cached_network_image)
|
|
||||||
// Note: flutter_cache_manager needs to be imported if used explicitly,
|
|
||||||
// but typically it manages itself. If we want to force clear:
|
|
||||||
// await DefaultCacheManager().emptyCache();
|
|
||||||
// Since we didn't add the import yet, we'll stick to Hive for now as that handles EPG/API.
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('XtremFlow: Failed to clear cache: $e');
|
debugPrint('XtremFlow: Failed to clear cache: $e');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register adapters for Hive
|
|
||||||
if (!Hive.isAdapterRegistered(1)) {
|
|
||||||
Hive.registerAdapter(PlaylistConfigAdapter());
|
|
||||||
}
|
|
||||||
|
|
||||||
runApp(const ProviderScope(child: MobileApp()));
|
runApp(const ProviderScope(child: MobileApp()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import '../../../../core/models/playlist_config.dart';
|
import '../../../../core/models/playlist_config.dart';
|
||||||
import '../../../../core/theme/app_colors.dart';
|
import '../../../../core/theme/app_colors.dart';
|
||||||
import '../../../theme/mobile_theme.dart';
|
import '../../../theme/mobile_theme.dart';
|
||||||
import '../../../services/local_playlist_service.dart';
|
import '../../../../core/services/playlist_api_service.dart';
|
||||||
import 'mobile_dashboard_screen.dart';
|
import 'mobile_dashboard_screen.dart';
|
||||||
|
|
||||||
/// Provider for local playlists
|
/// Provider for local playlists
|
||||||
final localPlaylistsProvider =
|
final localPlaylistsProvider =
|
||||||
FutureProvider<List<PlaylistConfig>>((ref) async {
|
FutureProvider<List<PlaylistConfig>>((ref) async {
|
||||||
final service = LocalPlaylistService();
|
final service = PlaylistApiService();
|
||||||
return service.getPlaylists();
|
return service.getPlaylists();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -24,12 +24,12 @@ class MobilePlaylistScreen extends ConsumerStatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MobilePlaylistScreenState extends ConsumerState<MobilePlaylistScreen> {
|
class _MobilePlaylistScreenState extends ConsumerState<MobilePlaylistScreen> {
|
||||||
final LocalPlaylistService _playlistService = LocalPlaylistService();
|
final PlaylistApiService _playlistService = PlaylistApiService();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_playlistService.init();
|
// _playlistService.init(); // PlaylistAPI service inits internally via HiveService
|
||||||
|
|
||||||
// Auto-login: if playlists exist, navigate to first one automatically
|
// Auto-login: if playlists exist, navigate to first one automatically
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
@@ -40,7 +40,7 @@ class _MobilePlaylistScreenState extends ConsumerState<MobilePlaylistScreen> {
|
|||||||
Future<void> _checkAutoLogin() async {
|
Future<void> _checkAutoLogin() async {
|
||||||
try {
|
try {
|
||||||
final playlists = await _playlistService.getPlaylists();
|
final playlists = await _playlistService.getPlaylists();
|
||||||
if (playlists.isNotEmpty && mounted) {
|
if (playlists.length == 1 && mounted) {
|
||||||
// Navigate to first playlist automatically
|
// Navigate to first playlist automatically
|
||||||
_navigateToDashboard(playlists.first);
|
_navigateToDashboard(playlists.first);
|
||||||
}
|
}
|
||||||
@@ -211,7 +211,7 @@ class _MobilePlaylistScreenState extends ConsumerState<MobilePlaylistScreen> {
|
|||||||
password: passwordController.text.trim(),
|
password: passwordController.text.trim(),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await _playlistService.addPlaylist(
|
await _playlistService.createPlaylist(
|
||||||
name: nameController.text.trim(),
|
name: nameController.text.trim(),
|
||||||
dns: dns,
|
dns: dns,
|
||||||
username: usernameController.text.trim(),
|
username: usernameController.text.trim(),
|
||||||
|
|||||||
@@ -37,9 +37,8 @@ class _MobilePlaylistSelectionScreenState
|
|||||||
|
|
||||||
Future<void> _checkAutoLogin() async {
|
Future<void> _checkAutoLogin() async {
|
||||||
final playlists = await ref.read(playlistsProvider.future);
|
final playlists = await ref.read(playlistsProvider.future);
|
||||||
if (playlists.isNotEmpty && mounted) {
|
// Only auto-login if there is exactly ONE playlist
|
||||||
// Auto-login to the first playlist (or last used)
|
if (playlists.length == 1 && mounted) {
|
||||||
// Ideally last used, but for now first is fine as per request "une playlist existe".
|
|
||||||
context.go('/dashboard', extra: playlists.first);
|
context.go('/dashboard', extra: playlists.first);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import '../../../../core/models/playlist_config.dart';
|
|||||||
import '../../../../core/theme/app_colors.dart';
|
import '../../../../core/theme/app_colors.dart';
|
||||||
import '../../../providers/mobile_settings_providers.dart';
|
import '../../../providers/mobile_settings_providers.dart';
|
||||||
import '../../../providers/mobile_xtream_providers.dart';
|
import '../../../providers/mobile_xtream_providers.dart';
|
||||||
import '../../../../features/iptv/services/xtream_service_mobile.dart';
|
|
||||||
import '../screens/native_player_screen.dart';
|
import '../screens/native_player_screen.dart';
|
||||||
|
|
||||||
class MobileMoviesTab extends ConsumerStatefulWidget {
|
class MobileMoviesTab extends ConsumerStatefulWidget {
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
import 'package:hive_flutter/hive_flutter.dart';
|
|
||||||
import 'package:uuid/uuid.dart';
|
|
||||||
import '../../core/models/playlist_config.dart';
|
|
||||||
|
|
||||||
/// Local playlist storage service for mobile
|
|
||||||
/// Uses Hive for persistent local storage
|
|
||||||
class LocalPlaylistService {
|
|
||||||
static const String _boxName = 'playlists_mobile';
|
|
||||||
Box<PlaylistConfig>? _box;
|
|
||||||
|
|
||||||
/// Initialize the service and open the Hive box
|
|
||||||
Future<void> init() async {
|
|
||||||
if (_box != null && _box!.isOpen) return;
|
|
||||||
|
|
||||||
// Register adapter if not already registered
|
|
||||||
if (!Hive.isAdapterRegistered(1)) {
|
|
||||||
Hive.registerAdapter(PlaylistConfigAdapter());
|
|
||||||
}
|
|
||||||
|
|
||||||
_box = await Hive.openBox<PlaylistConfig>(_boxName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get all playlists
|
|
||||||
Future<List<PlaylistConfig>> getPlaylists() async {
|
|
||||||
await init();
|
|
||||||
return _box!.values.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a new playlist
|
|
||||||
Future<PlaylistConfig> addPlaylist({
|
|
||||||
required String name,
|
|
||||||
required String dns,
|
|
||||||
required String username,
|
|
||||||
required String password,
|
|
||||||
}) async {
|
|
||||||
await init();
|
|
||||||
|
|
||||||
final playlist = PlaylistConfig(
|
|
||||||
id: const Uuid().v4(),
|
|
||||||
name: name,
|
|
||||||
dns: dns.endsWith('/') ? dns.substring(0, dns.length - 1) : dns,
|
|
||||||
username: username,
|
|
||||||
password: password,
|
|
||||||
createdAt: DateTime.now(),
|
|
||||||
);
|
|
||||||
|
|
||||||
await _box!.put(playlist.id, playlist);
|
|
||||||
return playlist;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update an existing playlist
|
|
||||||
Future<PlaylistConfig?> updatePlaylist({
|
|
||||||
required String id,
|
|
||||||
required String name,
|
|
||||||
required String dns,
|
|
||||||
required String username,
|
|
||||||
required String password,
|
|
||||||
}) async {
|
|
||||||
await init();
|
|
||||||
|
|
||||||
final existing = _box!.get(id);
|
|
||||||
if (existing == null) return null;
|
|
||||||
|
|
||||||
final updated = existing.copyWith(
|
|
||||||
name: name,
|
|
||||||
dns: dns.endsWith('/') ? dns.substring(0, dns.length - 1) : dns,
|
|
||||||
username: username,
|
|
||||||
password: password,
|
|
||||||
);
|
|
||||||
|
|
||||||
await _box!.put(id, updated);
|
|
||||||
return updated;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete a playlist
|
|
||||||
Future<bool> deletePlaylist(String id) async {
|
|
||||||
await init();
|
|
||||||
await _box!.delete(id);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a single playlist by ID
|
|
||||||
Future<PlaylistConfig?> getPlaylist(String id) async {
|
|
||||||
await init();
|
|
||||||
return _box!.get(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Close the box when done
|
|
||||||
Future<void> dispose() async {
|
|
||||||
await _box?.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user