## Screens ### home_screen.dart - Removed unused imports (flutter/services) - Removed unused _focusedIndex state variable - Simplified responsive layout logic: - Removed _isMediumScreen, _gridCrossAxisCount getters - Removed _titleFontSize, _iconSize getters - Kept only _headerPadding for responsive padding - Improved navigation with mounted checks - Better MaterialPageRoute formatting - Enhanced _downloadPlaylistAsJson method ## Services ### xtream_api.dart - Added http.Client dependency injection for testability - Implemented _countryExtractionCache for performance - Added regex patterns for country code extraction: - _leadingCodeRegex for "AR - Channel" format - _bracketCodeRegex for "[AR] Channel" format - Enhanced football channel detection patterns - Improved error handling and messages - Better formatted country mapping with regions ### iptv_provider.dart - Better state management separation - Optimized stream filtering for large lists - Refactored country filtering methods - Enhanced playlist download and caching logic - Improved memory management ## Widgets ### countries_sidebar.dart - Better responsive design for TV screens - Enhanced FocusableActionDetector implementation - Improved focus indicators for Android TV - Smoother transitions between selections ### simple_countries_sidebar.dart - Cleaner, more maintainable code structure - Better keyboard/remote navigation support - Improved visual feedback and styling ## Player ### player_screen.dart - Better error handling for playback failures - Enhanced responsive layout - Improved Android TV control visibility - Better buffer management and loading indicators ## Tests ### widget_test.dart - Updated to match new widget signatures - Improved test coverage for refactored components ## Technical Improvements - Better separation of concerns across all layers - Dependency injection patterns for testability - Performance optimizations with caching - Consistent code formatting and documentation - Removed unused code and imports - Enhanced Android TV support with FocusableActionDetector ## Statistics - 8 files changed - +1300 insertions - -1139 deletions - Net: +161 lines of cleaner code ## Breaking Changes None - all internal refactorings with no API changes
660 lines
19 KiB
Dart
660 lines
19 KiB
Dart
import 'package:flutter/foundation.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import '../services/xtream_api.dart';
|
|
import '../models/xtream_models.dart';
|
|
|
|
enum ContentType { live, movies, series }
|
|
|
|
// Special category constants
|
|
class SpecialCategories {
|
|
static const String argentineFootball = 'Fútbol Argentino';
|
|
}
|
|
|
|
class IPTVProvider extends ChangeNotifier {
|
|
final XtreamApiService _api = XtreamApiService();
|
|
|
|
bool _isLoading = false;
|
|
String? _error;
|
|
XtreamUserInfo? _userInfo;
|
|
|
|
int _loadedChannels = 0;
|
|
int _totalChannels = 0;
|
|
|
|
List<XtreamCategory> _liveCategories = [];
|
|
List<XtreamCategory> _vodCategories = [];
|
|
List<XtreamCategory> _seriesCategories = [];
|
|
|
|
List<XtreamStream> _liveStreams = [];
|
|
List<XtreamStream> _vodStreams = [];
|
|
List<XtreamSeries> _seriesList = [];
|
|
List<XtreamEpisode> _seriesEpisodes = [];
|
|
|
|
String _selectedLiveCategory = '';
|
|
String _selectedCountry = '';
|
|
String _selectedCategory =
|
|
''; // For special categories like "Fútbol Argentino"
|
|
List<String> _countries = [];
|
|
bool _isOrganizingCountries = false;
|
|
XtreamSeries? _selectedSeries;
|
|
|
|
Map<String, String>? _categoryToCountryMapCache;
|
|
List<XtreamStream>? _filteredLiveStreamsCache;
|
|
String _filteredCountryCacheKey = '';
|
|
String _filteredCategoryCacheKey = '';
|
|
int _liveStreamsVersion = 0;
|
|
int _filteredCacheVersion = -1;
|
|
int _lastProgressUiUpdateMs = 0;
|
|
|
|
bool get isLoading => _isLoading;
|
|
String? get error => _error;
|
|
XtreamUserInfo? get userInfo => _userInfo;
|
|
XtreamApiService get api => _api;
|
|
int get loadedChannels => _loadedChannels;
|
|
int get totalChannels => _totalChannels;
|
|
double get loadingProgress =>
|
|
_totalChannels > 0 ? _loadedChannels / _totalChannels : 0.0;
|
|
bool get isOrganizingCountries => _isOrganizingCountries;
|
|
|
|
List<XtreamCategory> get liveCategories => _liveCategories;
|
|
List<XtreamCategory> get vodCategories => _vodCategories;
|
|
List<XtreamCategory> get seriesCategories => _seriesCategories;
|
|
|
|
List<XtreamStream> get liveStreams => _liveStreams;
|
|
List<XtreamStream> get vodStreams => _vodStreams;
|
|
List<XtreamSeries> get seriesList => _seriesList;
|
|
List<XtreamEpisode> get seriesEpisodes => _seriesEpisodes;
|
|
|
|
String get selectedLiveCategory => _selectedLiveCategory;
|
|
String get selectedCountry => _selectedCountry;
|
|
String get selectedCategory => _selectedCategory;
|
|
List<String> get countries => _countries;
|
|
|
|
/// Get display items for sidebar including special categories
|
|
/// Returns a list of maps with 'name', 'type', and 'priority' for proper ordering
|
|
List<Map<String, dynamic>> get sidebarItems {
|
|
final items = <Map<String, dynamic>>[];
|
|
|
|
// Add all countries with their priority
|
|
for (final country in _countries) {
|
|
int priority = _getCountryPriority(country);
|
|
items.add({'name': country, 'type': 'country', 'priority': priority});
|
|
}
|
|
|
|
// Add special category: Fútbol Argentino (priority 2.5 - between Perú and other countries)
|
|
// Only add if there are any Argentine football channels
|
|
final hasArgentineFootball = _liveStreams.any(
|
|
(s) => _api.isArgentineFootballChannel(s.name),
|
|
);
|
|
if (hasArgentineFootball) {
|
|
items.add({
|
|
'name': SpecialCategories.argentineFootball,
|
|
'type': 'category',
|
|
'priority':
|
|
2.5, // Between Perú (2) and other South American countries (3)
|
|
});
|
|
}
|
|
|
|
// Sort by priority, then alphabetically
|
|
items.sort((a, b) {
|
|
final priorityCompare = (a['priority'] as double).compareTo(
|
|
b['priority'] as double,
|
|
);
|
|
if (priorityCompare != 0) return priorityCompare;
|
|
return (a['name'] as String).compareTo(b['name'] as String);
|
|
});
|
|
|
|
return items;
|
|
}
|
|
|
|
/// Get priority for a country (lower number = higher priority)
|
|
int _getCountryPriority(String country) {
|
|
switch (country) {
|
|
case 'Argentina':
|
|
return 1;
|
|
case 'Perú':
|
|
case 'Peru':
|
|
return 2;
|
|
case 'Bolivia':
|
|
case 'Brasil':
|
|
case 'Brazil':
|
|
case 'Chile':
|
|
case 'Colombia':
|
|
case 'Ecuador':
|
|
case 'Paraguay':
|
|
case 'Uruguay':
|
|
case 'Venezuela':
|
|
return 3;
|
|
default:
|
|
return 100; // Low priority for other countries
|
|
}
|
|
}
|
|
|
|
XtreamSeries? get selectedSeries => _selectedSeries;
|
|
|
|
void _invalidateLiveDerivedCaches() {
|
|
_categoryToCountryMapCache = null;
|
|
_filteredLiveStreamsCache = null;
|
|
_filteredCacheVersion = -1;
|
|
}
|
|
|
|
void _setLiveStreams(List<XtreamStream> streams, String categoryId) {
|
|
_liveStreams = streams;
|
|
_selectedLiveCategory = categoryId;
|
|
_liveStreamsVersion++;
|
|
_invalidateLiveDerivedCaches();
|
|
}
|
|
|
|
void _notifyProgressUpdate() {
|
|
final nowMs = DateTime.now().millisecondsSinceEpoch;
|
|
final shouldUpdate =
|
|
(nowMs - _lastProgressUiUpdateMs) >= 120 ||
|
|
(_totalChannels > 0 && _loadedChannels >= _totalChannels);
|
|
if (shouldUpdate) {
|
|
_lastProgressUiUpdateMs = nowMs;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
Future<void> login(String server, String username, String password) async {
|
|
_isLoading = true;
|
|
_error = null;
|
|
notifyListeners();
|
|
|
|
try {
|
|
_api.setCredentials(server, username, password);
|
|
_userInfo = await _api.getUserInfo();
|
|
|
|
// No automatic data loading on startup - data loads on demand only
|
|
|
|
await _saveCredentials(server, username, password);
|
|
} catch (e) {
|
|
_error = e.toString();
|
|
}
|
|
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> loadLiveStreams([String categoryId = '']) async {
|
|
_isLoading = true;
|
|
_isOrganizingCountries = false;
|
|
_loadedChannels = 0;
|
|
_totalChannels = 0;
|
|
_countries = [];
|
|
_lastProgressUiUpdateMs = 0;
|
|
_invalidateLiveDerivedCaches();
|
|
notifyListeners();
|
|
|
|
try {
|
|
// STEP 1: Load from API first (much faster than M3U)
|
|
|
|
try {
|
|
_setLiveStreams(await _api.getLiveStreams(categoryId), categoryId);
|
|
_totalChannels = _liveStreams.length;
|
|
_loadedChannels = _liveStreams.length;
|
|
|
|
if (_liveStreams.isEmpty) {
|
|
throw Exception('API returned 0 streams');
|
|
}
|
|
} catch (apiError) {
|
|
// Fallback to M3U only if API fails
|
|
_setLiveStreams(
|
|
await _api.getM3UStreams(
|
|
onProgress: (loaded, total) {
|
|
_loadedChannels = loaded;
|
|
_totalChannels = total;
|
|
_notifyProgressUpdate();
|
|
},
|
|
),
|
|
categoryId,
|
|
);
|
|
|
|
if (_liveStreams.isEmpty) {
|
|
throw Exception('No channels available from API or M3U');
|
|
}
|
|
}
|
|
|
|
// STEP 2: Mark loading complete - channels ready to display
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
|
|
// STEP 3: Extract countries in background (using optimized method)
|
|
_extractCountriesInBackground();
|
|
} catch (e) {
|
|
_error = e.toString();
|
|
}
|
|
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Extract countries from streams in the background to avoid UI freezing
|
|
void _extractCountriesInBackground() {
|
|
if (_liveStreams.isEmpty) return;
|
|
|
|
_isOrganizingCountries = true;
|
|
notifyListeners();
|
|
|
|
// Use Future.microtask to schedule the extraction after the current frame
|
|
Future.microtask(() {
|
|
try {
|
|
// Use optimized extraction (only sample 2000 channels for speed)
|
|
_countries = _api.getCountriesOptimized(
|
|
_liveStreams,
|
|
maxChannelsToProcess: 2000,
|
|
);
|
|
} catch (e) {
|
|
_countries = [];
|
|
} finally {
|
|
_isOrganizingCountries = false;
|
|
notifyListeners();
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Check if a string is a group title (not a country)
|
|
bool _isGroupTitle(String name) {
|
|
final normalized = name.toLowerCase().trim();
|
|
final groupTitles = {
|
|
'24/7',
|
|
'24/7 ar',
|
|
'24/7 in',
|
|
'24/7-es',
|
|
'24/7-de',
|
|
'24/7-gr',
|
|
'24/7-my',
|
|
'24/7-pt',
|
|
'24/7-ro',
|
|
'24/7-tr',
|
|
'24/7-latino',
|
|
'vip',
|
|
'vip - pk',
|
|
'ppv',
|
|
'movies',
|
|
'cine',
|
|
'cine sd',
|
|
'cine y serie',
|
|
'latino',
|
|
'general',
|
|
'music',
|
|
'religious',
|
|
'bein',
|
|
'mbc',
|
|
'tod',
|
|
'osn',
|
|
'myhd',
|
|
'dstv',
|
|
'art',
|
|
'icc-ca',
|
|
'icc-car',
|
|
'icc-dstv',
|
|
'icc-in',
|
|
'icc-nz',
|
|
'icc-pk',
|
|
'icc-uk',
|
|
'xmas',
|
|
'sin',
|
|
'ezd',
|
|
'exyu',
|
|
'rot',
|
|
'ar-kids',
|
|
'ar-sp',
|
|
'islam',
|
|
'bab',
|
|
'as',
|
|
'ei',
|
|
};
|
|
return groupTitles.contains(normalized);
|
|
}
|
|
|
|
// Build a map from category ID to country name for API streams
|
|
Map<String, String> _buildCategoryToCountryMap() {
|
|
if (_categoryToCountryMapCache != null) {
|
|
return _categoryToCountryMapCache!;
|
|
}
|
|
|
|
final map = <String, String>{};
|
|
for (final category in _liveCategories) {
|
|
final countryName = category.name.split('|').first.trim();
|
|
// Only add if it's a valid country (not a group title)
|
|
if (countryName.isNotEmpty && !_isGroupTitle(countryName)) {
|
|
map[category.id] = countryName;
|
|
}
|
|
}
|
|
_categoryToCountryMapCache = map;
|
|
return map;
|
|
}
|
|
|
|
void filterByCountry(String country) {
|
|
final normalizedCountry = country.trim();
|
|
if (_selectedCountry == normalizedCountry && _selectedCategory.isEmpty) {
|
|
return;
|
|
}
|
|
_selectedCountry = normalizedCountry;
|
|
_selectedCategory = ''; // Clear special category when country is selected
|
|
_filteredLiveStreamsCache = null;
|
|
notifyListeners();
|
|
}
|
|
|
|
void filterByCategory(String category) {
|
|
final normalizedCategory = category.trim();
|
|
if (_selectedCategory == normalizedCategory && _selectedCountry.isEmpty) {
|
|
return;
|
|
}
|
|
_selectedCategory = normalizedCategory;
|
|
_selectedCountry = ''; // Clear country when special category is selected
|
|
_filteredLiveStreamsCache = null;
|
|
notifyListeners();
|
|
}
|
|
|
|
List<XtreamStream> get filteredLiveStreams {
|
|
final selectedCountry = _selectedCountry.trim();
|
|
final selectedCategory = _selectedCategory.trim();
|
|
if (_filteredLiveStreamsCache != null &&
|
|
_filteredCacheVersion == _liveStreamsVersion &&
|
|
_filteredCountryCacheKey == selectedCountry &&
|
|
_filteredCategoryCacheKey == selectedCategory) {
|
|
return _filteredLiveStreamsCache!;
|
|
}
|
|
|
|
late final List<XtreamStream> result;
|
|
|
|
// If a special category is selected, filter by that
|
|
if (selectedCategory.isNotEmpty) {
|
|
result = _api.filterByCategory(_liveStreams, selectedCategory);
|
|
} else if (selectedCountry.isEmpty ||
|
|
selectedCountry.toLowerCase() == 'todos' ||
|
|
selectedCountry.toLowerCase() == 'all') {
|
|
result = _liveStreams;
|
|
} else {
|
|
// Build category map for API streams that don't have country in name
|
|
final categoryMap = _buildCategoryToCountryMap();
|
|
result = _api.filterByCountry(
|
|
_liveStreams,
|
|
selectedCountry,
|
|
categoryToCountryMap: categoryMap,
|
|
);
|
|
}
|
|
|
|
_filteredCountryCacheKey = selectedCountry;
|
|
_filteredCategoryCacheKey = selectedCategory;
|
|
_filteredCacheVersion = _liveStreamsVersion;
|
|
_filteredLiveStreamsCache = identical(result, _liveStreams)
|
|
? _liveStreams
|
|
: List.unmodifiable(result);
|
|
return _filteredLiveStreamsCache!;
|
|
}
|
|
|
|
Future<void> loadVodStreams([String categoryId = '']) async {
|
|
_isLoading = true;
|
|
notifyListeners();
|
|
|
|
try {
|
|
_vodStreams = await _api.getVodStreams(categoryId);
|
|
} catch (e) {
|
|
_error = e.toString();
|
|
}
|
|
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> loadSeries() async {
|
|
_isLoading = true;
|
|
notifyListeners();
|
|
|
|
try {
|
|
_seriesList = await _api.getSeries();
|
|
} catch (e) {
|
|
_error = e.toString();
|
|
}
|
|
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> loadSeriesEpisodes(XtreamSeries series) async {
|
|
_isLoading = true;
|
|
notifyListeners();
|
|
|
|
try {
|
|
_selectedSeries = series;
|
|
_seriesEpisodes = await _api.getSeriesEpisodes(series.seriesId);
|
|
} catch (e) {
|
|
_error = e.toString();
|
|
}
|
|
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> reloadM3UStreams() async {
|
|
_isLoading = true;
|
|
_isOrganizingCountries = false;
|
|
_error = null;
|
|
_loadedChannels = 0;
|
|
_totalChannels = 0;
|
|
_countries = [];
|
|
_lastProgressUiUpdateMs = 0;
|
|
_invalidateLiveDerivedCaches();
|
|
notifyListeners();
|
|
|
|
try {
|
|
// Try API first, then M3U fallback
|
|
try {
|
|
_setLiveStreams(await _api.getLiveStreams(''), '');
|
|
_totalChannels = _liveStreams.length;
|
|
_loadedChannels = _liveStreams.length;
|
|
} catch (apiError) {
|
|
_setLiveStreams(
|
|
await _api.getM3UStreams(
|
|
onProgress: (loaded, total) {
|
|
_loadedChannels = loaded;
|
|
_totalChannels = total;
|
|
_notifyProgressUpdate();
|
|
},
|
|
),
|
|
'',
|
|
);
|
|
}
|
|
|
|
// Mark loading as complete - channels are ready to display
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
|
|
// Extract countries in background (optimized)
|
|
_extractCountriesInBackground();
|
|
} catch (e) {
|
|
_error = 'Error al cargar canales: $e';
|
|
_isLoading = false;
|
|
_isOrganizingCountries = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
/// Downloads M3U playlist and saves it as JSON file
|
|
/// Returns the file path where the JSON was saved
|
|
Future<String> downloadAndSaveM3UAsJson() async {
|
|
_isLoading = true;
|
|
_error = null;
|
|
notifyListeners();
|
|
|
|
try {
|
|
// If we already have streams loaded, save those instead of downloading again
|
|
if (_liveStreams.isNotEmpty) {
|
|
// Create M3U result from loaded streams
|
|
final channels = _liveStreams
|
|
.map(
|
|
(stream) => M3UChannel(
|
|
name: stream.name,
|
|
url: stream.url ?? '',
|
|
groupTitle: stream.plot ?? 'Unknown',
|
|
tvgLogo: stream.streamIcon,
|
|
),
|
|
)
|
|
.toList();
|
|
|
|
final result = M3UDownloadResult(
|
|
sourceUrl: '${_api.server}/get.php',
|
|
downloadTime: DateTime.now(),
|
|
totalChannels: channels.length,
|
|
groupsCount: _groupChannelsByCountry(channels),
|
|
channels: channels,
|
|
);
|
|
|
|
// Save as JSON file
|
|
final filePath = await _api.saveM3UAsJson(result);
|
|
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
|
|
return filePath;
|
|
}
|
|
|
|
// If no streams loaded, try to download
|
|
final result = await _api.downloadM3UAsJson();
|
|
|
|
// Save as JSON file
|
|
final filePath = await _api.saveM3UAsJson(result);
|
|
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
|
|
return filePath;
|
|
} catch (e) {
|
|
_error = 'Error al descargar playlist: $e';
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
throw Exception(_error);
|
|
}
|
|
}
|
|
|
|
Map<String, int> _groupChannelsByCountry(List<M3UChannel> channels) {
|
|
final groups = <String, int>{};
|
|
for (final channel in channels) {
|
|
final country = channel.groupTitle ?? 'Unknown';
|
|
groups[country] = (groups[country] ?? 0) + 1;
|
|
}
|
|
return groups;
|
|
}
|
|
|
|
/// Saves all loaded live channels as a text file for analysis
|
|
Future<String> saveChannelsAsText() async {
|
|
try {
|
|
// Build text content
|
|
final buffer = StringBuffer();
|
|
buffer.writeln('=== XSTREAM TV - LISTA DE CANALES ===');
|
|
buffer.writeln('Fecha: ${DateTime.now()}');
|
|
buffer.writeln('Total de canales: ${_liveStreams.length}');
|
|
buffer.writeln('');
|
|
|
|
// Build category map for API streams
|
|
final categoryMap = _buildCategoryToCountryMap();
|
|
|
|
// Group by country
|
|
final groupedChannels = <String, List<XtreamStream>>{};
|
|
for (final stream in _liveStreams) {
|
|
String? country;
|
|
|
|
// First try to extract from name (M3U format)
|
|
final countryFromName = _api.extractCountryFromChannelName(stream.name);
|
|
if (countryFromName.isNotEmpty) {
|
|
country = countryFromName;
|
|
}
|
|
// If not found, try category mapping (API format)
|
|
else if (stream.categoryId != null &&
|
|
categoryMap.containsKey(stream.categoryId)) {
|
|
country = categoryMap[stream.categoryId];
|
|
}
|
|
|
|
final normalizedCountry = country != null && country.isNotEmpty
|
|
? _api.normalizeCountry(country)
|
|
: 'Sin País';
|
|
|
|
if (!groupedChannels.containsKey(normalizedCountry)) {
|
|
groupedChannels[normalizedCountry] = [];
|
|
}
|
|
groupedChannels[normalizedCountry]!.add(stream);
|
|
}
|
|
|
|
// Write grouped channels
|
|
final sortedCountries = groupedChannels.keys.toList()..sort();
|
|
for (final country in sortedCountries) {
|
|
final channels = groupedChannels[country]!;
|
|
buffer.writeln('');
|
|
buffer.writeln('=== $country (${channels.length} canales) ===');
|
|
buffer.writeln('');
|
|
|
|
for (int i = 0; i < channels.length; i++) {
|
|
final stream = channels[i];
|
|
buffer.writeln('${i + 1}. ${stream.name}');
|
|
if (stream.url != null && stream.url!.isNotEmpty) {
|
|
buffer.writeln(' URL: ${stream.url}');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Save to file
|
|
final fileName =
|
|
'xstream_canales_${DateTime.now().millisecondsSinceEpoch}.txt';
|
|
final filePath = await _api.saveTextFile(fileName, buffer.toString());
|
|
|
|
return filePath;
|
|
} catch (e) {
|
|
throw Exception('Error al guardar lista: $e');
|
|
}
|
|
}
|
|
|
|
void clearError() {
|
|
_error = null;
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> _saveCredentials(
|
|
String server,
|
|
String username,
|
|
String password,
|
|
) async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.setString('server', server);
|
|
await prefs.setString('username', username);
|
|
await prefs.setString('password', password);
|
|
}
|
|
|
|
Future<bool> loadSavedCredentials() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final server = prefs.getString('server');
|
|
final username = prefs.getString('username');
|
|
final password = prefs.getString('password');
|
|
|
|
if (server != null && username != null && password != null) {
|
|
await login(server, username, password);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
Future<void> logout() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.remove('server');
|
|
await prefs.remove('username');
|
|
await prefs.remove('password');
|
|
|
|
_userInfo = null;
|
|
_liveCategories = [];
|
|
_vodCategories = [];
|
|
_seriesCategories = [];
|
|
_liveStreams = [];
|
|
_vodStreams = [];
|
|
_seriesList = [];
|
|
_countries = [];
|
|
_selectedLiveCategory = '';
|
|
_selectedCountry = '';
|
|
_selectedCategory = '';
|
|
_isOrganizingCountries = false;
|
|
_liveStreamsVersion = 0;
|
|
_invalidateLiveDerivedCaches();
|
|
notifyListeners();
|
|
}
|
|
}
|