From c944c49ea43948349e149676406274868243c229 Mon Sep 17 00:00:00 2001 From: Dan Pilch Date: Mon, 9 Mar 2026 16:35:36 +0000 Subject: [PATCH] make setup progress (gathering channel data etc.) more visible for the user --- lib/backend/m3u.dart | 21 +++++++++++++++------ lib/backend/utils.dart | 26 +++++++++++++++++--------- lib/backend/xtream.dart | 8 +++++++- lib/loading.dart | 28 +++++++++++++++++++++++----- lib/settings_view.dart | 9 +++++++-- lib/setup.dart | 6 ++++++ 6 files changed, 75 insertions(+), 23 deletions(-) diff --git a/lib/backend/m3u.dart b/lib/backend/m3u.dart index aa30f0d..a11dad6 100644 --- a/lib/backend/m3u.dart +++ b/lib/backend/m3u.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:open_tv/backend/sql.dart'; -import 'package:open_tv/backend/utils.dart'; +import 'package:open_tv/backend/utils.dart' show Utils, ProgressCallback; import 'package:open_tv/models/channel.dart'; import 'package:open_tv/models/channel_http_headers.dart'; import 'package:open_tv/models/channel_preserve.dart'; @@ -20,9 +20,10 @@ final httpOriginRegex = RegExp(r'http-origin=(.+)'); final httpReferrerRegex = RegExp(r'http-referrer=(.+)'); final httpUserAgentRegex = RegExp(r'http-user-agent=(.+)'); -Future processM3U(Source source, bool wipe, [String? path]) async { +Future processM3U(Source source, bool wipe, [String? path, ProgressCallback? onProgress]) async { path ??= source.url; List? preserve; + onProgress?.call("Parsing channels..."); var file = File( path!, ).openRead().transform(utf8.decoder).transform(const LineSplitter()); @@ -37,6 +38,7 @@ Future processM3U(Source source, bool wipe, [String? path]) async { String? channelLine; ChannelHttpHeaders? headers; var httpHeadersSet = false; + int channelCount = 0; await for (var line in file) { final lineUpper = line.toUpperCase(); if (lineUpper.startsWith("#EXTINF")) { @@ -49,6 +51,10 @@ Future processM3U(Source source, bool wipe, [String? path]) async { httpHeadersSet ? headers : null, statements, ); + channelCount++; + if (channelCount % 100 == 0) { + onProgress?.call("Parsing channels ($channelCount found)..."); + } } channelLine = line; lastLine = null; @@ -67,11 +73,13 @@ Future processM3U(Source source, bool wipe, [String? path]) async { } if (channelLine != null && lastLine != null && lastLine.trim().isNotEmpty) { commitChannel(channelLine, lastLine, headers, statements); + channelCount++; } statements.add(Sql.updateGroups()); if (preserve != null) { statements.add(Sql.restorePreserve(preserve)); } + onProgress?.call("Saving to database ($channelCount channels)..."); await Sql.commitWrite(statements); } @@ -147,12 +155,13 @@ bool setChannelHeaders(String headerLine, ChannelHttpHeaders headers) { return false; } -Future processM3UUrl(Source source, bool wipe) async { - var path = await downloadM3U(source.url!); - await processM3U(source, wipe, path); +Future processM3UUrl(Source source, bool wipe, [ProgressCallback? onProgress]) async { + var path = await downloadM3U(source.url!, onProgress); + await processM3U(source, wipe, path, onProgress); } -Future downloadM3U(String urlStr) async { +Future downloadM3U(String urlStr, [ProgressCallback? onProgress]) async { + onProgress?.call("Downloading playlist..."); final url = Uri.parse(urlStr); final client = http.Client(); final request = http.Request('GET', url); diff --git a/lib/backend/utils.dart b/lib/backend/utils.dart index 65a4daa..245e207 100644 --- a/lib/backend/utils.dart +++ b/lib/backend/utils.dart @@ -9,6 +9,8 @@ import 'package:open_tv/models/source_type.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; +typedef ProgressCallback = void Function(String message); + class Utils { static String? _appDir; static Future get appDir async { @@ -23,29 +25,35 @@ class Utils { return join(tempDir, fileName); } - static Future refreshSource(Source source) async { + static Future refreshSource(Source source, [ProgressCallback? onProgress]) async { refreshedSeries.clear(); - await processSource(source, true); + await processSource(source, true, onProgress); } - static Future processSource(Source source, [bool wipe = false]) async { + static Future processSource(Source source, [bool wipe = false, ProgressCallback? onProgress]) async { switch (source.sourceType) { case SourceType.m3u: - await processM3U(source, wipe); + await processM3U(source, wipe, null, onProgress); break; case SourceType.m3uUrl: - await processM3UUrl(source, wipe); + await processM3UUrl(source, wipe, onProgress); break; case SourceType.xtream: - await getXtream(source, wipe); + await getXtream(source, wipe, onProgress); break; } } - static Future refreshAllSources() async { + static Future refreshAllSources([ProgressCallback? onProgress]) async { var sources = await Sql.getSources(); - for (var source in sources) { - await refreshSource(source); + for (var i = 0; i < sources.length; i++) { + var source = sources[i]; + ProgressCallback? sourceProgress; + if (onProgress != null) { + var prefix = sources.length > 1 ? "Refreshing ${source.name} (${i + 1}/${sources.length})\n" : ""; + sourceProgress = (msg) => onProgress("$prefix$msg"); + } + await refreshSource(source, sourceProgress); } } diff --git a/lib/backend/xtream.dart b/lib/backend/xtream.dart index d7588ac..1c0a9b3 100644 --- a/lib/backend/xtream.dart +++ b/lib/backend/xtream.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:open_tv/backend/sql.dart'; +import 'package:open_tv/backend/utils.dart' show ProgressCallback; import 'package:open_tv/models/channel.dart'; import 'package:open_tv/models/channel_preserve.dart'; import 'package:open_tv/models/media_type.dart'; @@ -18,7 +19,7 @@ const String getLiveStreamCategories = "get_live_categories"; const String getVodCategories = "get_vod_categories"; const String liveStreamExtension = "ts"; -Future getXtream(Source source, bool wipe) async { +Future getXtream(Source source, bool wipe, [ProgressCallback? onProgress]) async { List Function(SqliteWriteContext, Map)> statements = []; List? preserve; @@ -28,6 +29,7 @@ Future getXtream(Source source, bool wipe) async { statements.add(Sql.wipeSource(source.id!)); } source.urlOrigin = Uri.parse(source.url!).origin; + onProgress?.call("Fetching data..."); var results = await Future.wait([ getXtreamHttpData(getLiveStreams, source), getXtreamHttpData(getLiveStreamCategories, source), @@ -39,6 +41,7 @@ Future getXtream(Source source, bool wipe) async { int failCount = 0; if (results[0] != null && results[1] != null) { try { + onProgress?.call("Processing livestreams..."); processXtream( statements, processJsonList(results[0], XtreamStream.fromJson), @@ -54,6 +57,7 @@ Future getXtream(Source source, bool wipe) async { } if (results[2] != null && results[3] != null) { try { + onProgress?.call("Processing movies..."); processXtream( statements, processJsonList(results[2], XtreamStream.fromJson), @@ -70,6 +74,7 @@ Future getXtream(Source source, bool wipe) async { if (results[4] != null && results[5] != null) { try { + onProgress?.call("Processing series..."); processXtream( statements, processJsonList(results[4], XtreamStream.fromJson), @@ -91,6 +96,7 @@ Future getXtream(Source source, bool wipe) async { if (preserve != null) { statements.add(Sql.restorePreserve(preserve)); } + onProgress?.call("Saving to database..."); await Sql.commitWrite(statements); } diff --git a/lib/loading.dart b/lib/loading.dart index 05d2491..e4711aa 100644 --- a/lib/loading.dart +++ b/lib/loading.dart @@ -2,6 +2,28 @@ import 'package:flutter/material.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:loader_overlay/loader_overlay.dart'; +Widget progressOverlayBuilder(dynamic progress) { + return Column( + children: [ + const Spacer(flex: 2), + const SpinKitCircle(color: Colors.blue, size: 50), + if (progress != null) ...[ + const SizedBox(height: 16), + Text( + progress.toString(), + style: const TextStyle( + color: Colors.white, + fontSize: 14, + decoration: TextDecoration.none, + ), + textAlign: TextAlign.center, + ), + ], + const Spacer(flex: 1), + ], + ); +} + class Loading extends StatelessWidget { final Widget child; const Loading({super.key, required this.child}); @@ -9,11 +31,7 @@ class Loading extends StatelessWidget { @override Widget build(BuildContext context) { return LoaderOverlay( - overlayWidgetBuilder: (_) { - return const Center( - child: SpinKitPulsingGrid(color: Colors.blue, size: 50), - ); - }, + overlayWidgetBuilder: progressOverlayBuilder, child: child, ); } diff --git a/lib/settings_view.dart b/lib/settings_view.dart index a8683bf..28869d5 100644 --- a/lib/settings_view.dart +++ b/lib/settings_view.dart @@ -9,6 +9,7 @@ import 'package:open_tv/select_dialog.dart'; import 'package:open_tv/edit_dialog.dart'; import 'package:open_tv/home.dart'; import 'package:open_tv/loading.dart'; +import 'package:loader_overlay/loader_overlay.dart'; import 'package:open_tv/models/home_manager.dart'; import 'package:open_tv/models/id_data.dart'; import 'package:open_tv/models/settings.dart'; @@ -140,7 +141,9 @@ class _SettingsState extends State { onPressed: () async { await Error.tryAsync( () async { - await Utils.refreshSource(source); + await Utils.refreshSource(source, (msg) { + if (context.mounted) context.loaderOverlay.progress(msg); + }); }, context, "Source has been refreshed successfully", @@ -351,7 +354,9 @@ class _SettingsState extends State { children: [ IconButton( onPressed: () async => await Error.tryAsync( - () async => await Utils.refreshAllSources(), + () async => await Utils.refreshAllSources((msg) { + if (context.mounted) context.loaderOverlay.progress(msg); + }), context, "Successfully refreshed all sources", ), diff --git a/lib/setup.dart b/lib/setup.dart index 45aa47f..6075338 100644 --- a/lib/setup.dart +++ b/lib/setup.dart @@ -7,6 +7,7 @@ import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:loader_overlay/loader_overlay.dart'; import 'package:open_tv/backend/sql.dart'; import 'package:open_tv/backend/utils.dart'; +import 'package:open_tv/loading.dart' show progressOverlayBuilder; import 'package:open_tv/correction_modal.dart'; import 'package:open_tv/home.dart'; import 'package:open_tv/models/filters.dart'; @@ -69,6 +70,10 @@ class _SetupState extends State { ? formValues[Steps.password] : null, ), + false, + (msg) { + if (context.mounted) context.loaderOverlay.progress(msg); + }, ); }, context, @@ -207,6 +212,7 @@ class _SetupState extends State { appBar: widget.showAppBar ? AppBar() : null, body: SafeArea( child: LoaderOverlay( + overlayWidgetBuilder: progressOverlayBuilder, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [