From 7d9034e96eb56b9f1d8f866db942ff9ac17666b2 Mon Sep 17 00:00:00 2001 From: Ghassen Ben Zahra Date: Sun, 10 Nov 2024 21:57:43 +0100 Subject: [PATCH 1/9] Fix / Remove android.mawaqit.net backend (#1396) * remove all android mawaqit backend config * refactor(constants): fix typo in MawaqitBackendSettingsConstants class name --------- Co-authored-by: Yassin --- lib/main.dart | 2 - lib/src/const/constants.dart | 10 + lib/src/elements/HorizontalList.dart | 363 ---------------------- lib/src/pages/PageScreen.dart | 73 ----- lib/src/pages/SplashScreen.dart | 10 +- lib/src/pages/WebScreen.dart | 188 ----------- lib/src/repository/settings_service.dart | 70 ----- lib/src/services/settings_manager.dart | 29 -- lib/src/widgets/MawaqitDrawer.dart | 62 +--- lib/src/widgets/MawaqitWebViewWidget.dart | 14 +- 10 files changed, 25 insertions(+), 796 deletions(-) delete mode 100644 lib/src/elements/HorizontalList.dart delete mode 100644 lib/src/pages/PageScreen.dart delete mode 100644 lib/src/pages/WebScreen.dart delete mode 100644 lib/src/repository/settings_service.dart delete mode 100644 lib/src/services/settings_manager.dart diff --git a/lib/main.dart b/lib/main.dart index 4ba22600e..e414618c2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -25,7 +25,6 @@ import 'package:mawaqit/src/pages/SplashScreen.dart'; import 'package:mawaqit/src/services/audio_manager.dart'; import 'package:mawaqit/src/services/FeatureManager.dart'; import 'package:mawaqit/src/services/mosque_manager.dart'; -import 'package:mawaqit/src/services/settings_manager.dart'; import 'package:mawaqit/src/services/theme_manager.dart'; import 'package:mawaqit/src/services/user_preferences_manager.dart'; import 'package:path_provider/path_provider.dart'; @@ -67,7 +66,6 @@ class MyApp extends StatelessWidget { ChangeNotifierProvider(create: (context) => ThemeNotifier()), ChangeNotifierProvider(create: (context) => AppLanguage()), ChangeNotifierProvider(create: (context) => MosqueManager()), - ChangeNotifierProvider(create: (context) => SettingsManager()), ChangeNotifierProvider(create: (context) => AudioManager()), ChangeNotifierProvider(create: (context) => FeatureManager(context)), ChangeNotifierProvider(create: (context) => UserPreferencesManager(), lazy: false), diff --git a/lib/src/const/constants.dart b/lib/src/const/constants.dart index 14499c311..655a714e2 100644 --- a/lib/src/const/constants.dart +++ b/lib/src/const/constants.dart @@ -96,3 +96,13 @@ abstract class SystemFeaturesConstant { static const String kHdmi = 'android.hardware.hdmi'; static const String kEthernet = 'android.hardware.ethernet'; } + +abstract class MawaqitBackendSettingsConstant { + static const String kSettingsTitle = "Mawaqit"; + static const String kSettingsShare = + "Download Mawaqit\r\nAndroid:\r\nhttps:\/\/play.google.com\/store\/apps\/details?id=com.mawaqit.admin\r\niOS:\r\nhttps:\/\/apps.apple.com\/fr\/app\/mawaqit-prayer-times-mosque\/id1460522683\r\n"; + static const String kSettingsAndroidUserAgent = + "Mozilla\/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/95.0.4638.69 Safari\/537.36"; + static const String kSettingsIosUserAgent = + "Mozilla\/5.0 (iPhone; CPU iPhone OS 14_5 like Mac OS X) AppleWebKit\/605.1.15 (KHTML, like Gecko) CriOS\/90.0.4430.78 Mobile\/15E148 Safari\/604.1"; +} diff --git a/lib/src/elements/HorizontalList.dart b/lib/src/elements/HorizontalList.dart deleted file mode 100644 index 90eab26a9..000000000 --- a/lib/src/elements/HorizontalList.dart +++ /dev/null @@ -1,363 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:mawaqit/src/helpers/HexColor.dart'; -import 'package:mawaqit/src/models/settings.dart'; -import 'package:mawaqit/src/themes/UIImages.dart'; - -class HorizontalList extends StatefulWidget { - String title; - String description; - String selected; - String selectedFirstColor; - String selectedSecondColor; - String type; - IconData icon; - List list; - Function? onTap; - Function? onTapColor; - Function? onTapLoader; - Settings? settings; - - HorizontalList( - {Key? key, - this.title = "", - this.description = "", - this.selected = "", - this.selectedFirstColor = "", - this.selectedSecondColor = "", - this.type = "", - this.icon = Icons.edit, - this.list = const [], - this.onTap, - this.onTapColor, - this.onTapLoader, - this.settings = null}) - : super(key: key); - - @override - State createState() => new _HorizontalList(); -} - -class _HorizontalList extends State { - @override - Widget build(BuildContext context) { - return Container( - width: MediaQuery.of(context).size.width, - alignment: Alignment.topLeft, - margin: EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 0.0), - padding: EdgeInsets.fromLTRB(0.0, 15.0, 0, 15.0), - decoration: BoxDecoration( - border: Border(bottom: BorderSide(color: Colors.transparent)), - color: Colors.transparent, - ), - child: Container( - child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Flexible( - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - child: Text( - widget.title, - style: TextStyle(fontSize: 24, fontWeight: FontWeight.w600), - ), - margin: EdgeInsets.only(top: 0.0, bottom: 0.0, left: 12.0, right: 12.0), - ), - SizedBox(height: 10.0), - _buildHorizontalList(widget.list, widget.onTap, widget.onTapColor, widget.onTapLoader, widget.selected, - widget.selectedFirstColor, widget.selectedSecondColor, widget.type, widget.settings) - ])) - ]))); - } - - Widget _buildHorizontalList(List list, Function? onTap, Function? onTapColor, Function? onTapLoader, String selected, - String selectedFirstColor, String selectedSecondColor, String type, Settings? settings) { - if (type == "option") { - return SizedBox( - height: 100.0, - child: new ListView( - scrollDirection: Axis.horizontal, - children: list.map((obj) { - return _buildItem(obj['image'], obj['value'], obj['url'], onTap, selected, settings!); - }).toList(), - ), - ); - } - if (type == "color") { - return SizedBox( - height: 150.0, - child: new ListView( - scrollDirection: Axis.horizontal, - children: list.map((obj) { - return _buildItemGradient(obj['title'], obj['image'], obj['firstColor'], obj['secondColor'], onTapColor, - selectedFirstColor, selectedSecondColor, settings); - }).toList(), - ), - ); - } else { - return SizedBox( - height: 120.0, - child: new ListView( - scrollDirection: Axis.horizontal, - children: list.map((obj) { - return _buildItemLoader(obj, onTapLoader, settings!); - }).toList(), - ), - ); - } - } - - Widget _buildItem(AssetImage image_, String? text, String? url, Function? onTap, String selected, Settings settings) { - double edgeSize = 0.0; - - return Container( - padding: EdgeInsets.all(edgeSize), - margin: EdgeInsets.fromLTRB(Directionality.of(context) == TextDirection.rtl ? 0 : 15, 12, - Directionality.of(context) == TextDirection.rtl ? 15 : 0, 12), - child: SizedBox( - width: 230, - child: Container( - margin: EdgeInsets.all(0.0), - padding: EdgeInsets.all(0.0), - alignment: Alignment.topCenter, - decoration: BoxDecoration( - color: Colors.transparent, - border: Border.all( - width: 0.0, - color: Colors.transparent, - ), - boxShadow: [ - new BoxShadow( - color: Colors.black.withOpacity(0.2), - offset: new Offset(2.0, 2.0), - blurRadius: 8.0, - spreadRadius: 1.0) - ]), - child: ElevatedButton( - onPressed: () { - onTap!(text, url); - }, - style: ElevatedButton.styleFrom( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)), - padding: EdgeInsets.all(0.0), - ), - child: Ink( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - HexColor(settings.firstColor).withOpacity(selected == text ? 1.0 : 0.4), - HexColor(settings.secondColor).withOpacity(selected == text ? 1.0 : 0.4) - ], - begin: Alignment.centerLeft, - end: Alignment.centerRight, - ), - borderRadius: BorderRadius.circular(12.0)), - child: new Column( - //constraints:BoxConstraints(maxWidth: 300.0, minHeight: 50.0), - //alignment: Alignment.center, - children: [ - new Expanded( - child: new Container( - decoration: new BoxDecoration( - borderRadius: BorderRadius.circular(12.0), - image: new DecorationImage( - image: image_, - fit: BoxFit.fill, - ), - ), - alignment: AlignmentDirectional.topCenter, - child: Row( - /* - children: [ - Expanded( - flex: 1, - child: Container( - child: new Text(selected), - //color: Colors.green, - ), - ), - Expanded( - flex: 1, - child: Container( - color: Colors.yellow.withOpacity(0.5), - child: new Text(text), - ), - ), - ], - */ - ), - ), - ), - ])))), - )); - } - - Widget _buildItemGradient(String title, AssetImage? image_, String? firstColor, String? secondColor, - Function? onTapColor, String selectedFirstColor, String selectedSecondColor, Settings? settings) { - double edgeSize = 0.0; - - return Container( - padding: EdgeInsets.all(edgeSize), - margin: EdgeInsets.fromLTRB(15, 12, 0, 12), - child: SizedBox( - width: 130, - child: Container( - margin: EdgeInsets.all(0.0), - padding: EdgeInsets.all(0.0), - alignment: Alignment.topCenter, - decoration: BoxDecoration( - color: Colors.transparent, - border: Border.all( - width: 0.0, - color: Colors.transparent, - ), - boxShadow: [ - new BoxShadow( - color: Colors.black.withOpacity(0.2), - offset: new Offset(2.0, 2.0), - blurRadius: 8.0, - spreadRadius: 1.0) - ]), - child: ElevatedButton( - onPressed: () { - onTapColor!(firstColor, secondColor); - }, - style: ElevatedButton.styleFrom( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)), - padding: EdgeInsets.all(0.0), - ), - child: Ink( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - HexColor(firstColor).withOpacity( - (selectedFirstColor == firstColor && selectedSecondColor == secondColor) ? 1.0 : 0.4), - HexColor(secondColor).withOpacity( - (selectedFirstColor == firstColor && selectedSecondColor == secondColor) ? 1.0 : 0.4) - ], - begin: Alignment.centerLeft, - end: Alignment.centerRight, - ), - borderRadius: BorderRadius.circular(12.0)), - child: new Column(children: [ - new Expanded( - child: new Container( - decoration: null, - alignment: AlignmentDirectional.topCenter, - child: Column( - children: [ - Expanded( - flex: 3, - child: Container( - alignment: FractionalOffset(0.5, 0.5), - child: (selectedFirstColor == firstColor && selectedSecondColor == secondColor) - ? UIImages.checked - : null, - ), - ), - Expanded( - flex: 1, - child: Container( - //color: Colors.yellow.withOpacity(0.5), - child: Text( - title, - //overflow: TextOverflow.ellipsis, - //softWrap: true, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: - (selectedFirstColor == firstColor && selectedSecondColor == secondColor) - ? Colors.white - : Colors.grey[300]), - ), - ), - ), - ], - ), - ), - ), - ])))), - )); - } - - Widget _buildItemLoader(dynamic obj, Function? onTapLoader, Settings settings) { - double edgeSize = 0.0; - - return Container( - padding: EdgeInsets.all(edgeSize), - margin: EdgeInsets.fromLTRB(15, 12, 0, 12), - child: SizedBox( - width: 100, - child: Container( - margin: EdgeInsets.all(0.0), - padding: EdgeInsets.all(0.0), - alignment: Alignment.topCenter, - decoration: BoxDecoration( - color: Colors.transparent, - border: Border.all( - width: 0.0, - color: Colors.transparent, - ), - boxShadow: [ - new BoxShadow( - color: Colors.black.withOpacity(0.2), - offset: new Offset(2.0, 2.0), - blurRadius: 8.0, - spreadRadius: 1.0) - ]), - child: ElevatedButton( - onPressed: () { - onTapLoader!(obj["value"]); - }, - style: ElevatedButton.styleFrom( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)), - padding: EdgeInsets.all(0.0), - ), - child: Ink( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - HexColor(settings.firstColor).withOpacity(settings.loader == obj["value"] ? 1.0 : 0.4), - HexColor(settings.secondColor).withOpacity(settings.loader == obj["value"] ? 1.0 : 0.4) - ], - begin: Alignment.centerLeft, - end: Alignment.centerRight, - ), - borderRadius: BorderRadius.circular(12.0)), - child: new Column(children: [ - new Expanded( - child: new Container( - decoration: null, - alignment: AlignmentDirectional.topCenter, - child: Column( - children: [ - Expanded( - flex: 3, - child: Container(alignment: FractionalOffset(0.5, 0.5), child: obj["loading"]), - ), - /* - Expanded( - flex: 1, - child: Container( - //color: Colors.yellow.withOpacity(0.5), - child: Text( - "title", - //overflow: TextOverflow.ellipsis, - //softWrap: true, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: - ( settings.loader == "RotatingCircle") - ? Colors.white - : Colors.grey[300]), - ), - ), - ), - */ - ], - ), - ), - ), - ])))), - )); - } -} diff --git a/lib/src/pages/PageScreen.dart b/lib/src/pages/PageScreen.dart deleted file mode 100644 index 8713e50e3..000000000 --- a/lib/src/pages/PageScreen.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:flutter/material.dart' hide Page; -import 'package:flutter_inappwebview/flutter_inappwebview.dart'; -import 'package:mawaqit/src/helpers/HexColor.dart'; -import 'package:mawaqit/src/models/page.dart'; -import 'package:mawaqit/src/models/settings.dart'; -import 'package:mawaqit/src/widgets/MawaqitWebViewWidget.dart'; -import 'package:mawaqit/src/services/settings_manager.dart'; -import 'package:provider/provider.dart'; - -/// displays data on dynamic pages of drawer -class PageScreen extends StatefulWidget { - final Page page; - - const PageScreen(this.page); - - @override - State createState() => new _PageScreen(); -} - -class _PageScreen extends State { - InAppWebViewController? _webViewController; - - bool isLoading = true; - - @override - Widget build(BuildContext context) { - final settingsManager = Provider.of(context); - final settings = settingsManager.settings; - - return Scaffold( - appBar: _renderAppBar(context, settings, widget.page), - body: MawaqitWebViewWidget( - path: widget.page.url, - clean: true, - ), - ); - } -} - -AppBar _renderAppBar(context, Settings settings, Page page) { - return AppBar( - title: Text( - page.title!, - style: TextStyle(color: Colors.white, fontSize: 22.0, fontWeight: FontWeight.bold), - ), - flexibleSpace: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.centerLeft, - end: Alignment.centerRight, - colors: [ - Theme.of(context).brightness == Brightness.light - ? HexColor(settings.firstColor) - : Theme.of(context).primaryColor, - Theme.of(context).brightness == Brightness.light - ? HexColor(settings.secondColor) - : Theme.of(context).primaryColor, - ], - ), - ), - )); -} - -const extractContent = ''' - const header = document.querySelector(".header"); - header?.parentElement.removeChild(header); - - const footer = document.querySelector(".footer"); - footer?.parentElement.removeChild(footer); - - const breadcrumb = document.querySelector(".breadcrumb"); - breadcrumb?.parentElement.removeChild(breadcrumb); -'''; diff --git a/lib/src/pages/SplashScreen.dart b/lib/src/pages/SplashScreen.dart index f322a41c0..176b4539b 100644 --- a/lib/src/pages/SplashScreen.dart +++ b/lib/src/pages/SplashScreen.dart @@ -20,12 +20,10 @@ import 'package:mawaqit/src/helpers/PerformanceHelper.dart'; import 'package:mawaqit/src/helpers/RelativeSizes.dart'; import 'package:mawaqit/src/helpers/SharedPref.dart'; import 'package:mawaqit/src/helpers/StreamGenerator.dart'; -import 'package:mawaqit/src/models/settings.dart'; import 'package:mawaqit/src/pages/ErrorScreen.dart'; import 'package:mawaqit/src/pages/home/OfflineHomeScreen.dart'; import 'package:mawaqit/src/pages/onBoarding/OnBoardingScreen.dart'; import 'package:mawaqit/src/services/mosque_manager.dart'; -import 'package:mawaqit/src/services/settings_manager.dart'; import 'package:mawaqit/src/state_management/random_hadith/random_hadith_notifier.dart'; import 'package:mawaqit/src/widgets/InfoWidget.dart'; import 'package:provider/provider.dart'; @@ -91,13 +89,10 @@ class _SpashState extends ConsumerState { prefs.setBool("isEventsSet", false); } - Future _initSettings() async { + Future _initSettings() async { FeatureManagerProvider.initialize(context); await context.read().fetchLocale(); await context.read().init().logPerformance("Mosque manager"); - final settingsManage = context.read(); - await settingsManage.init().logPerformance('Setting manager'); - return settingsManage.settings; } Future loadBoarding() async { @@ -111,6 +106,7 @@ class _SpashState extends ConsumerState { try { await initApplicationUI(); var settings = await _initSettings(); + var goBoarding = await loadBoarding(); var mosqueManager = context.read(); bool hasNoMosque = mosqueManager.mosqueUUID == null; @@ -118,7 +114,7 @@ class _SpashState extends ConsumerState { /// waite for the animation if it is not loaded yet await animationFuture.future; - if (hasNoMosque || goBoarding && settings.boarding == "1") { + if (hasNoMosque || goBoarding) { AppRouter.pushReplacement(OnBoardingScreen()); } else { AppRouter.pushReplacement(OfflineHomeScreen()); diff --git a/lib/src/pages/WebScreen.dart b/lib/src/pages/WebScreen.dart deleted file mode 100644 index 15392c78e..000000000 --- a/lib/src/pages/WebScreen.dart +++ /dev/null @@ -1,188 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_inappwebview/flutter_inappwebview.dart'; -import 'package:geolocator/geolocator.dart'; -import 'package:global_configuration/global_configuration.dart'; -import 'package:mawaqit/i18n/l10n.dart'; -import 'package:mawaqit/src/elements/RaisedGradientButton.dart'; -import 'package:mawaqit/src/domain/model/connectivity_status.dart'; -import 'package:mawaqit/src/helpers/HexColor.dart'; -import 'package:mawaqit/src/models/settings.dart'; -import 'package:mawaqit/src/services/settings_manager.dart'; -import 'package:mawaqit/src/themes/UIImages.dart'; -import 'package:mawaqit/src/widgets/MawaqitWebViewWidget.dart'; -import 'package:provider/provider.dart'; -import 'package:uni_links/uni_links.dart'; - -class WebScreen extends StatefulWidget { - final String? url; - - const WebScreen(this.url); - - @override - State createState() { - return new _WebScreen(); - } -} - -class _WebScreen extends State { - InAppWebViewController? get _webViewController => webViewKey.currentState?.webViewController; - final webViewKey = GlobalKey(); - - PullToRefreshController? pullToRefreshController; - - List> webViewGPSPositionStreams = []; - - StreamSubscription? _sub; - - @override - void initState() { - super.initState(); - - pullToRefreshController = PullToRefreshController( - options: PullToRefreshOptions(color: Colors.blue), - onRefresh: () async { - if (Platform.isAndroid) { - _webViewController?.reload(); - } else if (Platform.isIOS) { - _webViewController?.loadUrl(urlRequest: URLRequest(url: await _webViewController?.getUrl())); - } - }, - ); - - _handleIncomingLinks(); - } - - void _handleIncomingLinks() { - if (!kIsWeb) { - _sub = uriLinkStream.listen((Uri? uri) { - var link = uri.toString().replaceAll('${GlobalConfiguration().getValue('deeplink')}://url/', ''); - _webViewController?.loadUrl(urlRequest: URLRequest(url: Uri.parse(link))); - }, onError: (Object err) {}); - } - } - - @override - void dispose() { - _sub?.cancel(); - - webViewGPSPositionStreams - .forEach((StreamSubscription _flutterGeolocationStream) => _flutterGeolocationStream.cancel()); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - MediaQueryData mediaQueryData = MediaQuery.of(context); - var bottomPadding = mediaQueryData.padding.bottom; - var connectionStatus = Provider.of(context); - final settingsManager = Provider.of(context); - final settings = settingsManager.settings; - - if (connectionStatus == ConnectivityStatus.Offline) return _offline(bottomPadding, settings); - - return WillPopScope( - onWillPop: _onBackPressed, - child: Container( - decoration: BoxDecoration(color: HexColor("#f5f4f4")), - padding: EdgeInsets.only(bottom: bottomPadding), - child: Scaffold( - appBar: _renderAppBar(context, settings), - body: MawaqitWebViewWidget(path: widget.url, key: webViewKey), - ), - ), - ); - } - - Widget _offline(bottomPadding, Settings settings) { - return WillPopScope( - onWillPop: _onBackPressed, - child: Container( - decoration: BoxDecoration(color: HexColor("#f5f4f4")), - padding: EdgeInsets.only(bottom: bottomPadding), - child: Scaffold( - body: Column( - children: [ - Container( - height: 130, - ), - Column(crossAxisAlignment: CrossAxisAlignment.center, children: [ - Container( - width: 100.0, - height: 100.0, - child: Image.asset( - UIImages.imageDir + "/wifi.png", - color: Colors.black26, - fit: BoxFit.contain, - )), - SizedBox(height: 40), - Text( - S.of(context).whoops, - style: TextStyle(color: Colors.black45, fontSize: 40.0, fontWeight: FontWeight.bold), - ), - SizedBox(height: 20), - Text( - S.of(context).noInternet, - style: TextStyle(color: Colors.black87, fontSize: 15.0), - ), - SizedBox(height: 5), - SizedBox(height: 60), - RaisedGradientButton( - child: Text( - S.of(context).tryAgain, - style: TextStyle(color: Colors.white, fontSize: 18.0, fontWeight: FontWeight.bold), - ), - width: 250, - gradient: LinearGradient( - colors: [HexColor(settings.secondColor), HexColor(settings.firstColor)], - ), - onPressed: () {}), - ]), - Container( - height: 100, - ), - ], - ), - )), - ); - } - - PreferredSizeWidget _renderAppBar(context, Settings settings) { - return AppBar( - flexibleSpace: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.centerLeft, - end: Alignment.centerRight, - colors: [ - Theme.of(context).brightness == Brightness.light - ? HexColor(settings.firstColor) - : Theme.of(context).primaryColor, - Theme.of(context).brightness == Brightness.light - ? HexColor(settings.secondColor) - : Theme.of(context).primaryColor, - ], - ), - ), - )); - } - - Future _onBackPressed() async { - try { - if (_webViewController != null) { - if (await _webViewController!.canGoBack()) { - _webViewController!.goBack(); - return false; - } else { - Navigator.pop(context); - } - } - } catch (e) { - Navigator.pop(context); - } - return true; - } -} diff --git a/lib/src/repository/settings_service.dart b/lib/src/repository/settings_service.dart deleted file mode 100644 index cb7261aa1..000000000 --- a/lib/src/repository/settings_service.dart +++ /dev/null @@ -1,70 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter/cupertino.dart'; -import 'package:flutter/services.dart'; -import 'package:global_configuration/global_configuration.dart'; -import 'package:http/http.dart' as http; -import 'package:mawaqit/const/resource.dart'; -import 'package:mawaqit/src/helpers/SharedPref.dart'; -import 'package:mawaqit/src/models/settings.dart'; - -ValueNotifier setting = new ValueNotifier(new Settings()); - -class SettingsService { - final _sharedPref = SharedPref(); - - /// fetch the setting from the server and cache it for future usage - /// - /// 1. load settings from server - /// 2. in case of server error uses the last cached settings value - /// 3. in case of not exists uses the default value in `assets/cfg/settings.json` - Future getSettings() async { - try { - var res = await http - .get( - Uri.parse( - '${GlobalConfiguration().getValue('api_base_url')}/api/settings/settings.php', - ), - ) - .timeout(Duration(seconds: 5)); - - if (res.statusCode == 200) { - final json = jsonDecode(res.body); - - Settings settings = Settings.fromJson(json["data"]); - - saveCachedSettings(json['data']); - - return settings; - } else { - throw Exception('Getting local saved settings'); - } - } catch (e) { - var localSettings = await getCachedSettings().catchError((e) => null); - localSettings ??= await getLocalSettings(); - - if (localSettings == null) throw Exception('Failed to load /api/settings'); - - return localSettings; - } - } - - Future saveCachedSettings(dynamic settings) => _sharedPref.save("settings", settings); - - /// used for performance improvement (initial start up time) - /// used for fall back in case of server down - Future getCachedSettings() async { - final settings = await _sharedPref.read('settings'); - - if (settings == null) return null; - - return Settings.fromJson(settings); - } - - Future getLocalSettings() async { - final data = await rootBundle.loadString(R.ASSETS_CFG_SETTINGS_JSON); - - final settings = jsonDecode(data); - return Settings.fromJson(settings); - } -} diff --git a/lib/src/services/settings_manager.dart b/lib/src/services/settings_manager.dart deleted file mode 100644 index 9486427ae..000000000 --- a/lib/src/services/settings_manager.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:mawaqit/main.dart'; -import 'package:mawaqit/src/helpers/SharedPref.dart'; -import 'package:mawaqit/src/models/settings.dart'; -import 'package:mawaqit/src/repository/settings_service.dart'; - -class SettingsManager extends ChangeNotifier { - final settingsService = SettingsService(); - final sharedPref = SharedPref(); - - Settings? _settings; - - Settings get settings => _settings!; - - bool get settingsLoaded => _settings != null; - - /// 1- check for cached value first to speed up the first load time - /// 2- fetch the new value and cache it for future use - Future init() async { - _settings = await settingsService.getCachedSettings().catchError((e) => null); - - if (_settings != null) { - if (hasListeners) notifyListeners(); - } - - _settings = await settingsService.getSettings(); - notifyListeners(); - } -} diff --git a/lib/src/widgets/MawaqitDrawer.dart b/lib/src/widgets/MawaqitDrawer.dart index bfd7fddc9..bd6edc767 100644 --- a/lib/src/widgets/MawaqitDrawer.dart +++ b/lib/src/widgets/MawaqitDrawer.dart @@ -1,9 +1,8 @@ import 'dart:developer'; -import 'dart:io'; import 'package:flutter/material.dart' hide Page; import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart' show ConsumerWidget, WidgetRef, ProviderContainer; +import 'package:flutter_riverpod/flutter_riverpod.dart' show ConsumerWidget, WidgetRef; import 'package:flutter_svg/svg.dart'; import 'package:launch_review/launch_review.dart'; import 'package:mawaqit/const/resource.dart'; @@ -12,16 +11,8 @@ import 'package:mawaqit/src/const/constants.dart'; import 'package:mawaqit/src/elements/DrawerListTitle.dart'; import 'package:mawaqit/src/helpers/AppRouter.dart'; import 'package:mawaqit/src/helpers/RelativeSizes.dart'; -import 'package:mawaqit/src/helpers/StringUtils.dart'; -import 'package:mawaqit/src/models/menu.dart'; -import 'package:mawaqit/src/models/page.dart'; -import 'package:mawaqit/src/models/settings.dart'; import 'package:mawaqit/src/pages/AboutScreen.dart'; -import 'package:mawaqit/src/pages/PageScreen.dart'; -import 'package:mawaqit/src/pages/WebScreen.dart'; import 'package:mawaqit/src/pages/quran/page/reciter_selection_screen.dart'; -import 'package:mawaqit/src/services/mosque_manager.dart'; -import 'package:mawaqit/src/services/settings_manager.dart'; import 'package:mawaqit/src/services/user_preferences_manager.dart'; import 'package:mawaqit/src/widgets/InfoWidget.dart'; import 'package:provider/provider.dart'; @@ -42,8 +33,6 @@ class MawaqitDrawer extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final settings = Provider.of(context).settings; - final mosqueManager = context.watch(); final userPrefs = context.watch(); final theme = Theme.of(context); @@ -159,15 +148,10 @@ class MawaqitDrawer extends ConsumerWidget { icon: Icons.home, text: S.of(context).home, onTap: () async { - if (settings.tabNavigationEnable == "1") { - AppRouter.popAndPush(WebScreen(settings.url), name: 'HomeScreen'); - } else { - Navigator.pop(context); + Navigator.pop(context); - goHome(); - } + goHome(); }), - _renderMenuDrawer(settings, context), DrawerListTitle( icon: Icons.book, text: S.of(context).quran, @@ -211,7 +195,8 @@ class MawaqitDrawer extends ConsumerWidget { icon: Icons.share, text: S.of(context).share, onTap: () { - _shareApp(context, settings.title, settings.share!); + _shareApp(context, MawaqitBackendSettingsConstant.kSettingsTitle, + MawaqitBackendSettingsConstant.kSettingsShare); }), DrawerListTitle( icon: Icons.star, @@ -226,43 +211,6 @@ class MawaqitDrawer extends ConsumerWidget { ); } - Widget _renderMenuDrawer(Settings settings, BuildContext context) { - List menus = settings.menus ?? []; - - return new Column( - children: menus - .map((Menu menu) => DrawerListTitle( - iconUrl: menu.iconUrl, - forceThemeColor: true, - autoTranslate: true, - text: menu.title, - onTap: () async { - AppRouter.push(WebScreen(menu.url), name: menu.title); - Navigator.pop(context); - })) - .toList(), - ); - } - - Widget _renderPageDrawer(List pages, context) { - final translations = { - "privacyPolicy": S.of(context).privacyPolicy, - "networkStatus": S.of(context).networkStatus, - "termsOfService": S.of(context).termsOfService, - "installationGuide": S.of(context).installationGuide, - }; - - return Column( - children: pages - .map((Page page) => DrawerListTitle( - forceThemeColor: true, - iconUrl: page.iconUrl, - text: translations[page.title!.toCamelCase] ?? page.title, - onTap: () => AppRouter.popAndPush(PageScreen(page), name: page.title))) - .toList(), - ); - } - _shareApp(BuildContext context, String? text, String share) { final RenderBox box = context.findRenderObject() as RenderBox; Share.share(share, subject: text, sharePositionOrigin: box.localToGlobal(Offset.zero) & box.size); diff --git a/lib/src/widgets/MawaqitWebViewWidget.dart b/lib/src/widgets/MawaqitWebViewWidget.dart index 918c06244..4b0427ca4 100644 --- a/lib/src/widgets/MawaqitWebViewWidget.dart +++ b/lib/src/widgets/MawaqitWebViewWidget.dart @@ -12,11 +12,11 @@ import 'package:geolocator/geolocator.dart'; import 'package:mawaqit/const/resource.dart'; //import 'package:location/location.dart' hide LocationAccuracy; import 'package:mawaqit/i18n/l10n.dart'; +import 'package:mawaqit/src/const/constants.dart'; import 'package:mawaqit/src/domain/model/position/PositionResponse.dart'; import 'package:mawaqit/src/elements/Loader.dart'; import 'package:mawaqit/src/helpers/HexColor.dart'; import 'package:mawaqit/src/pages/OfflineScreen.dart'; -import 'package:mawaqit/src/services/settings_manager.dart'; import 'package:provider/provider.dart'; import 'package:store_redirect/store_redirect.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -119,7 +119,6 @@ class MawaqitWebViewWidgetState extends State Widget build(BuildContext context) { super.build(context); print(widget.path); - final settings = Provider.of(context).settings; final userPreferences = context.watch(); return Focus( @@ -151,7 +150,9 @@ class MawaqitWebViewWidgetState extends State useShouldOverrideUrlLoading: true, useOnDownloadStart: true, mediaPlaybackRequiresUserGesture: false, - userAgent: Platform.isAndroid ? settings.userAgent!.valueAndroid! : settings.userAgent!.valueIOS!, + userAgent: Platform.isAndroid + ? MawaqitBackendSettingsConstant.kSettingsAndroidUserAgent + : MawaqitBackendSettingsConstant.kSettingsIosUserAgent, ), android: AndroidInAppWebViewOptions( useHybridComposition: true, @@ -159,7 +160,6 @@ class MawaqitWebViewWidgetState extends State ios: IOSInAppWebViewOptions( allowsInlineMediaPlayback: true, )), - pullToRefreshController: settings.pullRefresh == "1" ? pullToRefreshController : null, onWebViewCreated: (InAppWebViewController controller) { webViewController = controller; controller.addJavaScriptHandler( @@ -266,7 +266,7 @@ class MawaqitWebViewWidgetState extends State print(consoleMessage); }, ), - (isLoading && settings.loader != "empty") + (isLoading) ? Positioned( top: 0, bottom: 0, @@ -275,9 +275,9 @@ class MawaqitWebViewWidgetState extends State child: Container( color: Theme.of(context).scaffoldBackgroundColor, child: Loader( - type: settings.loader, + type: "Circle", color: Theme.of(context).brightness == Brightness.light - ? HexColor(settings.loaderColor) + ? HexColor("#490094") : Theme.of(context).primaryColor), ), ) From 763111515f9893589844e3d45abe83e83605a125 Mon Sep 17 00:00:00 2001 From: Ghassen Ben Zahra Date: Sun, 10 Nov 2024 21:57:51 +0100 Subject: [PATCH 2/9] Feat/ Add a Third Jumua Section (#1395) * add section for third jumua time * refactor(JumuaWidget): improve handling of multiple jumua times - Add getOrderedJumuaTimes method to collect and order available jumua times - Refactor jumuaTile to handle cases where jumua1 is null but jumua2/3 exist - Remove direct dependency on jumua field being non-null --------- Co-authored-by: Yassin --- lib/src/models/times.dart | 6 + .../home/widgets/salah_items/SalahItem.dart | 123 +++++++++++++----- lib/src/pages/times/widgets/jumua_widget.dart | 31 ++++- 3 files changed, 125 insertions(+), 35 deletions(-) diff --git a/lib/src/models/times.dart b/lib/src/models/times.dart index ce30f7379..763c651d4 100644 --- a/lib/src/models/times.dart +++ b/lib/src/models/times.dart @@ -6,6 +6,7 @@ import 'package:mawaqit/src/helpers/time_utils.dart'; class Times { final String? jumua; final String? jumua2; + final String? jumua3; final String? aidPrayerTime; final String? aidPrayerTime2; final int hijriAdjustment; @@ -70,6 +71,7 @@ class Times { const Times({ required this.jumua, required this.jumua2, + required this.jumua3, required this.aidPrayerTime, required this.aidPrayerTime2, required this.hijriAdjustment, @@ -88,6 +90,7 @@ class Times { runtimeType == other.runtimeType && jumua == other.jumua && jumua2 == other.jumua2 && + jumua3 == other.jumua3 && aidPrayerTime == other.aidPrayerTime && aidPrayerTime2 == other.aidPrayerTime2 && hijriAdjustment == other.hijriAdjustment && @@ -101,6 +104,7 @@ class Times { int get hashCode => jumua.hashCode ^ jumua2.hashCode ^ + jumua3.hashCode ^ aidPrayerTime.hashCode ^ aidPrayerTime2.hashCode ^ hijriAdjustment.hashCode ^ @@ -115,6 +119,7 @@ class Times { return 'Times{' + ' jumua: $jumua,' + ' jumua2: $jumua2,' + + ' jumua3: $jumua3,' + ' aidPrayerTime: $aidPrayerTime,' + ' aidPrayerTime2: $aidPrayerTime2,' + ' hijriAdjustment: $hijriAdjustment,' + @@ -132,6 +137,7 @@ class Times { return Times( jumua: map['jumua'] ?? map['jumua2'], jumua2: replacedJumua, + jumua3: map['jumua3'], aidPrayerTime: map['aidPrayerTime'], aidPrayerTime2: map['aidPrayerTime2'], hijriAdjustment: map['hijriAdjustment'] ?? -1, diff --git a/lib/src/pages/home/widgets/salah_items/SalahItem.dart b/lib/src/pages/home/widgets/salah_items/SalahItem.dart index a1c1178cc..381ba0a77 100644 --- a/lib/src/pages/home/widgets/salah_items/SalahItem.dart +++ b/lib/src/pages/home/widgets/salah_items/SalahItem.dart @@ -14,6 +14,7 @@ class SalahItemWidget extends StatelessOrientationWidget { required this.time, this.title, this.iqama, + this.iqama2, this.active = false, this.removeBackground = false, this.withDivider = true, @@ -24,6 +25,7 @@ class SalahItemWidget extends StatelessOrientationWidget { final String? title; final String time; final String? iqama; + final String? iqama2; /// show divider only when both time and iqama exists final bool withDivider; @@ -76,39 +78,98 @@ class SalahItemWidget extends StatelessOrientationWidget { ), ), SizedBox(height: 1.vr), - if (time.trim().isEmpty) - Icon(Icons.dnd_forwardslash, size: 6.vwr) - else - Container( - decoration: (iqama != null && showIqama && withDivider) - ? BoxDecoration( - border: Border(bottom: BorderSide(color: Colors.white, width: 1)), - ) - : null, - child: TimeWidget.fromString( - show24hFormat: !is12period, - time: time, - style: TextStyle( - fontSize: isIqamaMoreImportant ? smallFont : bigFont, - fontWeight: FontWeight.w700, - shadows: kHomeTextShadow, - color: Colors.white, + if (iqama2 != null) // Three times layout + Column( + children: [ + TimeWidget.fromString( + show24hFormat: !is12period, + time: time, + style: TextStyle( + fontSize: bigFont, + fontWeight: FontWeight.w700, + shadows: kHomeTextShadow, + color: Colors.white, + height: 1, + ), + ), + Container( + margin: EdgeInsets.symmetric(vertical: 1.vr), + width: 20.vwr, // Adjust this value to match your needs height: 1, - // fontFamily: StringManager.getFontFamily(context), + color: Colors.white, ), - ), - ), - if (iqama != null && showIqama) - TimeWidget.fromString( - show24hFormat: !is12period, - time: iqama!, - style: TextStyle( - fontSize: isIqamaMoreImportant ? bigFont : smallFont, - fontWeight: FontWeight.bold, - shadows: kHomeTextShadow, - letterSpacing: 1, - color: Colors.white, - ), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + TimeWidget.fromString( + show24hFormat: !is12period, + time: iqama!, + style: TextStyle( + fontSize: smallFont, + fontWeight: FontWeight.w700, + shadows: kHomeTextShadow, + color: Colors.white, + height: 1, + ), + ), + Container( + height: bigFont, + width: 1, + margin: EdgeInsets.symmetric(horizontal: 1.vwr), + color: Colors.white, + ), + TimeWidget.fromString( + show24hFormat: !is12period, + time: iqama2!, + style: TextStyle( + fontSize: smallFont, + fontWeight: FontWeight.w700, + shadows: kHomeTextShadow, + color: Colors.white, + height: 1, + ), + ), + ], + ), + ], + ) + else // Original two times layout + Column( + children: [ + if (time.trim().isEmpty) + Icon(Icons.dnd_forwardslash, size: 6.vwr) + else + Container( + decoration: (iqama != null && showIqama && withDivider) + ? BoxDecoration( + border: Border(bottom: BorderSide(color: Colors.white, width: 1)), + ) + : null, + child: TimeWidget.fromString( + show24hFormat: !is12period, + time: time, + style: TextStyle( + fontSize: isIqamaMoreImportant ? smallFont : bigFont, + fontWeight: FontWeight.w700, + shadows: kHomeTextShadow, + color: Colors.white, + height: 1, + ), + ), + ), + if (iqama != null && showIqama) + TimeWidget.fromString( + show24hFormat: !is12period, + time: iqama!, + style: TextStyle( + fontSize: isIqamaMoreImportant ? bigFont : smallFont, + fontWeight: FontWeight.bold, + shadows: kHomeTextShadow, + letterSpacing: 1, + color: Colors.white, + ), + ), + ], ), ], ), diff --git a/lib/src/pages/times/widgets/jumua_widget.dart b/lib/src/pages/times/widgets/jumua_widget.dart index 690546f55..4ac15fb15 100644 --- a/lib/src/pages/times/widgets/jumua_widget.dart +++ b/lib/src/pages/times/widgets/jumua_widget.dart @@ -11,12 +11,22 @@ import 'package:provider/provider.dart'; class JumuaWidget extends StatelessWidget { const JumuaWidget({super.key}); + List getOrderedJumuaTimes(MosqueManager mosqueManager) { + final times = mosqueManager.times; + List jumuaTimes = []; + + if (times?.jumua != null) jumuaTimes.add(times!.jumua!); + if (times?.jumua2 != null) jumuaTimes.add(times!.jumua2!); + if (times?.jumua3 != null) jumuaTimes.add(times!.jumua3!); + + return jumuaTimes; + } + @override Widget build(BuildContext context) { final mosqueManager = context.watch(); final userPrefs = context.watch(); - /// show eid instead of jumuaa if its eid time and eid is enabled if (mosqueManager.showEid(userPrefs.hijriAdjustments)) { return FadeInOutWidget( first: eidWidget(mosqueManager, context), @@ -43,14 +53,27 @@ class JumuaWidget extends StatelessWidget { } Widget jumuaTile(MosqueManager mosqueManager, BuildContext context) { + final jumuaTimes = getOrderedJumuaTimes(mosqueManager); + + if (jumuaTimes.isEmpty) { + return SalahItemWidget( + withDivider: true, + removeBackground: true, + title: S.of(context).jumua, + time: "", + isIqamaMoreImportant: false, + ); + } + return SalahItemWidget( withDivider: true, removeBackground: true, title: S.of(context).jumua, - time: !mosqueManager.isJumuaOrJumua2EmptyOrNull() ? DateFormat.Hm().format(mosqueManager.activeJumuaaDate()) : "", - iqama: mosqueManager.times!.jumua2, + time: jumuaTimes[0], + iqama: jumuaTimes.length > 1 ? jumuaTimes[1] : null, + iqama2: jumuaTimes.length > 2 ? jumuaTimes[2] : null, isIqamaMoreImportant: false, - active: mosqueManager.nextIqamaIndex() == 1 && AppDateTime.isFriday && mosqueManager.times?.jumua != null, + active: mosqueManager.nextIqamaIndex() == 1 && AppDateTime.isFriday, ); } } From f5b698b8ba66c4eaadd7be57f828ac93012b738b Mon Sep 17 00:00:00 2001 From: Ghassen Ben Zahra Date: Sun, 10 Nov 2024 21:58:01 +0100 Subject: [PATCH 3/9] switch condition when appLang is ar (#1407) --- lib/src/pages/home/widgets/FlashWidget.dart | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/src/pages/home/widgets/FlashWidget.dart b/lib/src/pages/home/widgets/FlashWidget.dart index f8f179685..5db8cdad4 100644 --- a/lib/src/pages/home/widgets/FlashWidget.dart +++ b/lib/src/pages/home/widgets/FlashWidget.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:marquee/marquee.dart'; +import 'package:mawaqit/i18n/AppLanguage.dart'; import 'package:mawaqit/src/helpers/RelativeSizes.dart'; import 'package:mawaqit/src/models/mosque.dart'; import 'package:provider/provider.dart'; @@ -21,11 +22,22 @@ class _FlashWidgetState extends State { final isFlashEnabled = context.select((mosque) => mosque.flashEnabled); final flash = context.select((mosque) => mosque.mosque?.flash); if (!isFlashEnabled) return SizedBox(); + final isPortrait = MediaQuery.of(context).orientation == Orientation.portrait; + final appLanguage = Provider.of(context); + if (!isFlashEnabled) return SizedBox(); + + TextDirection getTextDirection() { + if (isPortrait && appLanguage.appLocal.toLanguageTag() == "ar") { + return flash?.orientation == 'rtl' ? TextDirection.ltr : TextDirection.rtl; + } else { + return flash?.orientation == 'rtl' ? TextDirection.rtl : TextDirection.ltr; + } + } return RepaintBoundary( child: Marquee( key: ValueKey(flash!.content), - textDirection: flash.orientation == 'rtl' ? TextDirection.rtl : TextDirection.ltr, + textDirection: getTextDirection(), text: flash.content ?? '', velocity: 90, blankSpace: 400, From 4148696d2f6922f60e6b93083608e8e69af748cc Mon Sep 17 00:00:00 2001 From: Yassin Nouh <70436855+YassinNouh21@users.noreply.github.com> Date: Sat, 23 Nov 2024 16:39:04 +0200 Subject: [PATCH 4/9] chore: Replace Israeli flag asset with Hebrew logo (#1437) --- assets/img/flag/he.png | Bin 2429 -> 42652 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/assets/img/flag/he.png b/assets/img/flag/he.png index bc8a0b589e8ac80894dd7ead1c1882e6e8e25c44..e87d1b0622f910f5e8b9069fdd2f8001f89f12a3 100644 GIT binary patch literal 42652 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 F{{!F~)wlov literal 2429 zcmV-@34->CP) zK~#90WYC22}e+Ju&{wX|u9 zA?N_v4kQJWa6&jqN={1>o1RbzZ36VP1QyF8KL^KjW8h}^;JOI6?TO$AgfE7RqW9$kM z6>Bt_BQY^CHKF9kLZMU?#R&iw5YY?(nL&(00E!u72Z-onp65?1M$!Yt98h0hZ_sEo z&jWZCK!#F!JA;TWF~&Z0xm?@LX0u%hok2T56h)IHNy`~yOCe8fHDD#8JjU1-p69K> zDC)NZh=>V-@C;*Y6998CB!WN#5v}8Sek)^4>W_kcIY1P}R7sL{GR7wLM_y?VQ6Xb& zArMY&IpD?juf!CV)?O?u5tDl(^41v zis~O&m{+tl(ETnQ@avDxKeV^Vx+HY8)J4Iunim#tKRvChdwvd>pP#Qebk@4J$) zjndsfv(wEEU$X6)pP#R}^Pcw3hLxkIEV&ZfY*2@qsxXX9%ve+aZ0p?U^Dm2{Xdz*;lUUiWwlzz0v$78 zfoh`zc>T{6s1}{x>sp%&8~0UoBcVJPV^0Ac4w$Ldz@=3!c<*nw0RU!$4rYT60I>bg zZIo8Es6_(+rUL+Hx7*{1Xp~x`=6-qu&NexHx8|`FtbSD9YdPB_tj@oo77YN%ve|5L zOb~=802C;tb^hjJ{OelajJ43zk*G07$$_ZY+|kBI`vs?7Oc)-6f)$wxu#`bWk86o& zG-Jwz{kijo;^Fn@AW_dtTW7=D(b++r!5Hw;TxD}I9LHs98Dr^6D!ng7kDP2XJ~<{= zw9Ez_GLxcl`Bt;{#Wy@55<>=r`^ep_ci>vJ{0>Mkh9M&{3dNO8sI|$tj3=|>F>Y9l zl2jo{Qo0tv2t~C5ST{cf`~O`Jd#f8QP6@9q7>3zzTmS$>=s0|~ax{|Td>wGB!GZCs ziqPVe;AoTZ`cuO&W5YQBKy#+(Dj(S?k{3OrWufH+dkL5SQ-3f6r)3&yt z49(8KHOy9*g!O+a_i0=G=?%2H`c?Pr4eT5MMe6K#;++GxaJ@!e)(nwyD(<%6#OA+L z29$Yge5A=w?Wgq+;ed##V(8 zofx|lKghd;FU~dq0Jbbl1p!#GvlIaENR|noELUx3E^!=}zyLrHgbPq9C01J1f=Mr* zfwN8WCPQj%E`(`099uUQsRJX`paB49c%HwX0|3U@LA6Fr9TR9QikMlaXNT*xZ;6I{a#my5}$2ZRP;La{^P*mtl9%M`*BWUWdVJ z9!v4A`D%cOF2%;io&|jC0y|Y>0N|Oa2^gEA$C%`3JUt~|&7lF{{q`T;H-BwyZH!K* zE64w9Y0PXk+leUPNs3S)dxSx4bNN$Dn&No;TM z3w};su3CDzvN2CxUg{uYNX*Y49GUQL=Qh8%bJK;x+rByxzy54ld;ihp{qBV4zIyW1-qW=cLc&ZX%pDhZVsGxm>_GRsO^Vu_&6Bdz z3}uS5QVCN=^5yqWYWY@oeR`bamVN(;`m*ra_2XB?1m&oO9@%Mzs#%j$hg;?hZtkvM zk7;qsoWaeh0}R8nM;KJCT?d27>BjQOql~FN84vU!31YDjcQNwRk=-Y2zZRd#1j5WL zQ;{*F^1-g&_+6pT@A;$WZ$6!9ihP?f@O`HjIuKy5797hbW+r~v7rA~dbb=rZV~qWt zh`N2jsEES=e$4ZHpC3~tKAYPJK=u8s2MqvzYcv{nFvj{7xCI^RSglr`bpb zSbe=jJ!5Q(+wIQlys_yI6pQt0YHIX4o$h&#{_kXv7U2yGXUI+UM#l(AOScJ;2;t0YENS+iI7M8Hk&QZ?RHP)IBpaXWkCLX-h{3{ vpKl Date: Wed, 27 Nov 2024 15:09:01 +0100 Subject: [PATCH 5/9] Feature/ Introduce New Camera Connection Feature for Android TV App (#1392) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * working local app link rtsp * feat: add new localization keys and placeholders for Quran-related st… (#1402) * feat: add new localization keys and placeholders for Quran-related strings - Added new keys for Quran reading page placeholders in portrait mode in intl_sq.arb and intl_bs.arb - Introduced 'switchQuranType' placeholder with 'name' in intl_sq.arb and intl_bs.arb - Updated onboarding_language_selector.dart to include debug print for locale language code - Ensured proper formatting with newlines at the end of arb files * fix: remove Montenegrin because flutter doesn't support it * remove the print * Feat/quran/auto scrolling reading (#1389) * feat(auto_reading): add state management for auto reading feature - Implement `AutoScrollState` to handle auto scroll speed, visibility, and font size settings. - Add `AutoScrollNotifier` to manage auto-scrolling functionality with start, stop, and speed control. - Include derived properties for controlling the visibility of speed control and scroll behavior. - Support toggling between single-page view and auto-scrolling. * feat(quran): add play toggle button and refactor directory structure - Add play toggle button to `QuranReadingScreen` with portrait and landscape support. - Move `quran_reading_screen.dart` to new `reading` directory for better organization. - Create `QuranFloatingActionButtons` widget for handling floating action buttons in portrait and landscape modes. * refactor: Extract floating action controls into new widget with passed focus nodes - Extracted floating action controls into `QuranFloatingActionControls` widget. - Used `OrientationBuilder` within the new widget to determine orientation internally. - Passed focus nodes from `QuranReadingScreen` to the new widget for external focus management. - Maintained existing UI and design without modifications. * feat: add the to_string and making the AutoScrollNotifier auto disposed * modify the new ui * feat: add auto-scrolling reading mode with font size and speed controls - QuranFloatingActionControls: - Implemented `_buildAutoScrollingReadingMode` to display controls when auto-scroll is active. - Added methods: - `_buildFontSizeControls` for adjusting font size. - `_buildSpeedControls` for adjusting auto-scroll speed. - `_buildPlayPauseButton` for toggling auto-scroll. - `_buildActionButton` as a helper for creating action buttons. - Modified `_buildFloatingPortrait` and `_buildFloatingLandscape` to display auto-scroll controls based on the current state. - AutoScrollState: - Fixed `isAutoScrolling` getter to correctly represent the auto-scrolling state. - AutoScrollNotifier: - Added methods: - `increaseFontSize` and `decreaseFontSize` to adjust font size. - `increaseSpeed` and `decreaseSpeed` to adjust auto-scroll speed. - Updated `startAutoScroll` to use dynamic speed settings. * fix: scrolling functionality and refactor Quran reading code - Implement auto-scrolling that aligns with the current page and page height. - Refactor floating action buttons into separate widget classes for better code organization. - Update auto-scroll state and notifier to handle scroll controller and dynamic speed adjustments. * refactor QuranReadingScreen: Remove unused imports and redundant widget functions - Removed unnecessary imports such as SvgPicture and ReciterSelectionScreen. - Cleaned up redundant widget methods like `buildFloatingPortrait`, `buildFloatingLandscape`, and other floating action button handlers. - Simplified the UI logic by eliminating unused `QuranModeButton` and `PlayToggleButton` widgets. * merge on main * refactor: remove unused floating action buttons * refactor: migrate screen rotation to state management - Add isRotated field to QuranReadingState to manage rotation state - Add toggleRotation method to QuranReadingNotifier - Remove local ValueNotifier for rotation management - Update QuranFloatingActionControls to use state-managed rotation - Simplify _OrientationToggleButton to use state rotation - Remove orientation dependencies from UserPreferencesManager * refactor(quran): improve keyboard navigation and focus management - Replace custom key event handlers with FocusTraversalPolicy for better focus management - Add ArrowButtonsFocusTraversalPolicy to handle navigation between left/right buttons - Implement up/down navigation from arrow buttons to back button and page selector - Fix positioning issues with Stack and Positioned widgets - Remove ValueNotifier in favor of setState for rotation state management - Clean up widget hierarchy and remove redundant wrapper classes - Add proper focus order using FocusTraversalOrder - Fix duplicate Positioned widgets causing layout issues - Improve code organization and readability * remove unused `ArrowButtonsFocusTraversalPolicy` in the quran_reading_widgets.dart * fix: resolve Positioned widget conflicts and improve focus navigation - Remove nested Positioned widgets causing render conflicts - Fix focus navigation system in reading screen: * Add proper FocusTraversalOrder for all interactive elements * Implement custom ArrowButtonsFocusTraversalPolicy * Add keyboard navigation support (arrows, tab, enter/space) - Reorganize widget tree structure to prevent parent data conflicts - Improve navigation button layout and accessibility - Fix RTL/LTR direction handling in navigation buttons * remove the unnecessary `FocusTraversalGroup` and order * refactor: remove `QuranFocusTraversalPolicy` class from `quran_floating_action_buttons.dart` * refactor: implement strategy pattern for Quran reading view and focus management Introduced the `QuranViewStrategy` abstract class and created two concrete strategies, `AutoScrollViewStrategy` and `NormalViewStrategy`, to handle view and control layout for different Quran reading modes. Replaced previous inline focus management with a new `FocusNodes` helper class for organizing focus nodes. Refactored loading and error indicators into separate widget methods for cleaner code structure. This update enhances readability and allows for easier expansion of view strategies in the future. * refactor: add font size and speed controls for Quran auto-scrolling mode - Updated `autoScrollSpeed` default value in `AutoScrollState` to 0.1 for a slower starting speed. - Added `cycleFontSize` and `cycleSpeed` methods in `AutoScrollNotifier` to allow cycling through font sizes and scroll speeds with a single button, improving user control and simplifying UI. - Refactored `_FontSizeControls` and `_SpeedControls` widgets to use a single `_ActionButton` for adjusting font size and speed, displaying current values in tooltips. - Re-introduced `_ActionButton` class with autofocus support for enhanced focus management. * refactor: Quran reading widgets for improved modularity and maintainability - Converted functions in `quran_reading_widgets.dart` into distinct `ConsumerWidget` classes: - `VerticalPageViewWidget`, `HorizontalPageViewWidget` - `RightSwitchButtonWidget`, `LeftSwitchButtonWidget` - `PageNumberIndicatorWidget`, `MoshafSelectorPositionedWidget` - `BackButtonWidget`, `SvgPictureWidget` * feat: add scaling the size of the pages with the font * feat: add stop and pause and add close the mode * fix: maintain scroll position and speed when changing auto-scroll settings - Prevent scroll position reset when changing scroll speed - Only restart timer instead of full scroll reinitialize when adjusting speed * remove _handleFloatingActionButtons in the quran floating action * feat(quran-reader): Add auto-scroll pause/resume on tap - Add tap gesture detection to auto-scrolling view - Implement play/pause toggle functionality on tap - Disable manual scrolling in auto-scroll mode - Clean up code formatting and indentation * reformat * feat(quran): integrate surah name display in SurahSelectorWidget - Replace icon with current surah name display in the top bar - Add transparent background with white text for better visibility - Maintain existing dialog functionality for surah selection * feat(ui): show quran reading controls in both portrait & landscape modes - Remove orientation-specific conditional rendering - Display navigation controls, surah selector and page indicators in all orientations - Maintain consistent control behavior across screen modes * fix: portrait mode focus traversal for Quran reading screen - Removed unused `FocusScopeNode` in `QuranFloatingActionControls`. - Introduced a new focus traversal policy (`PortraitModeFocusTraversalPolicy`) for better keyboard navigation in portrait mode. - Updated `_buildBody` to handle focus nodes in both portrait and landscape orientation * refactor: `quran_floating_action_buttons.dart` for dynamic button sizing and improved readability - Updated button and icon sizes to scale dynamically based on screen width, enhancing UI consistency across different devices. * refactor * refactor(quran-reading): update back button behavior and add exit button focus handling - Removed the `BackButtonWidget` from the `quran_reading_screen.dart` page to simplify UI elements. - Enhanced the `_ExitButton` widget in `quran_floating_action_buttons.dart`: - Changed from `ConsumerWidget` to `ConsumerStatefulWidget` for state management. - Added a `FocusNode` for the exit button to set autofocus on load. - Implemented an `initState` method to request focus after widget binding. * feat: add name for the exitFocusNode * reformat * keep highlight one same salah item until iqama (#1394) * stable rtsp & youtube live url links * Fix/ Error in console for 403 images for loading the reciters (#1382) * switch to extended image package to handle exception throw * switch extended image version * Update pubspec.yaml * switch to fast cached library as a temp workaround --------- Co-authored-by: Ibrahim ZEHHAF <97339607+ibrahim-zehhaf-mawaqit@users.noreply.github.com> * add missing translation strings and fix focus issue * use correct constants * fix translation string and add french * add arabic translation * add internet check to setup feature * edit wrong translation * refactor & applied all review suggestions * Feat/close quran when salah (#1408) * feat(routes): add Quran-specific routes and route generator * refactor(routes): migrate to named routes and simplify navigation logic in the quran * fix: Improve Quran mode selection navigation - Modify route generator to handle QuranModeSelection separately * fix: waiting for the handle push * fix the formating * fix: Pop the screen while it has dialog in reading * refactor: AdhanSubScreen to use ConsumerStatefulWidget and manage Quran mode - Updated AdhanSubScreen to use `ConsumerStatefulWidget` and `ConsumerState` for improved state management with Riverpod. - Moved Quran mode exit logic to AdhanSubScreen and JummuaLive components, removing redundant code from salah_workflow. - Added post-frame callback in AdhanSubScreen and JummuaLive to trigger `exitQuranMode` via `quranNotifierProvider`. * pause quran player when adhan begins --------- Co-authored-by: Ghassen Ben Zahra * format code * add missing import * fix stuck at loading * fix merging import * refactor: use constants for RTSP camera preference keys * fix spelling mistake of `clearSnackBarFlag` * refactor: Enhance resource cleanup with proper dispose methods in RTSPCameraStreamNotifier * refactor: RTSP Camera Stream Management and Add Error Handling * refactor: RTSP Camera Stream Notifier for improved validation and error handling - Added `_initializeFromSavedUrl` to handle saved URL initialization with better URL validation logic. - Enhanced `toggleEnabled` method to pause/resume streams based on RTSP state changes. - Refactored `updateStream` to validate URL formats and handle errors gracefully. - Introduced new exception classes (`URLNotProvidedRTSPURLException`, `YouTubeVideoIdExtractionException`) for specific error scenarios. - Improved user feedback via snackbars on URL validation changes. - Extended `RTSPCameraSettingsState` with Equatable for better comparison and added `isInvalidUrl` flag. - Refined `JummuaLive` and RTSP settings screen logic to improve error handling and user experience. - Added `_buildErrorScreen` for consistent error display and retry functionality. - Consolidated and clarified RTSP stream state management for YouTube and RTSP sources. * fix: jumma live not switching in the Youtube * fix: jumma switching * fix the ci formating * refactor: automatically handle both RTSP and YouTube URLs, and the stream from the RTSP settings will override the mosque manager's stream when valid. * fix: manage correctly the dispose of the controllers --------- Co-authored-by: Yassin Nouh <70436855+YassinNouh21@users.noreply.github.com> Co-authored-by: Ibrahim ZEHHAF <97339607+ibrahim-zehhaf-mawaqit@users.noreply.github.com> Co-authored-by: Yassin --- lib/l10n/intl_ar.arb | 20 +- lib/l10n/intl_en.arb | 19 +- lib/l10n/intl_fr.arb | 91 +++-- lib/main.dart | 2 + lib/src/const/constants.dart | 11 + lib/src/domain/error/rtsp_expceptions.dart | 40 +++ lib/src/pages/SettingScreen.dart | 53 +++ .../pages/home/sub_screens/JummuaLive.dart | 116 +++++- .../pages/rtsp_camera_settings_screen.dart | 335 ++++++++++++++++++ .../rtsp_camera_stream_notifier.dart | 282 +++++++++++++++ .../rtsp_camera_stream_state.dart | 63 ++++ pubspec.yaml | 4 +- 12 files changed, 977 insertions(+), 59 deletions(-) create mode 100644 lib/src/domain/error/rtsp_expceptions.dart create mode 100644 lib/src/pages/rtsp_camera_settings_screen.dart create mode 100644 lib/src/state_management/rtsp_camera_stream/rtsp_camera_stream_notifier.dart create mode 100644 lib/src/state_management/rtsp_camera_stream/rtsp_camera_stream_state.dart diff --git a/lib/l10n/intl_ar.arb b/lib/l10n/intl_ar.arb index 5c8460326..671c1f6ac 100644 --- a/lib/l10n/intl_ar.arb +++ b/lib/l10n/intl_ar.arb @@ -172,7 +172,7 @@ "@duaaElEftarText": { "description": "اللهم اني لگ صمت وعلى رزقك افطرت واليك انبت وعليگ توكلت ذهب الظما وابتلت العروق وثبت الاجر انشاء الله" }, - "secondaryScreenExplanation": "غرفة الصلاة الثانوية )غرفة النساء أو طابق آخر على سبيل المثال(، ستظهر هذه الشاشة البث المباشر للجمعة إذا تم تفعيله على حساب MAWAQIT", + "secondaryScreenExplanation": "غرفة الصلاة الثانوية (غرفة النساء أو طابق آخر على سبيل المثال)، ستظهر هذه الشاشة البث المباشر للجمعة إذا تم تفعيله على حساب MAWAQIT", "mainScreenExplanation": "غرفة المسجد الرئيسية، هذه الشاشة لن تظهر البث المباشر للجمعة", "normalModeExplanation": "ستظهر الشاشة العادية مع أوقات الصلاة والإعلانات.", "announcementOnlyModeExplanation": "ستظهر الإعلانات طوال الوقت", @@ -364,5 +364,21 @@ "downloadingUpdate": "جارٍ تنزيل التحديث...", "installingUpdate": "جارٍ تثبيت التحديث...", "updateCompletedSuccessfully": "تم التحديث بنجاح", - "updateFailed": "فشل التحديث" + "updateFailed": "فشل التحديث", + "save": "حفظ", + "enterRtspUrl": "أدخل رابط RTSP أو YouTube Live", + "addRtspUrl": "أضف رابط بث كاميرا RTSP الخاص بك أدناه", + "enableRtspCamera": "تفعيل بث الكاميرا", + "rtspCameraSettings": "إعدادات الكاميرا", + "invalidRtspUrl": "رابط RTSP غير صالح. يرجى التحقق من الرابط والمحاولة مرة أخرى.", + "validRtspUrl": "تم التحقق من رابط RTSP وحفظه بنجاح.", + "rtspCameraSettingTitle": "اتصال الكاميرا المباشر", + "rtspCameraSettingDesc": "اتصل بالكاميرا المحلية واعرض بث صلاة الجمعة على شاشة التلفاز.", + "rtspCameraSettingScreenDesc": "إذا أدخلت رابطًا هنا، ستتحول شاشتك تلقائيًا إلى وضع بث الفيديو عند وصول وقت الجمعة.", + "validatingStream": "جارٍ التحقق من البث...", + "checkInternetLiveCamera": "يجب عليك الاتصال بالإنترنت لإعداد الكاميرا ", + "somethingWentWrong": "حدث خطأ ما! يرجى المحاولة مرة أخرى", + "somethingWrong": "حدث خطأ ما", + "tryAgainLater": "يرجى المحاولة لاحقًا" + } diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 5f3d806c5..8df2039b0 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -364,6 +364,21 @@ "downloadingUpdate": "Downloading update...", "installingUpdate": "Installing update...", "updateCompletedSuccessfully": "Update completed successfully", - "updateFailed": "Update failed" - + "updateFailed": "Update failed", + "save":"Save", + "enterRtspUrl":"Enter RTSP or Youtube Live URL", + "addRtspUrl":"Add your camera stream URL below", + "enableRtspCamera":"Enable Camera Streaming", + "rtspCameraSettings":"Camera Settings", + "invalidRtspUrl":"Invalid URL. Please check the URL and try again.", + "validRtspUrl":"URL validated and saved successfully.", + "rtspCameraSettingTitle":"Live camera connection", + "rtspCameraSettingDesc":"Connect to your local camera and display jumua prayer stream on the TV screen.", + "rtspCameraSettingScreenDesc":"If you enter a URL here, your screen will automatically switch to video streaming when Jumua time arrives", + "validatingStream":"Validating Stream...", + "checkInternetLiveCamera": "You must connect to internet to setup the live camera", + "somethingWentWrong": "Something went wrong! please try again", + "somethingWrong": "Something went wrong", + "tryAgainLater": "Please try again later", + "hintTextRtspUrl": "rtsp://... or https://youtube.com/live/..." } diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index d2040c0b0..6916ef10d 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -24,17 +24,17 @@ "sec": "Sec", "online": "Connecté", "missingMosqueId": "Numéro d'identification MAWAQIT #ID ou MOSQUE #ID manquant", - "mosqueIdIsNotValid": "Désolé, le {mosqueId} n'est pas un id de mosquée valide", + "mosqueIdIsNotValid": "{mosqueId} n'est pas un id de mosquée valide", "selectMosqueId": "Veuillez saisir l'ID de votre MAWAQIT", "mawaqitWelcome": "Bienvenue sur MAWAQIT", - "mawaqitDesc": "Assalamu Alaikom, et Baraka'Allah fikom pour avoir choisi MAWAQIT, le premier et #1 réseau de mosquées intelligentes au monde, utilisé par des millions de musulmans dans le monde entier à travers une centaine de pays depuis 2016.\n\nNous vous fournissons l'affichage de mosquée intelligent le plus avancé, disponible sur plusieurs appareils (Mobile, Tablettes, Smartwatch, écrans de télévision), sans collecter ou partager vos données personnelles.\n\nNous sommes une organisation à but non lucratif, et ce projet est un \"Waqf fi'sabili Allah\" (dotation dédiée).\nVeuillez soutenir ce projet béni ici : https://donate.mawaqit.net\n\nVos dons permettent à ce projet d'être accessible à tous, partout, totalement GRATUITEMENT, SANS PUBLICITÉ, et SANS ABONNEMENT MENSUEL.\n\nCe projet ne serait pas possible sans l'aide d'Allah qui a rassemblé une communauté passionnée de bénévoles talentueux et passionnés, qui travaillent jour et nuit pour vous fournir le meilleur service possible, et un système à la pointe de la technologie disponible 7/24.\n\nVeuillez envisager de faire un don pour que ce projet béni puisse continuer. Baraka'Allah fikom pour votre confiance et votre soutien continus.", + "mawaqitDesc": "Assalamu Alaikom, et Baraka'Allah fikom pour avoir choisi MAWAQIT, le premier et #1 réseau de mosquées intelligentes au monde, utilisé par des millions de musulmans dans le monde entier à travers 85+ pays depuis 2016.\n\nNous vous fournissons l'affichage de mosquée intelligente le plus avancé, disponible sur plusieurs appareils (Mobile, Smartwatch, écrans de télévision), sans collecter ou partager vos données personnelles.\n\nVeuillez soutenir ce projet béni ici : https://donate.mawaqit.net\n\nNous sommes une organisation à but non lucratif, et ce projet est un \"Waqf fi'sabili Allah\" (dotation dédiée).\n\nVos dons permettent à ce projet d'être accessible à tous, partout, totalement GRATUITEMENT, SANS PUBLICITÉ, et SANS ABONNEMENT MENSUEL.\n\nCe projet ne serait pas possible sans l'aide d'Allah qui a rassemblé une communauté passionnée de bénévoles talentueux et passionnés, qui travaillent jour et nuit pour vous fournir le meilleur service possible, et un système à la pointe de la technologie disponible 24 heures sur 24, 7 jours sur 7.\n\nVeuillez envisager de faire un don pour que ce projet béni puisse continuer. Baraka'Allah fikom pour votre confiance et votre soutien continus.", "privacyPolicy": "Politique de confidentialité", "termsOfService": "Conditions générales d’utilisation", "installationGuide": "Guide d'installation", "drawerTitle": "MAWAQIT", "drawerDesc": "Connecting muslims to Mosques", "backendError": "Désolé, nous n'avons pas pu nous connecter au serveur.\nVeuillez vérifier votre connexion Internet ou réessayer plus tard.", - "selectWithMosqueId": "Votre ID se trouve dans votre espace utilisateur mawaqit.net", + "selectWithMosqueId": "Essayez: 256, c'est l'ID de 'Grande Mosquée de Paris'", "searchForMosque": "Quelle mosquée recherchez-vous ? (ID, nom, ville, code postal...)", "searchMosque": "Chercher une mosquée", "mosqueNameError": "Entrer le nom de la mosquée", @@ -88,36 +88,36 @@ "@azkarList6": { "description": "لا إِلَٰهَ إلاّ اللّهُ وحدَهُ لا شريكَ لهُ، لهُ المُـلْكُ ولهُ الحَمْد، وهوَ على كلّ شَيءٍ قَدير، اللّهُـمَّ لا مانِعَ لِما أَعْطَـيْت، وَلا مُعْطِـيَ لِما مَنَـعْت، وَلا يَنْفَـعُ ذا الجَـدِّ مِنْـكَ الجَـد" }, - "azkarList7": "اللهم أنت ربي، لا إله إلا أنت، خلقتني وأنا عبدُك, وأنا على عهدِك ووعدِك ما استطعتُ، أعوذ بك من شر ما صنعتُ، أبوءُ لَكَ بنعمتكَ عَلَيَّ، وأبوء بذنبي، فاغفر لي، فإنه لا يغفرُ الذنوب إلا أنت", + "azkarList7": "اللهم أنت ربي، لا إله إلا أنت، خلقتني وأنا عبدُك, وأنا على عهدِك ووعدِك ما استطعتُ، أعوذ بك من شر ما صنعتُ، أبوءُ لَكَ بنعمتكَ عَلَيَّ، وأبوء بذنبي، فاغفر لي، فإنه لا يغفرُ الذنوب إلا أنت", "@azkarList7": { "description": "اللهم أنت ربي، لا إله إلا أنت، خلقتني وأنا عبدُك, وأنا على عهدِك ووعدِك ما استطعتُ، أعوذ بك من شر ما صنعتُ، أبوءُ لَكَ بنعمتكَ عَلَيَّ، وأبوء بذنبي، فاغفر لي، فإنه لا يغفرُ الذنوب إلا أنت" }, - "azkarList8": "أصبحنا وأصبح الملك لله، والحمد لله ولا إله إلا الله وحده لا شريك له، له الملك وله الحمد، وهو على كل شيء قدير، أسألك خير ما في هذا اليوم، وخير ما بعده، وأعوذ بك من شر هذا اليوم، وشر ما بعده، وأعوذ بك من الكسل وسوء الكبر، وأعوذ بك من عذاب النار وعذاب القبر", + "azkarList8": "أصبحنا وأصبح الملك لله، والحمد لله ولا إله إلا الله وحده لا شريك له، له الملك وله الحمد، وهو على كل شيء قدير، أسألك خير ما في هذا اليوم، وخير ما بعده، وأعوذ بك من شر هذا اليوم، وشر ما بعده، وأعوذ بك من الكسل وسوء الكبر، وأعوذ بك من عذاب النار وعذاب القبر", "@azkarList8": { "description": "أصبحنا وأصبح الملك لله، والحمد لله ولا إله إلا الله وحده لا شريك له، له الملك وله الحمد، وهو على كل شيء قدير، أسألك خير ما في هذا اليوم، وخير ما بعده، وأعوذ بك من شر هذا اليوم، وشر ما بعده، وأعوذ بك من الكسل وسوء الكبر، وأعوذ بك من عذاب النار وعذاب القبر" }, - "azkarList9": "اللَّهُمَّ إِنِّي أَصْبَحْتُ أُشْهِدُكَ، وَأُشْهِدُ حَمَلَةَ عَرْشِكَ، وَمَلاَئِكَتِكَ، وَجَمِيعَ خَلْقِكَ، أَنَّكَ أَنْتَ اللَّهُ لَا إِلَهَ إِلاَّ أَنْتَ وَحْدَكَ لاَ شَرِيكَ لَكَ، وَأَنَّ مُحَمَّداً عَبْدُكَ وَرَسُولُكَ |أربعَ مَرَّات|. [ وإذا أمسى قال: اللَّهم إني أمسيت...]", + "azkarList9": "اللَّهُمَّ إِنِّي أَصْبَحْتُ أُشْهِدُكَ، وَأُشْهِدُ حَمَلَةَ عَرْشِكَ، وَمَلاَئِكَتِكَ، وَجَمِيعَ خَلْقِكَ، أَنَّكَ أَنْتَ اللَّهُ لَا إِلَهَ إِلاَّ أَنْتَ وَحْدَكَ لاَ شَرِيكَ لَكَ، وَأَنَّ مُحَمَّداً عَبْدُكَ وَرَسُولُكَ |أربعَ مَرَّات|. [ وإذا أمسى قال: اللَّهم إني أمسيت...]", "@azkarList9": { "description": "اللَّهُمَّ إِنِّي أَصْبَحْتُ أُشْهِدُكَ، وَأُشْهِدُ حَمَلَةَ عَرْشِكَ، وَمَلاَئِكَتِكَ، وَجَمِيعَ خَلْقِكَ، أَنَّكَ أَنْتَ اللَّهُ لَا إِلَهَ إِلاَّ أَنْتَ وَحْدَكَ لاَ شَرِيكَ لَكَ، وَأَنَّ مُحَمَّداً عَبْدُكَ وَرَسُولُكَ |أربعَ مَرَّات|. [ وإذا أمسى قال: اللَّهم إني أمسيت...]" }, - "azkarList10": "|اللَّهُمَّ عَافِنِي فِي بَدَنِي، اللَّهُمَّ عَافِنِي فِي سَمْعِي، اللَّهُمَّ عَافِنِي فِي بَصَرِي، لاَ إِلَهَ إِلاَّ أَنْتَ. اللَّهُمَّ إِنِّي أَعُوذُ بِكَ مِنَ الْكُفْرِ، وَالفَقْرِ، وَأَعُوذُ بِكَ مِنْ عَذَابِ القَبْرِ، لاَ إِلَهَ إِلاَّ أَنْتَ |ثلاثَ مرَّاتٍ", + "azkarList10": "|اللَّهُمَّ عَافِنِي فِي بَدَنِي، اللَّهُمَّ عَافِنِي فِي سَمْعِي، اللَّهُمَّ عَافِنِي فِي بَصَرِي، لاَ إِلَهَ إِلاَّ أَنْتَ. اللَّهُمَّ إِنِّي أَعُوذُ بِكَ مِنَ الْكُفْرِ، وَالفَقْرِ، وَأَعُوذُ بِكَ مِنْ عَذَابِ القَبْرِ، لاَ إِلَهَ إِلاَّ أَنْتَ |ثلاثَ مرَّاتٍ", "@azkarList10": { "description": "|اللَّهُمَّ عَافِنِي فِي بَدَنِي، اللَّهُمَّ عَافِنِي فِي سَمْعِي، اللَّهُمَّ عَافِنِي فِي بَصَرِي، لاَ إِلَهَ إِلاَّ أَنْتَ. اللَّهُمَّ إِنِّي أَعُوذُ بِكَ مِنَ الْكُفْرِ، وَالفَقْرِ، وَأَعُوذُ بِكَ مِنْ عَذَابِ القَبْرِ، لاَ إِلَهَ إِلاَّ أَنْتَ |ثلاثَ مرَّاتٍ" }, - "azkarList11": "|حَسْبِيَ اللَّهُ لاَ إِلَهَ إِلاَّ هُوَ عَلَيهِ تَوَكَّلتُ وَهُوَ رَبُّ الْعَرْشِ الْعَظِيمِ |سَبْعَ مَرّاتٍ", + "azkarList11": "|حَسْبِيَ اللَّهُ لاَ إِلَهَ إِلاَّ هُوَ عَلَيهِ تَوَكَّلتُ وَهُوَ رَبُّ الْعَرْشِ الْعَظِيمِ |سَبْعَ مَرّاتٍ", "@azkarList11": { "description": "|حَسْبِيَ اللَّهُ لاَ إِلَهَ إِلاَّ هُوَ عَلَيهِ تَوَكَّلتُ وَهُوَ رَبُّ الْعَرْشِ الْعَظِيمِ |سَبْعَ مَرّاتٍ" }, - "azkarList12": "|رَضِيتُ بِاللَّهِ رَبَّاً، وَبِالْإِسْلاَمِ دِيناً، وَبِمُحَمَّدٍ صلى الله عليه وسلم نَبِيّاً |ثلاثَ مرَّاتٍ", + "azkarList12": "|رَضِيتُ بِاللَّهِ رَبَّاً، وَبِالْإِسْلاَمِ دِيناً، وَبِمُحَمَّدٍ صلى الله عليه وسلم نَبِيّاً |ثلاثَ مرَّاتٍ", "@azkarList12": { "description": "|رَضِيتُ بِاللَّهِ رَبَّاً، وَبِالْإِسْلاَمِ دِيناً، وَبِمُحَمَّدٍ صلى الله عليه وسلم نَبِيّاً |ثلاثَ مرَّاتٍ" }, - "azkarList13": "|لاَ إِلَهَ إِلاَّ اللَّهُ وَحْدَهُ لاَ شَرِيكَ لَهُ، لَهُ الْمُلْكُ وَلَهُ الْحَمْدُ، وَهُوَ عَلَى كُلِّ شَيْءٍ قَدِيرٌ |عشرَ مرَّاتٍ", + "azkarList13": "|لاَ إِلَهَ إِلاَّ اللَّهُ وَحْدَهُ لاَ شَرِيكَ لَهُ، لَهُ الْمُلْكُ وَلَهُ الْحَمْدُ، وَهُوَ عَلَى كُلِّ شَيْءٍ قَدِيرٌ |عشرَ مرَّاتٍ", "@azkarList13": { "description": "|لاَ إِلَهَ إِلاَّ اللَّهُ وَحْدَهُ لاَ شَرِيكَ لَهُ، لَهُ الْمُلْكُ وَلَهُ الْحَمْدُ، وَهُوَ عَلَى كُلِّ شَيْءٍ قَدِيرٌ |عشرَ مرَّات" }, "jumuaaScreenTitle": "L'heure du Joumoua", - "jumuaaHadith": "Le Prophète Alayhi essalam a dit : \"Quiconque fait les ablutions parfaitement puis va à la joumoua puis écoute et se tait, il lui est pardonné ce qui se trouve entre ce moment et le vendredi suivant et trois autres jours, et celui qui touche des pierres a certainement fait une futilité\".", + "jumuaaHadith": "Le Prophète ﷺ a dit : \"Quiconque fait les ablutions parfaitement puis va à la jumua puis écoute et se tait, il lui est pardonné ce qui se trouve entre ce moment et le vendredi suivant et trois autres jours, et celui qui touche des pierres a certainement fait une futilité\".", "shuruk": "Chourouk", "reset": "Reset", "mosqueNotFoundMessage": "Désolé, votre mosquée n'a pas été trouvée, elle est peut-être manquante ou temporairement désactivée.", @@ -127,16 +127,16 @@ "muharram": "Mouharram", "safar": "Safar", "rabiAlawwal": "Rabi' al-Awwal", - "rabiAlthani": "Rabi' al-thani", - "jumadaAlula": "Joumada al-oula", + "rabiAlthani": "Rabi' al-akhir", + "jumadaAlula": "Jumada al-Ula", "jumadaAlakhirah": "Joumada al-akhirah", "rajab": "Rajab", "shaban": "Chaabane", "ramadan": "Ramadan", - "shawwal": "Chaoual", - "dhuAlqidah": "Dhou al-Qi'dah", - "dhuAlhijjah": "Dhou al-Hijja", - "duaaBetweenSalahAndAdhan": "Selon Anas Ibn Mâlik, le Prophète (ﷺ) a dit : \"Les invocations entre l'Adhân et l’Iqâma ne sont pas rejetées\"", + "shawwal": "Shawwal", + "dhuAlqidah": "Dhu al-Qi'dah", + "dhuAlhijjah": "Dhu al-Hijja", + "duaaBetweenSalahAndAdhan": " Selon Anas Ibn Mâlik, le Prophète (ﷺ) a dit : \"Les invocations entre l'Adhân et l’Iqâmah ne sont pas rejetées\"", "salatKhayrMinaNawm": "Assalatou khayroun mina nawm", "salatElEid": "Salat Al Aïd", "webView": "Forcer l'ancienne version (Online)", @@ -290,27 +290,13 @@ } } }, - "quranReadingPagePortrait": "Page {currentPage} / {totalPages}", - "@quranReadingPagePortrait": { - "description": "Placeholder text for displaying Quran reading page portrait numbers", - "placeholders": { - "currentPage": { - "type": "int", - "example": "1" - }, - "totalPages": { - "type": "int", - "example": "604" - } - } - }, "chooseQuranPage": "Choisir une page", "checkingForUpdates": "Vérification des mises à jour...", "chooseQuranType": "Choisir quran", "hafs": "Hafs", - "warsh": "Guerrier", - "favorites": "Favoris", + "warsh": "Warsh", "allReciters": "Tous les Réciteurs", + "noFavoriteReciters": "Pas de réciteur favori. Essayez d'en ajouter un à la liste", "reciterAddedToFavorites": "Le réciteur {name} a été ajouté aux favoris", "@reciterAddedToFavorites": { "description": "Message shown when a reciter is added to favorites", @@ -335,11 +321,11 @@ "@noFavoriteReciters": { "description": "Message shown when there are no favorite reciters" }, - "noReciterSearchResult": "Aucun résultat trouvé pour votre recherche.", - "searchForReciter": "Chercher un réciteur", + "noReciterSearchResult": "Aucun résultat trouvé pour votre recherche.", + "searchForReciter": "Chercher un réciteur", "downloadAllSuwarSuccessfully": "Tout le Coran est téléchargé", "noSuwarDownload": "Aucune nouvelle sourate à télécharger", - "connectDownloadQuran": "Veuillez vous connecter à Internet pour télécharger", + "connectDownloadQuran":"Veuillez vous connecter à Internet pour télécharger", "playInOnlineModeQuran": "Veuillez vous connecter à Internet pour jouer", "downloaded": "Téléchargé", "switchQuranType": "Aller à {name}", @@ -352,7 +338,36 @@ } } }, - "surahSelector": "Sélectionner une sourat", + "surahSelector":"Sélectionner une sourat", + "quranReadingPagePortrait": "Page {currentPage} / {totalPages}", + "@quranReadingPagePortrait": { + "description": "Placeholder text for displaying Quran reading page portrait numbers", + "placeholders": { + "currentPage": { + "type": "int", + "example": "1" + }, + "totalPages": { + "type": "int", + "example": "604" + } + } + }, + "save": "Enregistrer", + "enterRtspUrl": "Entrez l'URL RTSP ou YouTube Live", + "addRtspUrl": "Ajoutez l'URL de votre flux de caméra ci-dessous", + "enableRtspCamera": "Activer le flux de la caméra", + "rtspCameraSettings": "Paramètres de la caméra", + "invalidRtspUrl": "URL invalide. Veuillez vérifier l'URL et réessayer.", + "validRtspUrl": "URL validée et enregistrée avec succès.", + "rtspCameraSettingTitle": "Connexion de la caméra en direct", + "rtspCameraSettingDesc": "Connectez-vous à votre caméra locale et affichez le flux de la prière de la jumua sur l'écran de la TV.", + "rtspCameraSettingScreenDesc": "Si vous entrez une URL ici, votre écran passera automatiquement en mode streaming vidéo lorsque l'heure de la Jumua arrive.", + "validatingStream": "Validation du flux...", + "checkInternetLiveCamera": "Vous devez vous connecter à Internet pour configurer la caméra en direct", + "somethingWentWrong": "Quelque chose s'est mal passé ! Veuillez réessayer", + "somethingWrong": "Quelque chose s'est mal passé", + "tryAgainLater": "Veuillez réessayer plus tard", "checkForUpdates": "Vérifier les mises à jour", "checkForNewVersion": "Vérifiez si une nouvelle version est disponible", "wouldYouLikeToUpdate": "Souhaitez-vous mettre à jour l'application ?", @@ -365,4 +380,4 @@ "installingUpdate": "Installation de la mise à jour...", "updateCompletedSuccessfully": "Mise à jour terminée avec succès", "updateFailed": "Échec de la mise à jour" -} \ No newline at end of file +} diff --git a/lib/main.dart b/lib/main.dart index 0aee7752a..cab356a42 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -29,6 +29,7 @@ import 'package:mawaqit/src/services/FeatureManager.dart'; import 'package:mawaqit/src/services/mosque_manager.dart'; import 'package:mawaqit/src/services/theme_manager.dart'; import 'package:mawaqit/src/services/user_preferences_manager.dart'; +import 'package:media_kit/media_kit.dart'; import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; import 'package:sizer/sizer.dart'; @@ -50,6 +51,7 @@ Future main() async { Hive.registerAdapter(SurahModelAdapter()); Hive.registerAdapter(ReciterModelAdapter()); Hive.registerAdapter(MoshafModelAdapter()); + MediaKit.ensureInitialized(); runApp( riverpod.ProviderScope( child: MyApp(), diff --git a/lib/src/const/constants.dart b/lib/src/const/constants.dart index aa529c988..9ddb1f0c5 100644 --- a/lib/src/const/constants.dart +++ b/lib/src/const/constants.dart @@ -111,3 +111,14 @@ abstract class ManualUpdateConstant { static const String githubApiBaseUrl = 'https://api.github.com/repos/mawaqit/android-tv-app/releases'; static const String githubAcceptHeader = 'application/vnd.github.v3+json'; } + +abstract class RtspCameraStreamConstant { + static const maxRetries = 3; + static const retryDelay = Duration(seconds: 2); + static const prefKeyEnabled = 'rtsp_enabled'; + static const prefKeyUrl = 'rtsp_url'; + static const String youtubeUrlPattern = + r'http(?:s?):\/\/(?:www\.)?youtu(?:be\.com\/watch\?v=|\.be\/)([\w\-\_]*)(&(amp;)?‌​[\w\?‌​=]*)?'; + + static final RegExp youtubeUrlRegex = RegExp(youtubeUrlPattern); +} diff --git a/lib/src/domain/error/rtsp_expceptions.dart b/lib/src/domain/error/rtsp_expceptions.dart new file mode 100644 index 000000000..0354c6837 --- /dev/null +++ b/lib/src/domain/error/rtsp_expceptions.dart @@ -0,0 +1,40 @@ +abstract class RTSPCameraException implements Exception { + final String message; + final String errorCode; + + RTSPCameraException(this.message, this.errorCode); + + @override + String toString() => 'Error ($errorCode): $message'; +} + +class RTSPInitializationException extends RTSPCameraException { + RTSPInitializationException(String message) + : super('Error during RTSP initialization: $message', 'RTSP_INITIALIZATION_ERROR'); +} + +class RTSPToggleException extends RTSPCameraException { + RTSPToggleException(String message) : super('Error toggling RTSP camera: $message', 'RTSP_TOGGLE_ERROR'); +} + +class InvalidRTSPURLException extends RTSPCameraException { + InvalidRTSPURLException(String message) : super('Invalid RTSP URL: $message', 'INVALID_RTSP_URL_ERROR'); +} + +class URLNotProvidedRTSPURLException extends RTSPCameraException { + URLNotProvidedRTSPURLException(String message) + : super('URL not provided: $message', 'URL_NOT_PROVIDED_RTSP_URL_ERROR'); +} + +class YouTubeVideoIdExtractionException extends RTSPCameraException { + YouTubeVideoIdExtractionException(String message) + : super('Error extracting YouTube video ID: $message', 'YOUTUBE_VIDEO_ID_EXTRACTION_ERROR'); +} + +class RTSPStreamUpdateException extends RTSPCameraException { + RTSPStreamUpdateException(String message) : super('Error updating RTSP stream: $message', 'RTSP_STREAM_UPDATE_ERROR'); +} + +class RTSPUnknownException extends RTSPCameraException { + RTSPUnknownException(String message) : super('Unknown RTSP error: $message', 'RTSP_UNKNOWN_ERROR'); +} diff --git a/lib/src/pages/SettingScreen.dart b/lib/src/pages/SettingScreen.dart index b414cfd49..1237b878d 100644 --- a/lib/src/pages/SettingScreen.dart +++ b/lib/src/pages/SettingScreen.dart @@ -41,6 +41,7 @@ import '../state_management/random_hadith/random_hadith_notifier.dart'; import '../widgets/screen_lock_widget.dart'; import '../widgets/time_picker_widget.dart'; import 'home/widgets/show_check_internet_dialog.dart'; +import 'rtsp_camera_settings_screen.dart'; class SettingScreen extends ConsumerStatefulWidget { const SettingScreen({super.key}); @@ -175,6 +176,58 @@ class _SettingScreenState extends ConsumerState { ); }, ), + Consumer( + builder: (context, ref, child) { + return _SettingSwitchItem( + title: S.of(context).automaticUpdate, + subtitle: S.of(context).automaticUpdateDescription, + icon: Icon(Icons.update, size: 35), + onChanged: (value) { + logger.d('setting: disable the update $value'); + ref.read(appUpdateProvider.notifier).toggleAutoUpdateChecking(); + }, + value: ref.watch(appUpdateProvider).maybeWhen( + orElse: () => false, + data: (data) => data.isAutoUpdateChecking, + ), + ); + }, + ), + _SettingItem( + title: S.of(context).rtspCameraSettingTitle, + subtitle: S.of(context).rtspCameraSettingDesc, + icon: Icon(Icons.video_camera_back, size: 35), + onTap: () async { + await ref.read(connectivityProvider.notifier).checkInternetConnection(); + ref.watch(connectivityProvider).maybeWhen( + orElse: () { + showCheckInternetDialog( + context: context, + onRetry: () { + AppRouter.pop(); + }, + title: checkInternet, + content: S.of(context).checkInternetLiveCamera, + ); + }, + data: (isConnectedToInternet) { + if (isConnectedToInternet == ConnectivityStatus.disconnected) { + showCheckInternetDialog( + context: context, + onRetry: () { + AppRouter.pop(); + }, + title: checkInternet, + content: S.of(context).checkInternetLiveCamera, + ); + } else { + AppRouter.push(RTSPCameraSettingsScreen()); + } + }, + ); + }, + ), + SizedBox(height: 30), Divider(), SizedBox(height: 10), Text( diff --git a/lib/src/pages/home/sub_screens/JummuaLive.dart b/lib/src/pages/home/sub_screens/JummuaLive.dart index e4e2cc7f3..6a073a227 100644 --- a/lib/src/pages/home/sub_screens/JummuaLive.dart +++ b/lib/src/pages/home/sub_screens/JummuaLive.dart @@ -6,9 +6,15 @@ import 'package:mawaqit/i18n/l10n.dart'; import 'package:mawaqit/src/helpers/RelativeSizes.dart'; import 'package:mawaqit/src/models/address_model.dart'; import 'package:mawaqit/src/services/mosque_manager.dart'; +import 'package:mawaqit/src/state_management/rtsp_camera_stream/rtsp_camera_stream_notifier.dart'; +import 'package:mawaqit/src/state_management/rtsp_camera_stream/rtsp_camera_stream_state.dart'; import 'package:mawaqit/src/state_management/quran/quran/quran_notifier.dart'; import 'package:mawaqit/src/themes/UIShadows.dart'; +import 'package:media_kit/media_kit.dart'; +import 'package:media_kit_video/media_kit_video.dart'; import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:youtube_player_flutter/youtube_player_flutter.dart'; import '../../../../main.dart'; import '../../../helpers/connectivity_provider.dart'; @@ -29,7 +35,6 @@ class JummuaLive extends ConsumerStatefulWidget { } class _JummuaLiveState extends ConsumerState { - /// invalid channel id bool invalidStreamUrl = false; @override @@ -41,6 +46,7 @@ class _JummuaLiveState extends ConsumerState { }); log('JummuaLive: invalidStreamUrl: $invalidStreamUrl'); + super.initState(); } @@ -49,29 +55,104 @@ class _JummuaLiveState extends ConsumerState { final mosqueManager = context.read(); final userPrefs = context.watch(); final connectivity = ref.watch(connectivityProvider); + final streamStateAsync = ref.watch(rtspCameraSettingsProvider); - /// disable live stream in mosque primary screen final jumuaaDisableInMosque = !userPrefs.isSecondaryScreen && mosqueManager.typeIsMosque; - return switch (connectivity) { - AsyncData(:final value) => switchStreamWidget(value, mosqueManager, jumuaaDisableInMosque), - _ => CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(Theme.of(context).primaryColor), // Green color + return connectivity.when( + data: (value) => streamStateAsync.when( + data: (streamState) { + return _switchStreamWidget( + value, + mosqueManager, + jumuaaDisableInMosque, + streamState, + ); + }, + loading: () => const Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.blue), + ), + ), + error: (error, stack) => _switchStreamWidget( + value, + mosqueManager, + jumuaaDisableInMosque, + RTSPCameraSettingsState(), ), - }; + ), + loading: () => const CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.blue), + ), + error: (_, __) => _switchStreamWidget( + ConnectivityStatus.disconnected, + mosqueManager, + jumuaaDisableInMosque, + RTSPCameraSettingsState(), + ), + ); } - Widget switchStreamWidget( - ConnectivityStatus connectivityStatus, MosqueManager mosqueManager, bool jumuaaDisableInMosque) { - if (invalidStreamUrl || - mosqueManager.mosque?.streamUrl == null || - jumuaaDisableInMosque || - connectivityStatus == ConnectivityStatus.disconnected) { - if (mosqueManager.mosqueConfig!.jumuaDhikrReminderEnabled == true) + Widget _switchStreamWidget( + ConnectivityStatus connectivityStatus, + MosqueManager mosqueManager, + bool jumuaaDisableInMosque, + RTSPCameraSettingsState streamState, + ) { + // First check if we should show Hadith screen or black screen + if (jumuaaDisableInMosque || connectivityStatus == ConnectivityStatus.disconnected) { + if (mosqueManager.mosqueConfig!.jumuaDhikrReminderEnabled == true) { return JumuaHadithSubScreen(onDone: widget.onDone); + } + return const Scaffold(backgroundColor: Colors.black); + } + + // Check if RTSP is enabled and properly configured + final isRTSPWorking = streamState.isRTSPEnabled && + streamState.streamType == StreamType.rtsp && + streamState.videoController != null && + streamState.streamUrl != null && + connectivityStatus != ConnectivityStatus.disconnected; + + // Check if YouTube stream is configured + final isYouTubeWorking = streamState.isRTSPEnabled && + streamState.streamType == StreamType.youtubeLive && + streamState.youtubeController != null && + streamState.streamUrl != null && + connectivityStatus != ConnectivityStatus.disconnected; + + // Priority 1: RTSP Stream if working + if (isRTSPWorking) { + return Scaffold( + backgroundColor: Colors.black, + body: Center( + child: AspectRatio( + aspectRatio: 16 / 9, + child: Video( + controller: streamState.videoController!, + ), + ), + ), + ); + } - return Scaffold(backgroundColor: Colors.black); - } else { + // Priority 2: YouTube Stream from RTSP settings if working + if (isYouTubeWorking) { + return Scaffold( + backgroundColor: Colors.black, + body: Center( + child: AspectRatio( + aspectRatio: 16 / 9, + child: YoutubePlayer( + controller: streamState.youtubeController!, + ), + ), + ), + ); + } + + // Priority 3: Mosque Manager's YouTube stream as fallback + if (mosqueManager.mosque?.streamUrl != null) { return MawaqitYoutubePlayer( channelId: mosqueManager.mosque!.streamUrl!, onDone: widget.onDone, @@ -79,5 +160,8 @@ class _JummuaLiveState extends ConsumerState { onNotFound: () => setState(() => invalidStreamUrl = true), ); } + + // Fallback case + return const Scaffold(backgroundColor: Colors.black); } } diff --git a/lib/src/pages/rtsp_camera_settings_screen.dart b/lib/src/pages/rtsp_camera_settings_screen.dart new file mode 100644 index 000000000..637a993c5 --- /dev/null +++ b/lib/src/pages/rtsp_camera_settings_screen.dart @@ -0,0 +1,335 @@ +import 'dart:async'; +import 'dart:developer'; +import 'package:flutter/material.dart'; +import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mawaqit/i18n/l10n.dart'; +import 'package:mawaqit/src/domain/error/rtsp_expceptions.dart'; +import 'package:mawaqit/src/state_management/rtsp_camera_stream/rtsp_camera_stream_notifier.dart'; +import 'package:mawaqit/src/state_management/rtsp_camera_stream/rtsp_camera_stream_state.dart'; +import 'package:mawaqit/src/widgets/ScreenWithAnimation.dart'; +import 'package:media_kit_video/media_kit_video.dart'; +import 'package:sizer/sizer.dart'; +import 'package:youtube_player_flutter/youtube_player_flutter.dart'; + +class RTSPCameraSettingsScreen extends ConsumerStatefulWidget { + const RTSPCameraSettingsScreen({Key? key}) : super(key: key); + + @override + ConsumerState createState() => _RTSPCameraSettingsScreenState(); +} + +class _RTSPCameraSettingsScreenState extends ConsumerState { + final TextEditingController _urlController = TextEditingController(); + final FocusNode _saveButtonFocusNode = FocusNode(); + late StreamSubscription keyboardSubscription; + + @override + void initState() { + super.initState(); + var keyboardVisibilityController = KeyboardVisibilityController(); + keyboardSubscription = keyboardVisibilityController.onChange.listen((bool visible) { + if (!visible) { + FocusScope.of(context).requestFocus(_saveButtonFocusNode); + } + }); + + WidgetsBinding.instance.addPostFrameCallback((_) { + final state = ref.read(rtspCameraSettingsProvider); + state.whenData((value) { + if (value.streamUrl != null) { + _urlController.text = value.streamUrl!; + } + }); + }); + } + + @override + void dispose() { + _urlController.dispose(); + keyboardSubscription.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final asyncState = ref.watch(rtspCameraSettingsProvider); + ref.listen(rtspCameraSettingsProvider, (previous, next) { + if (previous != next && !next.isLoading && next.hasValue && !next.hasError && next.value!.isRTSPEnabled) { + final state = next.value!; + + // Only show snackbar when URL validation status changes + ScaffoldMessenger.of(context).clearSnackBars(); + + String message; + Color backgroundColor; + + if (state.streamUrl != null && !state.isInvalidUrl) { + message = S.of(context).validRtspUrl; + backgroundColor = Colors.green; + } else if (state.isInvalidUrl) { + message = S.of(context).invalidRtspUrl; + backgroundColor = Colors.red; + } else { + return; + } + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + message, + style: const TextStyle(fontSize: 16), + ), + backgroundColor: backgroundColor, + duration: const Duration(seconds: 3), + behavior: SnackBarBehavior.floating, + margin: const EdgeInsets.all(16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ); + } + }); + + return asyncState.when( + data: (state) { + return Scaffold( + appBar: state.isRTSPEnabled + ? AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back), + iconSize: 12.sp, + splashRadius: 7.sp, + onPressed: () => Navigator.of(context).pop(), + ), + ) + : null, + body: SafeArea( + child: Stack( + children: [ + if (!state.isRTSPEnabled) + ScreenWithAnimationWidget( + animation: "settings", + child: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: _buildSettingsContent(state), + ), + ) + else + Row( + children: [ + Expanded( + flex: 1, + child: Container( + margin: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + border: Border.all(color: Theme.of(context).dividerColor), + borderRadius: BorderRadius.circular(20), + ), + child: AspectRatio( + aspectRatio: 16 / 9, + child: _buildVideoPreview(state), + ), + ), + ), + Expanded( + flex: 1, + child: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: _buildSettingsContent(state), + ), + ), + ], + ), + ], + ), + ), + ); + }, + loading: () => Scaffold( + body: _buildLoadingOverlay(), + ), + error: (error, stackTrace) { + if (error is RTSPCameraException) { + return _buildErrorScreen(error); + } else { + return _buildErrorScreen(RTSPStreamUpdateException(error.toString())); + } + }, + ); + } + + Widget _buildLoadingOverlay() { + return Container( + color: Colors.black54, + child: Center( + child: Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 20, + horizontal: 30, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 16), + Text( + S.of(context).validatingStream, + style: Theme.of(context).textTheme.titleMedium, + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildErrorScreen(RTSPCameraException error) { + String errorMessage = ''; + + errorMessage = S.of(context).somethingWentWrong; + + return Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.error_outline, + size: 48, + color: Colors.red, + ), + const SizedBox(height: 16), + Text( + errorMessage, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + ref.invalidate(rtspCameraSettingsProvider); + }, + child: Text(S.of(context).tryAgain), + ), + ], + ), + ), + ); + } + + Widget _buildVideoPreview(RTSPCameraSettingsState state) { + if (state.streamType == StreamType.youtubeLive && state.youtubeController != null) { + return YoutubePlayer(controller: state.youtubeController!); + } + if (state.videoController != null) { + return Video(controller: state.videoController!); + } + return const SizedBox.shrink(); + } + + Widget _buildSettingsContent(RTSPCameraSettingsState state) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + S.of(context).rtspCameraSettings, + style: Theme.of(context).textTheme.titleMedium?.apply(fontSizeFactor: 2), + textAlign: TextAlign.center, + ), + const Divider(indent: 50, endIndent: 50), + const SizedBox(height: 10), + Text( + S.of(context).rtspCameraSettingScreenDesc, + style: Theme.of(context).textTheme.bodySmall?.apply(fontSizeFactor: 1.5), + textAlign: TextAlign.center, + ), + const SizedBox(height: 20), + SwitchListTile( + title: Text(S.of(context).enableRtspCamera), + value: state.isRTSPEnabled, + onChanged: (value) { + ref.read(rtspCameraSettingsProvider.notifier).toggleEnabled(value); + }, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + side: BorderSide(color: Theme.of(context).dividerColor), + ), + ), + if (state.isRTSPEnabled) ...[ + const SizedBox(height: 20), + Text( + S.of(context).addRtspUrl, + style: Theme.of(context).textTheme.bodyLarge?.apply(fontSizeFactor: 1.2), + textAlign: TextAlign.center, + ), + const Divider(indent: 50, endIndent: 50), + const SizedBox(height: 20), + TextField( + controller: _urlController, + onSubmitted: (_) => ref.read(rtspCameraSettingsProvider.notifier).updateStream( + isEnabled: true, + url: _urlController.text, + ), + decoration: InputDecoration( + labelText: S.of(context).enterRtspUrl, + hintText: S.of(context).hintTextRtspUrl, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + ), + const SizedBox(height: 20), + ElevatedButton.icon( + focusNode: _saveButtonFocusNode, + onPressed: () => ref.read(rtspCameraSettingsProvider.notifier).updateStream( + isEnabled: true, + url: _urlController.text, + ), + icon: const Icon(Icons.save), + label: Text(S.of(context).save), + style: ButtonStyle( + padding: MaterialStateProperty.all( + const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + ), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + backgroundColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.focused)) { + return Theme.of(context).primaryColor; + } + return Colors.white; + }), + iconColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.focused)) { + return Colors.white; + } + return Colors.black; + }), + foregroundColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.focused)) { + return Colors.white; + } + return Colors.black; + }), + ), + ) + ], + ], + ); + } +} diff --git a/lib/src/state_management/rtsp_camera_stream/rtsp_camera_stream_notifier.dart b/lib/src/state_management/rtsp_camera_stream/rtsp_camera_stream_notifier.dart new file mode 100644 index 000000000..045113165 --- /dev/null +++ b/lib/src/state_management/rtsp_camera_stream/rtsp_camera_stream_notifier.dart @@ -0,0 +1,282 @@ +import 'dart:developer'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mawaqit/src/const/constants.dart'; +import 'package:mawaqit/src/domain/error/rtsp_expceptions.dart'; +import 'package:mawaqit/src/state_management/rtsp_camera_stream/rtsp_camera_stream_state.dart'; +import 'package:media_kit/media_kit.dart'; +import 'package:media_kit_video/media_kit_video.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:youtube_explode_dart/youtube_explode_dart.dart'; +import 'package:youtube_player_flutter/youtube_player_flutter.dart'; + +enum StreamType { rtsp, youtubeLive } + +class RTSPCameraSettingsNotifier extends AutoDisposeAsyncNotifier { + YoutubePlayerController? _youtubeController; + Player? _player; + VideoController? _videoController; + + Future dispose() async { + try { + if (_youtubeController != null) { + _youtubeController!.dispose(); + _youtubeController = null; + } + + if (_player != null) { + await _player!.pause(); + await _player!.dispose(); + _player = null; + } + + if (_videoController != null) { + await _videoController!.player.dispose(); + _videoController = null; + } + } catch (e) { + log('Error disposing controllers: $e'); + } + } + + @override + Future build() async { + ref.onDispose(() async { + await dispose(); + }); + + return await initializeSettings(); + } + + Future initializeSettings() async { + try { + final prefs = await SharedPreferences.getInstance(); + final isEnabled = prefs.getBool(RtspCameraStreamConstant.prefKeyEnabled) ?? false; + final savedUrl = prefs.getString(RtspCameraStreamConstant.prefKeyUrl); + if (!isEnabled || savedUrl == null || savedUrl.isEmpty) { + return RTSPCameraSettingsState( + isRTSPEnabled: isEnabled, + streamUrl: savedUrl, + isInvalidUrl: false, + ); + } + return await _initializeFromSavedUrl(isEnabled: isEnabled, url: savedUrl); + } catch (e, s) { + throw RTSPInitializationException(e.toString()); + } + } + + Future _initializeFromSavedUrl({ + required bool isEnabled, + required String url, + }) async { + try { + await dispose(); + if (RtspCameraStreamConstant.youtubeUrlRegex.hasMatch(url)) { + return await _handleYoutubeStream(isEnabled, url); + } else if (url.startsWith('rtsp://')) { + return await _handleRTSPStream(isEnabled, url); + } + + throw InvalidRTSPURLException('Invalid URL format: $url'); + } catch (e) { + if (e is InvalidRTSPURLException) { + return RTSPCameraSettingsState( + isRTSPEnabled: isEnabled, + streamUrl: url, + isInvalidUrl: true, + ); + } + return RTSPCameraSettingsState( + isRTSPEnabled: isEnabled, + streamUrl: url, + ); + } + } + + // Modified toggleEnabled method + Future toggleEnabled(bool isEnabled) async { + try { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(RtspCameraStreamConstant.prefKeyEnabled, isEnabled); + + final currentState = state.value; + if (currentState != null) { + if (!isEnabled) { + await pauseStreams(); + } + state = AsyncValue.data( + currentState.copyWith( + isRTSPEnabled: isEnabled, + isInvalidUrl: false, + ), + ); + + if (isEnabled && currentState.streamUrl != null) { + await updateStream(isEnabled: isEnabled, url: currentState.streamUrl ?? ''); + await resumeStreams(); + } + } + } catch (e, s) { + state = AsyncValue.error(RTSPToggleException(e.toString()), s); + } + } + + Future updateStream({ + required bool isEnabled, + required String url, + }) async { + state = const AsyncValue.loading(); + try { + if (url.isEmpty) { + throw URLNotProvidedRTSPURLException(url); + } + + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(RtspCameraStreamConstant.prefKeyEnabled, isEnabled); + await prefs.setString(RtspCameraStreamConstant.prefKeyUrl, url); + + // Dispose of existing controllers before creating new ones + await dispose(); + + // Handle YouTube URLs (including live streams) + if (RtspCameraStreamConstant.youtubeUrlRegex.hasMatch(url)) { + final newState = await _handleYoutubeStream(isEnabled, url); + if (state.hasValue) { + // Ensure we're not keeping any references to old controllers + state = AsyncValue.data( + state.value!.copyWith( + videoController: null, + youtubeController: newState.youtubeController, + streamType: StreamType.youtubeLive, + streamUrl: url, + isInvalidUrl: false, + ), + ); + } else { + state = AsyncValue.data(newState); + } + return; + } + // Handle RTSP URLs + else if (url.startsWith('rtsp://')) { + final newState = await _handleRTSPStream(isEnabled, url); + if (state.hasValue) { + // Ensure we're not keeping any references to old controllers + state = AsyncValue.data( + state.value!.copyWith( + youtubeController: null, + videoController: newState.videoController, + streamType: StreamType.rtsp, + streamUrl: url, + isInvalidUrl: false, + ), + ); + } else { + state = AsyncValue.data(newState); + } + return; + } + + throw InvalidRTSPURLException('Invalid URL format: $url'); + } catch (e, s) { + // Clean up on error + await dispose(); + + if (e is InvalidRTSPURLException || e is URLNotProvidedRTSPURLException) { + state = AsyncValue.data( + state.value!.copyWith( + isInvalidUrl: true, + videoController: null, + youtubeController: null, + ), + ); + } else { + log('Error updating stream: $e', error: e, stackTrace: s); + state = AsyncValue.error(e, s); + } + } + } + + String? extractVideoId(String url) { + if (url.contains('youtube.com/live/')) { + return url.split('youtube.com/live/')[1].split('?').first; + } + return YoutubePlayer.convertUrlToId(url); + } + + Future _handleYoutubeStream(bool isEnabled, String url) async { + try { + // Ensure previous controllers are disposed + await dispose(); + + final videoId = extractVideoId(url); + if (videoId == null) { + throw InvalidRTSPURLException('URL is empty: $url'); + } + _youtubeController = YoutubePlayerController( + initialVideoId: videoId, + flags: const YoutubePlayerFlags( + autoPlay: true, + mute: false, + enableCaption: false, + hideControls: true, + isLive: true, + useHybridComposition: true, + forceHD: true, + ), + ); + + return RTSPCameraSettingsState( + isRTSPEnabled: isEnabled, + streamUrl: url, + isInvalidUrl: false, + streamType: StreamType.youtubeLive, + youtubeController: _youtubeController, + ); + } catch (e) { + await dispose(); + throw YouTubeVideoIdExtractionException(e.toString()); + } + } + + Future _handleRTSPStream(bool isEnabled, String url) async { + try { + // Ensure previous controllers are disposed + await dispose(); + + _player = Player(); + _videoController = VideoController(_player!); + await _player!.open(Media(url)); + + return RTSPCameraSettingsState( + isRTSPEnabled: isEnabled, + streamUrl: url, + streamType: StreamType.rtsp, + isInvalidUrl: false, + videoController: _videoController, + ); + } catch (e) { + await dispose(); + throw RTSPStreamUpdateException(e.toString()); + } + } + + // Add this method to pause/stop streams + Future pauseStreams() async { + _youtubeController?.pause(); + await _player?.pause(); + } + + // Add this method to resume streams + Future resumeStreams() async { + _youtubeController?.play(); + await _player?.play(); + } +} + +final rtspCameraSettingsProvider = + AutoDisposeAsyncNotifierProvider(() { + return RTSPCameraSettingsNotifier(); +}); diff --git a/lib/src/state_management/rtsp_camera_stream/rtsp_camera_stream_state.dart b/lib/src/state_management/rtsp_camera_stream/rtsp_camera_stream_state.dart new file mode 100644 index 000000000..e5b6d7838 --- /dev/null +++ b/lib/src/state_management/rtsp_camera_stream/rtsp_camera_stream_state.dart @@ -0,0 +1,63 @@ +import 'dart:async'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mawaqit/src/state_management/rtsp_camera_stream/rtsp_camera_stream_notifier.dart'; +import 'package:media_kit_video/media_kit_video.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:youtube_player_flutter/youtube_player_flutter.dart'; + +class RTSPCameraSettingsState extends Equatable { + final bool isRTSPEnabled; + final String? streamUrl; + final StreamType? streamType; + final VideoController? videoController; + final YoutubePlayerController? youtubeController; + final bool isInvalidUrl; + + const RTSPCameraSettingsState({ + this.isRTSPEnabled = false, + this.streamUrl, + this.streamType, + this.videoController, + this.youtubeController, + this.isInvalidUrl = false, + }); + + RTSPCameraSettingsState copyWith({ + bool? isRTSPEnabled, + String? streamUrl, + StreamType? streamType, + VideoController? videoController, + YoutubePlayerController? youtubeController, + bool? invalidStreamUrl, + bool? showValidationSnackbar, + bool? isInvalidUrl, + }) { + return RTSPCameraSettingsState( + isRTSPEnabled: isRTSPEnabled ?? this.isRTSPEnabled, + streamUrl: streamUrl ?? this.streamUrl, + streamType: streamType ?? this.streamType, + videoController: videoController ?? this.videoController, + youtubeController: youtubeController ?? this.youtubeController, + isInvalidUrl: isInvalidUrl ?? this.isInvalidUrl, + ); + } + + @override + String toString() { + return 'RTSPCameraSettingsState(isRTSPEnabled: $isRTSPEnabled, streamUrl: $streamUrl, streamType: $streamType, videoController: $videoController, youtubeController: $youtubeController, isInvalidUrl: $isInvalidUrl)'; + } + + @override + List get props { + return [ + isRTSPEnabled, + streamUrl, + streamType, + videoController, + youtubeController, + isInvalidUrl, + ]; + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 2a6ac6fbf..e53bc1855 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -96,7 +96,9 @@ dependencies: hijri: ^3.0.0 # webview_flutter: ^3.0.0 - + media_kit: ^1.1.11 # Primary package. + media_kit_video: ^1.2.5 # For video rendering. + media_kit_libs_video: ^1.0.5 # Native video dependencies. rive_splash_screen: ^0.1.1 lottie: ^2.3.2 flutter_svg: ^2.0.5 From a69a4f497353fbad168264a72fe7c7802c18961e Mon Sep 17 00:00:00 2001 From: Ibrahim ZEHHAF <97339607+ibrahim-zehhaf-mawaqit@users.noreply.github.com> Date: Sat, 30 Nov 2024 12:01:03 +0100 Subject: [PATCH 6/9] Update pubspec.yaml --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index acbcdf719..9bcff344f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,7 +17,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.17.3+1 +version: 1.18.0+1 environment: From 2681a07645b3b5578526c65827cff2f76d11179f Mon Sep 17 00:00:00 2001 From: Yassin Nouh <70436855+YassinNouh21@users.noreply.github.com> Date: Tue, 3 Dec 2024 12:20:42 +0200 Subject: [PATCH 7/9] feat(localization): add Montenegrin language support (#1442) * feat(localization): add Montenegrin language support - Added `montenegrin_localization: ^0.1.0+1` to `pubspec.yaml`. * refactor: no need for concatenation * fix formatting --------- Co-authored-by: Ghassen Ben Zahra --- assets/img/flag/cnr.png | Bin 0 -> 62098 bytes lib/l10n/intl_cnr.arb | 369 ++++++++++++++++++++++++++++++++++++++ lib/main.dart | 4 + lib/src/const/config.dart | 1 + pubspec.yaml | 4 +- 5 files changed, 377 insertions(+), 1 deletion(-) create mode 100644 assets/img/flag/cnr.png create mode 100644 lib/l10n/intl_cnr.arb diff --git a/assets/img/flag/cnr.png b/assets/img/flag/cnr.png new file mode 100644 index 0000000000000000000000000000000000000000..67cf6d3f085b7d67220860bdd7c14ff9ac36a3c4 GIT binary patch literal 62098 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 )t@}LE1B4sNReeO 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 z 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 Date: Tue, 3 Dec 2024 15:04:26 +0100 Subject: [PATCH 8/9] Fix/ Update 1.18 branch with main changes (#1463) * fix: Improve Quran Download and Navigation Experience (#1452) * fix: Ensure correct Moshaf type (Hafs) is displayed after download * fix: display Hafs Quran correctly and remove success dialog - Set Hafs as default Moshaf type if none is selected. - Auto-dismiss success dialog on download completion. - Improved state invalidation for Quran reading updates. - Added FocusNode for better dialog interaction. - Optimized resource management with keepAlive and link.close(). * fix: improve Quran Download and Navigation Experience - Redirect user to Quran reading screen automatically after successful download and extraction of Quran (Hafs). - Remove the unnecessary "OK" button to confirm Quran download completion, streamlining the user experience. - Enhance state management for download-related UI in `quran_reading_screen.dart` to handle various download states (needed, downloading, extracting). - Update `download_quran_popup.dart` to ensure proper navigation based on the user's first-time download experience. - Improve error handling and loading indicators for a smoother and more intuitive flow. * fix formating * Update pubspec.yaml * fix: Resolve overlapping and focus issues for Back and Switch buttons (#1457) * fix: Resolve pop-up issue when selecting Listening mode (#1455) - updated `_handleNavigation` method in `quran_mode_selection_screen.dart` to use `async/await` for ensuring proper completion of Quran mode selection before navigation. - fixed unexpected pop-ups by adjusting the handling of the `moshafType` state in `download_quran_popup.dart`. - improved navigation flow for both Reading and Listening modes, ensuring seamless user experience. Co-authored-by: Ghassen Ben Zahra * Update pubspec.yaml --------- Co-authored-by: Yassin Nouh <70436855+YassinNouh21@users.noreply.github.com> Co-authored-by: Ibrahim ZEHHAF <97339607+ibrahim-zehhaf-mawaqit@users.noreply.github.com> --- .../page/quran_mode_selection_screen.dart | 6 +- .../quran/reading/quran_reading_screen.dart | 94 +++---- .../quran/widget/download_quran_popup.dart | 231 +++++++++++------- .../download_quran_notifier.dart | 18 +- .../quran/reading/quran_reading_notifer.dart | 87 ++++--- 5 files changed, 268 insertions(+), 168 deletions(-) diff --git a/lib/src/pages/quran/page/quran_mode_selection_screen.dart b/lib/src/pages/quran/page/quran_mode_selection_screen.dart index 093ebdec1..fe66126d5 100644 --- a/lib/src/pages/quran/page/quran_mode_selection_screen.dart +++ b/lib/src/pages/quran/page/quran_mode_selection_screen.dart @@ -80,12 +80,12 @@ class _QuranModeSelectionState extends ConsumerState { } } - void _handleNavigation(int index) { + Future _handleNavigation(int index) async { if (index == 0) { - ref.read(quranNotifierProvider.notifier).selectModel(QuranMode.reading); + await ref.read(quranNotifierProvider.notifier).selectModel(QuranMode.reading); Navigator.pushReplacementNamed(context, Routes.quranReading); } else { - ref.read(quranNotifierProvider.notifier).selectModel(QuranMode.listening); + await ref.read(quranNotifierProvider.notifier).selectModel(QuranMode.listening); Navigator.pushReplacementNamed(context, Routes.quranReciter); } } diff --git a/lib/src/pages/quran/reading/quran_reading_screen.dart b/lib/src/pages/quran/reading/quran_reading_screen.dart index 74fda1aea..fa2ff88dc 100644 --- a/lib/src/pages/quran/reading/quran_reading_screen.dart +++ b/lib/src/pages/quran/reading/quran_reading_screen.dart @@ -136,11 +136,6 @@ class NormalViewStrategy implements QuranViewStrategy { ) { if (isPortrait) { return [ - BackButtonWidget( - isPortrait: isPortrait, - userPrefs: userPrefs, - focusNode: focusNodes.backButtonNode, - ), SurahSelectorWidget( isPortrait: isPortrait, focusNode: focusNodes.surahSelectorNode, @@ -157,15 +152,15 @@ class NormalViewStrategy implements QuranViewStrategy { focusNode: focusNodes.switchQuranNode, isThereCurrentDialogShowing: false, ), + BackButtonWidget( + isPortrait: isPortrait, + userPrefs: userPrefs, + focusNode: focusNodes.backButtonNode, + ), ]; } return [ - BackButtonWidget( - isPortrait: isPortrait, - userPrefs: userPrefs, - focusNode: focusNodes.backButtonNode, - ), _buildNavigationButtons( context, focusNodes, @@ -188,6 +183,11 @@ class NormalViewStrategy implements QuranViewStrategy { focusNode: focusNodes.switchQuranNode, isThereCurrentDialogShowing: false, ), + BackButtonWidget( + isPortrait: isPortrait, + userPrefs: userPrefs, + focusNode: focusNodes.backButtonNode, + ), ]; } @@ -247,7 +247,6 @@ class _QuranReadingScreenState extends ConsumerState { WidgetsBinding.instance.addPostFrameCallback((_) async { ref.read(downloadQuranNotifierProvider); - ref.read(quranReadingNotifierProvider); }); } @@ -321,38 +320,51 @@ class _QuranReadingScreenState extends ConsumerState { }); final autoReadingState = ref.watch(autoScrollNotifierProvider); - - return WillPopScope( - onWillPop: () async { - userPrefs.orientationLandscape = true; - return true; - }, - child: quranReadingState.when( - data: (state) { - setState(() { - _isRotated = state.isRotated; - }); - return RotatedBox( - quarterTurns: state.isRotated ? -1 : 0, - child: SizedBox( - width: MediaQuery.of(context).size.height, - height: MediaQuery.of(context).size.width, - child: Scaffold( - backgroundColor: Colors.white, - floatingActionButtonLocation: _getFloatingActionButtonLocation(context), - floatingActionButton: QuranFloatingActionControls( - switchScreenViewFocusNode: _switchScreenViewFocusNode, - switchQuranModeNode: _switchQuranModeNode, - switchToPlayQuranFocusNode: _switchToPlayQuranFocusNode, - ), - body: _buildBody(quranReadingState, state.isRotated, userPrefs, autoReadingState), - ), + final downloadState = ref.watch(downloadQuranNotifierProvider); + return downloadState.when( + data: (data) { + if (data is NeededDownloadedQuran || data is Downloading || data is Extracting) { + return Scaffold( + body: Container( + color: Colors.white, ), ); - }, - loading: () => SizedBox(), - error: (error, stack) => const Icon(Icons.error), - ), + } + return WillPopScope( + onWillPop: () async { + userPrefs.orientationLandscape = true; + return true; + }, + child: quranReadingState.when( + data: (state) { + setState(() { + _isRotated = state.isRotated; + }); + return RotatedBox( + quarterTurns: state.isRotated ? -1 : 0, + child: SizedBox( + width: MediaQuery.of(context).size.height, + height: MediaQuery.of(context).size.width, + child: Scaffold( + backgroundColor: Colors.white, + floatingActionButtonLocation: _getFloatingActionButtonLocation(context), + floatingActionButton: QuranFloatingActionControls( + switchScreenViewFocusNode: _switchScreenViewFocusNode, + switchQuranModeNode: _switchQuranModeNode, + switchToPlayQuranFocusNode: _switchToPlayQuranFocusNode, + ), + body: _buildBody(quranReadingState, state.isRotated, userPrefs, autoReadingState), + ), + ), + ); + }, + loading: () => Scaffold(body: SizedBox()), + error: (error, stack) => Scaffold(body: const Icon(Icons.error)), + ), + ); + }, + loading: () => Scaffold(body: _buildLoadingIndicator()), + error: (error, stack) => Scaffold(body: _buildErrorIndicator(error)), ); } diff --git a/lib/src/pages/quran/widget/download_quran_popup.dart b/lib/src/pages/quran/widget/download_quran_popup.dart index a1e0db5ca..bc6db8535 100644 --- a/lib/src/pages/quran/widget/download_quran_popup.dart +++ b/lib/src/pages/quran/widget/download_quran_popup.dart @@ -8,7 +8,7 @@ import 'package:mawaqit/src/domain/model/quran/moshaf_type_model.dart'; import 'package:mawaqit/src/state_management/quran/download_quran/download_quran_notifier.dart'; import 'package:mawaqit/src/state_management/quran/download_quran/download_quran_state.dart'; import 'package:mawaqit/src/state_management/quran/reading/moshaf_type_notifier.dart'; -import 'package:mawaqit/src/state_management/quran/reading/quran_reading_state.dart'; +import 'package:mawaqit/src/state_management/quran/reading/quran_reading_notifer.dart'; class DownloadQuranDialog extends ConsumerStatefulWidget { const DownloadQuranDialog({super.key}); @@ -19,15 +19,23 @@ class DownloadQuranDialog extends ConsumerStatefulWidget { class _DownloadQuranDialogState extends ConsumerState { MoshafType selectedMoshafType = MoshafType.hafs; + late FocusNode _dialogFocusNode; @override void initState() { super.initState(); + _dialogFocusNode = FocusNode(); WidgetsBinding.instance.addPostFrameCallback((_) { _checkForUpdate(); }); } + @override + void dispose() { + _dialogFocusNode.dispose(); + super.dispose(); + } + void _checkForUpdate() { final notifier = ref.read(downloadQuranNotifierProvider.notifier); // notifier.checkForUpdate(notifier.selectedMoshafType); @@ -35,14 +43,34 @@ class _DownloadQuranDialogState extends ConsumerState { @override Widget build(BuildContext context) { - final state = ref.watch(downloadQuranNotifierProvider); - return state.when( - data: (data) => _buildContent(context, data), - loading: () => Container(), - error: (error, _) => _buildErrorDialog(context, error), + final downloadState = ref.watch(downloadQuranNotifierProvider); + + return downloadState.when( + data: (data) => _buildDialogContent(context, data), + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stack) => _buildErrorDialog(context, error), ); } + Widget _buildDialogContent(BuildContext context, DownloadQuranState state) { + return switch (state) { + NeededDownloadedQuran() => _buildChooseDownloadMoshaf(context), + Downloading() => _buildDownloadingDialog(context, state), + Extracting() => _buildExtractingDialog(context, state), + Success() => _handleSuccess(context), + CancelDownload() => const SizedBox(), + _ => const SizedBox(), + }; + } + + Widget _handleSuccess(BuildContext context) { + // Auto close dialog on success + WidgetsBinding.instance.addPostFrameCallback((_) { + Navigator.of(context).pop(); + }); + return const SizedBox(); + } + Widget _buildContent(BuildContext context, DownloadQuranState state) { // return Container(); return switch (state) { @@ -50,7 +78,7 @@ class _DownloadQuranDialogState extends ConsumerState { // UpdateAvailable() => _buildUpdateAvailableDialog(context, state), Downloading() => _buildDownloadingDialog(context, state), Extracting() => _buildExtractingDialog(context, state), - Success() => _buildSuccessDialog(context, state), + Success() => _successDialog(context), CancelDownload() => Container(), // NoUpdate() => _buildNoUpdateDialog(context, state), _ => Container(), @@ -79,63 +107,67 @@ class _DownloadQuranDialogState extends ConsumerState { } Widget _buildDownloadingDialog(BuildContext context, Downloading state) { - return AlertDialog( - title: Text(S.of(context).downloadingQuran), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - LinearProgressIndicator(value: state.progress / 100), - SizedBox(height: 16), - Text('${state.progress.toStringAsFixed(2)}%'), - ], - ), - actions: [ - TextButton( - autofocus: true, - onPressed: () async { - final notifier = ref.read(downloadQuranNotifierProvider.notifier); - ref.read(moshafTypeNotifierProvider).maybeWhen( - orElse: () {}, - data: (state) async { - state.selectedMoshaf.fold(() { - return null; - }, (selectedMoshaf) async { - await notifier.cancelDownload(selectedMoshaf); // Await cancellation - Navigator.pop(context); // Close dialog after cancel completes - }); - }, - ); - }, - child: Text(S.of(context).cancel), + return Focus( + focusNode: _dialogFocusNode, + child: AlertDialog( + title: Text(S.of(context).downloadingQuran), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + LinearProgressIndicator(value: state.progress / 100), + SizedBox(height: 16), + Text('${state.progress.toStringAsFixed(2)}%'), + ], ), - ], - ); - } - - Widget _buildExtractingDialog(BuildContext context, Extracting state) { - return AlertDialog( - title: Text(S.of(context).extractingQuran), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - LinearProgressIndicator(value: state.progress / 100), - SizedBox(height: 16), - Text('${state.progress.toStringAsFixed(2)}%'), + actions: [ + TextButton( + autofocus: true, + onPressed: () async { + final notifier = ref.read(downloadQuranNotifierProvider.notifier); + final moshafType = ref.watch(moshafTypeNotifierProvider); + ref.read(moshafTypeNotifierProvider).maybeWhen( + orElse: () {}, + data: (state) async { + state.selectedMoshaf.fold(() { + return null; + }, (selectedMoshaf) async { + await notifier.cancelDownload(selectedMoshaf); // Await cancellation + }); + }, + ); + moshafType.when( + data: (data) { + if (data.isFirstTime) { + Navigator.popUntil(context, (route) => route.isFirst); + } else { + Navigator.pop(context); + } + }, + error: (_, __) {}, + loading: () {}, + ); + }, + child: Text(S.of(context).cancel), + ), ], ), ); } - Widget _buildSuccessDialog(BuildContext context, Success state) { - return AlertDialog( - title: Text(S.of(context).quranDownloaded), - actions: [ - TextButton( - autofocus: true, - onPressed: () => Navigator.pop(context), - child: Text(S.of(context).ok), + Widget _buildExtractingDialog(BuildContext context, Extracting state) { + return Focus( + focusNode: _dialogFocusNode, + child: AlertDialog( + title: Text(S.of(context).extractingQuran), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + LinearProgressIndicator(value: state.progress / 100), + SizedBox(height: 16), + Text('${state.progress.toStringAsFixed(2)}%'), + ], ), - ], + ), ); } @@ -152,41 +184,57 @@ class _DownloadQuranDialogState extends ConsumerState { } Widget _buildChooseDownloadMoshaf(BuildContext context) { - return AlertDialog( - title: Text(S.of(context).chooseQuranType), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - _buildMoshafTypeRadio( - context, - title: S.of(context).warsh, - value: MoshafType.warsh, - setState: setState, + return Focus( + focusNode: _dialogFocusNode, + child: AlertDialog( + title: Text(S.of(context).chooseQuranType), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildMoshafTypeRadio( + context, + title: S.of(context).warsh, + value: MoshafType.warsh, + setState: setState, + autofocus: selectedMoshafType == MoshafType.warsh, + ), + _buildMoshafTypeRadio( + context, + title: S.of(context).hafs, + value: MoshafType.hafs, + setState: setState, + autofocus: selectedMoshafType == MoshafType.hafs, + ), + ], + ), + actions: [ + TextButton( + onPressed: () { + final moshafType = ref.watch(moshafTypeNotifierProvider); + moshafType.when( + data: (data) { + if (data.isFirstTime) { + Navigator.popUntil(context, (route) => route.isFirst); + } else { + Navigator.pop(context); + } + }, + error: (_, __) {}, + loading: () {}, + ); + }, + child: Text(S.of(context).cancel), ), - _buildMoshafTypeRadio( - context, - title: S.of(context).hafs, - value: MoshafType.hafs, - setState: setState, + TextButton( + autofocus: true, + onPressed: () async { + Navigator.pop(context); + await ref.read(downloadQuranNotifierProvider.notifier).downloadQuran(selectedMoshafType); + }, + child: Text(S.of(context).download), ), ], ), - actions: [ - TextButton( - onPressed: () { - Navigator.pop(context); - }, - child: Text(S.of(context).cancel), - ), - TextButton( - autofocus: true, - onPressed: () async { - Navigator.pop(context); - await ref.read(downloadQuranNotifierProvider.notifier).downloadQuran(selectedMoshafType); - }, - child: Text(S.of(context).download), - ), - ], ); } @@ -195,11 +243,12 @@ class _DownloadQuranDialogState extends ConsumerState { required String title, required MoshafType value, required void Function(VoidCallback fn) setState, + bool autofocus = false, }) { return RadioListTile( title: Text(title), value: value, - autofocus: true, + autofocus: autofocus, groupValue: selectedMoshafType, onChanged: (MoshafType? selected) { setState(() { @@ -232,4 +281,8 @@ class _DownloadQuranDialogState extends ConsumerState { ], ); } + + Widget _successDialog(BuildContext context) { + return Container(); + } } diff --git a/lib/src/state_management/quran/download_quran/download_quran_notifier.dart b/lib/src/state_management/quran/download_quran/download_quran_notifier.dart index 74d22929e..46e535360 100644 --- a/lib/src/state_management/quran/download_quran/download_quran_notifier.dart +++ b/lib/src/state_management/quran/download_quran/download_quran_notifier.dart @@ -131,13 +131,23 @@ class DownloadQuranNotifier extends AutoDisposeAsyncNotifier } Future downloadQuran(MoshafType moshafType) async { - state = const AsyncLoading(); + final link = ref.keepAlive(); // Keep alive during download + try { + state = const AsyncLoading(); + + // First ensure moshaf type is selected + await ref.read(moshafTypeNotifierProvider.notifier).selectMoshafType(moshafType); + final downloadState = await _downloadQuran(moshafType); if (downloadState is Success) { await ref.read(moshafTypeNotifierProvider.notifier).setNotFirstTime(); - } - if (downloadState is! CancelDownload) { + + state = AsyncData(downloadState); + + // Force rebuild reading provider in next frame + ref.invalidate(quranReadingNotifierProvider); + } else if (downloadState is! CancelDownload) { state = AsyncData(downloadState); } } catch (e, s) { @@ -145,6 +155,8 @@ class DownloadQuranNotifier extends AutoDisposeAsyncNotifier return; } state = AsyncError(e, s); + } finally { + link.close(); } } diff --git a/lib/src/state_management/quran/reading/quran_reading_notifer.dart b/lib/src/state_management/quran/reading/quran_reading_notifer.dart index d6fd02e25..b23bcc3eb 100644 --- a/lib/src/state_management/quran/reading/quran_reading_notifer.dart +++ b/lib/src/state_management/quran/reading/quran_reading_notifer.dart @@ -1,12 +1,15 @@ import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:mawaqit/src/const/constants.dart'; import 'package:mawaqit/src/domain/model/quran/moshaf_type_model.dart'; import 'package:mawaqit/src/domain/model/quran/surah_model.dart'; import 'package:mawaqit/src/domain/repository/quran/quran_reading_repository.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:mawaqit/src/module/shared_preference_module.dart'; +import 'package:mawaqit/src/state_management/quran/download_quran/download_quran_notifier.dart'; +import 'package:mawaqit/src/state_management/quran/download_quran/download_quran_state.dart'; import 'package:mawaqit/src/state_management/quran/quran/quran_notifier.dart'; import 'package:mawaqit/src/state_management/quran/reading/moshaf_type_notifier.dart'; import 'package:mawaqit/src/state_management/quran/reading/quran_reading_state.dart'; @@ -15,13 +18,24 @@ import 'package:mawaqit/src/data/repository/quran/quran_reading_impl.dart'; class QuranReadingNotifier extends AutoDisposeAsyncNotifier { @override Future build() async { - final repository = ref.read(quranReadingRepositoryProvider.future); - ref.onDispose(() { - if (state.hasValue) { - state.value!.pageController.dispose(); - } - }); - return _initState(repository); + final link = ref.keepAlive(); + + try { + final repository = await ref.read(quranReadingRepositoryProvider.future); + + ref.onDispose(() { + if (state.hasValue) { + state.value!.pageController.dispose(); + } + }); + + final result = await _initState(repository); + link.close(); + return result; + } catch (e) { + link.close(); + rethrow; + } } void nextPage({bool isPortrait = false}) async { @@ -106,38 +120,47 @@ class QuranReadingNotifier extends AutoDisposeAsyncNotifier { ); } - Future _initState(Future repository) async { - final quranReadingRepository = await repository; + Future _initState(QuranReadingRepository repository) async { final mosqueModel = await ref.read(moshafTypeNotifierProvider.future); - return mosqueModel.selectedMoshaf.fold( - () { - throw Exception('No MoshafType'); - }, - (moshaf) async { - state = AsyncLoading(); - final svgs = await _loadSvgs(moshafType: moshaf); - final lastReadPage = await quranReadingRepository.getLastReadPage(); - final pageController = PageController(initialPage: (lastReadPage / 2).floor()); - final suwar = await getAllSuwar(); - final initialSurahName = _getCurrentSurahName(lastReadPage, suwar); - return QuranReadingState( - currentJuz: 1, - currentSurah: 1, - suwar: suwar, - currentPage: lastReadPage, - svgs: svgs, - pageController: pageController, - currentSurahName: initialSurahName, - ); - }, - ); + + try { + // Get moshaf type or set default + final moshafType = mosqueModel.selectedMoshaf.getOrElse(() => MoshafType.hafs); + + // Set moshaf type if none selected + if (mosqueModel.selectedMoshaf.isNone()) { + await ref.read(moshafTypeNotifierProvider.notifier).selectMoshafType(moshafType); + } + + state = AsyncLoading(); + final svgs = await _loadSvgs(moshafType: moshafType); + + if (svgs.isEmpty) { + throw Exception('No SVGs found for moshaf type: ${moshafType.name}'); + } + + final lastReadPage = await repository.getLastReadPage(); + final pageController = PageController(initialPage: (lastReadPage / 2).floor()); + final suwar = await getAllSuwar(); + + return QuranReadingState( + currentJuz: 1, + currentSurah: 1, + suwar: suwar, + currentPage: lastReadPage, + svgs: svgs, + pageController: pageController, + currentSurahName: _getCurrentSurahName(lastReadPage, suwar), + ); + } catch (e) { + rethrow; + } } Future _saveLastReadPage(int index) async { try { final quranRepository = await ref.read(quranReadingRepositoryProvider.future); await quranRepository.saveLastReadPage(index); - log('quran: QuranReadingNotifier: Saved last read page: $index'); } catch (e, s) { state = AsyncError(e, s); } From 90eaa393c9259239b6d43e54ed8d598bf036c8ed Mon Sep 17 00:00:00 2001 From: Yassin Nouh <70436855+YassinNouh21@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:52:56 +0200 Subject: [PATCH 9/9] feat: update Quran configuration file URL to tv_config.json (#1465) * feat: update Quran configuration file URL to tv_config.json * feat(quran): Add moshaf type and version to update notification - Enhanced `UpdateAvailable` state to include `moshafType` (Hafs or Warsh) alongside version information. - Updated notifier logic to handle `moshafType` when checking and comparing Quran updates. - Localized new dialog content with placeholders for `moshafName` and `version` in `intl_en.arb`. - Refactored update dialog to display the moshaf type and version dynamically. - Streamlined `downloadQuran` functionality to pass the correct moshaf type to the notifier. * fix: solve the overlapping issue when updating the Quran version * fix formatting --- lib/l10n/intl_en.arb | 14 +++++++++++ lib/src/const/constants.dart | 2 +- .../repository/quran/quran_download_impl.dart | 20 +++++++++++++++ .../quran/widget/download_quran_popup.dart | 23 +++++++---------- .../download_quran_notifier.dart | 15 ++++++++--- .../download_quran/download_quran_state.dart | 11 +++++++- .../quran/reading/quran_reading_notifer.dart | 25 ++++++++++++++++--- 7 files changed, 88 insertions(+), 22 deletions(-) diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 237a0b3fd..aa41e2135 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -395,5 +395,19 @@ "example": "604" } } + }, + "quranUpdateDialogContent": "A new update for the {moshafName} Quran (version {version}) is available.", + "@quranUpdateDialogContent": { + "description": "Message to display in the update available dialog with Quran name and version.", + "placeholders": { + "moshafName": { + "type": "String", + "example": "Hafs" + }, + "version": { + "type": "String", + "example": "2.0" + } + } } } diff --git a/lib/src/const/constants.dart b/lib/src/const/constants.dart index 9ddb1f0c5..5dc154d59 100644 --- a/lib/src/const/constants.dart +++ b/lib/src/const/constants.dart @@ -76,7 +76,7 @@ abstract class QuranConstant { static const String kQuranModePref = 'quran_mode'; static const String kSavedCurrentPage = 'saved_current_page'; static const String kFavoriteReciterBox = 'favorite_reciter_box'; - static const String quranMoshafConfigJsonUrl = 'https://cdn.mawaqit.net/quran/config.json'; + static const String quranMoshafConfigJsonUrl = 'https://cdn.mawaqit.net/quran/tv_config.json'; static const String kIsFirstTime = 'is_first_time_quran'; static const String kQuranReciterImagesBaseUrl = 'https://cdn.mawaqit.net/quran/reciters-pictures/'; } diff --git a/lib/src/data/repository/quran/quran_download_impl.dart b/lib/src/data/repository/quran/quran_download_impl.dart index 14c8ae07d..a8730ce37 100644 --- a/lib/src/data/repository/quran/quran_download_impl.dart +++ b/lib/src/data/repository/quran/quran_download_impl.dart @@ -48,6 +48,9 @@ class QuranDownloadRepositoryImpl implements QuranDownloadRepository { String? filePath, }) async { try { + // Clean up existing files before downloading + await _cleanupExistingFiles(); + await remoteDataSource.downloadQuranWithProgress( version: version, moshafType: moshafType, @@ -75,6 +78,23 @@ class QuranDownloadRepositoryImpl implements QuranDownloadRepository { } } + Future _cleanupExistingFiles() async { + try { + // Delete the quran directory if it exists + if (await Directory(quranPathHelper.quranDirectoryPath).exists()) { + await Directory(quranPathHelper.quranDirectoryPath).delete(recursive: true); + } + + // Delete the zip directory if it exists + if (await Directory(quranPathHelper.quranZipDirectoryPath).exists()) { + await Directory(quranPathHelper.quranZipDirectoryPath).delete(recursive: true); + } + } catch (e) { + // Log error but don't throw, as this is cleanup + print('Error during cleanup: $e'); + } + } + Future _cleanupAfterCancellation(String version) async { await DirectoryHelper.deleteDirectories([ quranPathHelper.quranZipDirectoryPath, diff --git a/lib/src/pages/quran/widget/download_quran_popup.dart b/lib/src/pages/quran/widget/download_quran_popup.dart index bc6db8535..9900c2508 100644 --- a/lib/src/pages/quran/widget/download_quran_popup.dart +++ b/lib/src/pages/quran/widget/download_quran_popup.dart @@ -59,6 +59,7 @@ class _DownloadQuranDialogState extends ConsumerState { Extracting() => _buildExtractingDialog(context, state), Success() => _handleSuccess(context), CancelDownload() => const SizedBox(), + UpdateAvailable() => _buildUpdateAvailableDialog(context, state), _ => const SizedBox(), }; } @@ -87,8 +88,14 @@ class _DownloadQuranDialogState extends ConsumerState { } Widget _buildUpdateAvailableDialog(BuildContext context, UpdateAvailable state) { + final moshafName = switch (state.moshafType) { + MoshafType.warsh => S.of(context).warsh, + MoshafType.hafs => S.of(context).hafs, + }; + return AlertDialog( title: Text(S.of(context).updateAvailable), + content: Text(S.of(context).quranUpdateDialogContent(moshafName, state.version)), actions: [ TextButton( onPressed: () => Navigator.pop(context), @@ -97,8 +104,8 @@ class _DownloadQuranDialogState extends ConsumerState { TextButton( autofocus: true, onPressed: () { - // final notifier = ref.read(downloadQuranNotifierProvider.notifier); - // notifier.downloadQuran(notifier.selectedMoshafType); + final notifier = ref.read(downloadQuranNotifierProvider.notifier); + notifier.downloadQuran(state.moshafType); }, child: Text(S.of(context).download), ), @@ -171,18 +178,6 @@ class _DownloadQuranDialogState extends ConsumerState { ); } - Widget _buildNoUpdateDialog(BuildContext context, NoUpdate state) { - return AlertDialog( - title: Text(S.of(context).updatedQuran), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(S.of(context).ok), - ), - ], - ); - } - Widget _buildChooseDownloadMoshaf(BuildContext context) { return Focus( focusNode: _dialogFocusNode, diff --git a/lib/src/state_management/quran/download_quran/download_quran_notifier.dart b/lib/src/state_management/quran/download_quran/download_quran_notifier.dart index 46e535360..99fbb8cb6 100644 --- a/lib/src/state_management/quran/download_quran/download_quran_notifier.dart +++ b/lib/src/state_management/quran/download_quran/download_quran_notifier.dart @@ -81,7 +81,10 @@ class DownloadQuranNotifier extends AutoDisposeAsyncNotifier orElse: () async { final remoteVersion = await downloadQuranRepoImpl.getRemoteQuranVersion(moshafType: moshafType); return localVersionOption.fold( - () => UpdateAvailable(remoteVersion), + () => UpdateAvailable( + version: remoteVersion, + moshafType: moshafType, + ), (localVersion) => _compareVersions(moshafType, localVersion, remoteVersion), ); }, @@ -90,7 +93,10 @@ class DownloadQuranNotifier extends AutoDisposeAsyncNotifier final remoteVersion = await downloadQuranRepoImpl.getRemoteQuranVersion(moshafType: moshafType); return localVersionOption.fold( - () => UpdateAvailable(remoteVersion), + () => UpdateAvailable( + version: remoteVersion, + moshafType: moshafType, + ), (localVersion) => _compareVersions(moshafType, localVersion, remoteVersion), ); } else { @@ -115,7 +121,10 @@ class DownloadQuranNotifier extends AutoDisposeAsyncNotifier Future _compareVersions(MoshafType moshafType, String localVersion, String remoteVersion) async { if (VersionHelper.isNewer(remoteVersion, localVersion)) { - return UpdateAvailable(remoteVersion); + return UpdateAvailable( + version: remoteVersion, + moshafType: moshafType, + ); } else { final savePath = await getApplicationSupportDirectory(); final quranPathHelper = QuranPathHelper( diff --git a/lib/src/state_management/quran/download_quran/download_quran_state.dart b/lib/src/state_management/quran/download_quran/download_quran_state.dart index 81a52a95a..1619bee28 100644 --- a/lib/src/state_management/quran/download_quran/download_quran_state.dart +++ b/lib/src/state_management/quran/download_quran/download_quran_state.dart @@ -46,8 +46,17 @@ class NoUpdate extends DownloadQuranState with EquatableMixin { class UpdateAvailable extends DownloadQuranState { final String version; + final MoshafType moshafType; + + const UpdateAvailable({ + required this.version, + required this.moshafType, + }); - const UpdateAvailable(this.version); + @override + String toString() { + return 'UpdateAvailable: $version , $moshafType'; + } } class CancelDownload extends DownloadQuranState { diff --git a/lib/src/state_management/quran/reading/quran_reading_notifer.dart b/lib/src/state_management/quran/reading/quran_reading_notifer.dart index b23bcc3eb..68bbb19d1 100644 --- a/lib/src/state_management/quran/reading/quran_reading_notifer.dart +++ b/lib/src/state_management/quran/reading/quran_reading_notifer.dart @@ -77,9 +77,13 @@ class QuranReadingNotifier extends AutoDisposeAsyncNotifier { final currentState = state.value!; if (page >= 0 && page < currentState.totalPages) { await _saveLastReadPage(page); - !isPortairt - ? currentState.pageController.jumpToPage((page / 2).floor()) - : currentState.pageController.jumpToPage(page); // Jump to the selected page + + if (currentState.pageController.hasClients) { + !isPortairt + ? currentState.pageController.jumpToPage((page / 2).floor()) + : currentState.pageController.jumpToPage(page); + } + final newSurahName = _getCurrentSurahName(page, currentState.suwar); return currentState.copyWith( @@ -133,6 +137,10 @@ class QuranReadingNotifier extends AutoDisposeAsyncNotifier { } state = AsyncLoading(); + + // Clear any existing SVGs in memory + await _clearSvgCache(); + final svgs = await _loadSvgs(moshafType: moshafType); if (svgs.isEmpty) { @@ -157,6 +165,17 @@ class QuranReadingNotifier extends AutoDisposeAsyncNotifier { } } + Future _clearSvgCache() async { + // Clear any existing state + state = AsyncLoading(); + state = await AsyncValue.guard(() async { + return state.value!.copyWith( + svgs: [], + pageController: PageController(), + ); + }); + } + Future _saveLastReadPage(int index) async { try { final quranRepository = await ref.read(quranReadingRepositoryProvider.future);