From 8d578a9fd2ed736541a3e207adb4f15c86d6fc44 Mon Sep 17 00:00:00 2001 From: Ghassen Ben Zahra Date: Fri, 8 Nov 2024 17:21:30 +0100 Subject: [PATCH 01/26] invert parentheses (#1406) --- lib/l10n/intl_ar.arb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/l10n/intl_ar.arb b/lib/l10n/intl_ar.arb index a6c6af234..f55a102d1 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": "ستظهر الإعلانات طوال الوقت", From 7d9034e96eb56b9f1d8f866db942ff9ac17666b2 Mon Sep 17 00:00:00 2001 From: Ghassen Ben Zahra Date: Sun, 10 Nov 2024 21:57:43 +0100 Subject: [PATCH 02/26] 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 03/26] 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 04/26] 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 99645e2f884badc48abae44de00163d9a240779d Mon Sep 17 00:00:00 2001 From: Yassin Nouh <70436855+YassinNouh21@users.noreply.github.com> Date: Mon, 11 Nov 2024 21:53:28 +0200 Subject: [PATCH 05/26] feat: add Ukrainian language support (#1416) --- lib/l10n/intl_uk.arb | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/lib/l10n/intl_uk.arb b/lib/l10n/intl_uk.arb index 65de1eaeb..e17a1745d 100644 --- a/lib/l10n/intl_uk.arb +++ b/lib/l10n/intl_uk.arb @@ -168,7 +168,7 @@ "announcementOnlyMode": "Режим оголошень", "normalMode": "Звичайний режим ", "announcementOnlyModeEXPLINATION": "Виберіть, чи будуть на екрані постійно показуватися оголошення, це може бути корисно, якщо ви встановите екран, наприклад, біля входу.", - "duaaElEftarText": "", + "duaaElEftarText": "اللهُمَّ إِنِّي لَكَ صُمْتُ وَعَلَى رِزْقِكَ أَفْطَرْتُ، وَإِلَيْكَ انْبَتُّ، وَعَلَيْكَ تَوَكَّلْتُ، ذَهَبَ الظَّمَأُ وَابْتلّت الْعُرُوِقُ، وَثَبَتَ الْأَجْرُ إِنْ شَاءَ اللَّهُ.", "@duaaElEftarText": { "description": "اللهم اني لگ صمت وعلى رزقك افطرت واليك انبت وعليگ توكلت ذهب الظما وابتلت العروق وثبت الاجر انشاء الله" }, @@ -290,6 +290,20 @@ } } }, + "quranReadingPagePortrait": "Сторінка {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": "Оберіть сторінку", "checkingForUpdates": "Перевірка оновлень...", "chooseQuranType": "Оберіть Куран", @@ -320,5 +334,23 @@ "noFavoriteReciters": "Немає улюблених читців. Спробуйте додати одного до списку", "@noFavoriteReciters": { "description": "Message shown when there are no favorite reciters" - } + }, + "noReciterSearchResult": "Нічого не знайдено по вашому запиту", + "searchForReciter": "Пошук читця", + "downloadAllSuwarSuccessfully": "Увесь Куран завантажений", + "noSuwarDownload": "Немає нових сур для завантаження", + "connectDownloadQuran": "Будь ласка, підключіться до Інтернету для завантаження", + "playInOnlineModeQuran": "Для відтворення, під'єднайтеся до Інтернету", + "downloaded": "Завантажено", + "switchQuranType": "Перейти до {name}", + "@switchQuranType": { + "description": "Message shown when a reciter is added to favorites", + "placeholders": { + "name": { + "type": "String", + "example": "Warsh" + } + } + }, + "surahSelector": "Select Surah" } \ No newline at end of file From 3c6f261e4017934d97d839cd63f9885f3be1c4d5 Mon Sep 17 00:00:00 2001 From: Yassin Nouh <70436855+YassinNouh21@users.noreply.github.com> Date: Mon, 11 Nov 2024 21:54:12 +0200 Subject: [PATCH 06/26] feat: add and update more persian language (#1415) --- lib/l10n/intl_fa.arb | 65 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 4 deletions(-) diff --git a/lib/l10n/intl_fa.arb b/lib/l10n/intl_fa.arb index 7c0e6764a..452fe9fbb 100644 --- a/lib/l10n/intl_fa.arb +++ b/lib/l10n/intl_fa.arb @@ -20,7 +20,7 @@ "darkMode": "حالت تاریک", "lightMode": "حالت روشن", "changeMosque": " مسجد را تغییر بده", - "in1": "در", + "in1": "پس از", "sec": "ثانیه", "online": "فعال", "missingMosqueId": "کد شناسایی مسجد فراموش شده است", @@ -58,7 +58,7 @@ "alIqama": "اقامه", "alAdhan": "اذان", "turnOfPhones": "لطفا موبایل خود را خاموش کنید", - "iqamaIn": "اقامه در", + "iqamaIn": "اقامه پس از", "alAthkar": "اذکار", "azkarList0": "استغفر الله استغفر الله استغفر الله اللهم أنت السلام و منک السلام تبارکت یا ذا الجلال و الإکرام، اللَّهُمَّ أَعِنِّي عَلَی ذِكْرِكَ، وَشُكْرِكَ، وَحُسْنِ عِبَادَتِكَ", "@azkarList0": { @@ -88,6 +88,34 @@ "@azkarList6": { "description": "لا إِلَٰهَ إلاّ اللّهُ وحدَهُ لا شريكَ لهُ، لهُ المُـلْكُ ولهُ الحَمْد، وهوَ على كلّ شَيءٍ قَدير، اللّهُـمَّ لا مانِعَ لِما أَعْطَـيْت، وَلا مُعْطِـيَ لِما مَنَـعْت، وَلا يَنْفَـعُ ذا الجَـدِّ مِنْـكَ الجَـد" }, + "azkarList7": "اللهم أنت ربي، لا إله إلا أنت، خلقتني وأنا عبدُك, وأنا على عهدِك ووعدِك ما استطعتُ، أعوذ بك من شر ما صنعتُ، أبوءُ لَكَ بنعمتكَ عَلَيَّ، وأبوء بذنبي، فاغفر لي، فإنه لا يغفرُ الذنوب إلا أنت", + "@azkarList7": { + "description": "اللهم أنت ربي، لا إله إلا أنت، خلقتني وأنا عبدُك, وأنا على عهدِك ووعدِك ما استطعتُ، أعوذ بك من شر ما صنعتُ، أبوءُ لَكَ بنعمتكَ عَلَيَّ، وأبوء بذنبي، فاغفر لي، فإنه لا يغفرُ الذنوب إلا أنت" + }, + "azkarList8": "أصبحنا وأصبح الملك لله، والحمد لله ولا إله إلا الله وحده لا شريك له، له الملك وله الحمد، وهو على كل شيء قدير، أسألك خير ما في هذا اليوم، وخير ما بعده، وأعوذ بك من شر هذا اليوم، وشر ما بعده، وأعوذ بك من الكسل وسوء الكبر، وأعوذ بك من عذاب النار وعذاب القبر", + "@azkarList8": { + "description": "أصبحنا وأصبح الملك لله، والحمد لله ولا إله إلا الله وحده لا شريك له، له الملك وله الحمد، وهو على كل شيء قدير، أسألك خير ما في هذا اليوم، وخير ما بعده، وأعوذ بك من شر هذا اليوم، وشر ما بعده، وأعوذ بك من الكسل وسوء الكبر، وأعوذ بك من عذاب النار وعذاب القبر" + }, + "azkarList9": "اللَّهُمَّ إِنِّي أَصْبَحْتُ أُشْهِدُكَ، وَأُشْهِدُ حَمَلَةَ عَرْشِكَ، وَمَلاَئِكَتِكَ، وَجَمِيعَ خَلْقِكَ، أَنَّكَ أَنْتَ اللَّهُ لَا إِلَهَ إِلاَّ أَنْتَ وَحْدَكَ لاَ شَرِيكَ لَكَ، وَأَنَّ مُحَمَّداً عَبْدُكَ وَرَسُولُكَ |أربعَ مَرَّات|. [ وإذا أمسى قال: اللَّهم إني أمسيت...]", + "@azkarList9": { + "description": "اللَّهُمَّ إِنِّي أَصْبَحْتُ أُشْهِدُكَ، وَأُشْهِدُ حَمَلَةَ عَرْشِكَ، وَمَلاَئِكَتِكَ، وَجَمِيعَ خَلْقِكَ، أَنَّكَ أَنْتَ اللَّهُ لَا إِلَهَ إِلاَّ أَنْتَ وَحْدَكَ لاَ شَرِيكَ لَكَ، وَأَنَّ مُحَمَّداً عَبْدُكَ وَرَسُولُكَ |أربعَ مَرَّات|. [ وإذا أمسى قال: اللَّهم إني أمسيت...]" + }, + "azkarList10": "|اللَّهُمَّ عَافِنِي فِي بَدَنِي، اللَّهُمَّ عَافِنِي فِي سَمْعِي، اللَّهُمَّ عَافِنِي فِي بَصَرِي، لاَ إِلَهَ إِلاَّ أَنْتَ. اللَّهُمَّ إِنِّي أَعُوذُ بِكَ مِنَ الْكُفْرِ، وَالفَقْرِ، وَأَعُوذُ بِكَ مِنْ عَذَابِ القَبْرِ، لاَ إِلَهَ إِلاَّ أَنْتَ |ثلاثَ مرَّاتٍ", + "@azkarList10": { + "description": "|اللَّهُمَّ عَافِنِي فِي بَدَنِي، اللَّهُمَّ عَافِنِي فِي سَمْعِي، اللَّهُمَّ عَافِنِي فِي بَصَرِي، لاَ إِلَهَ إِلاَّ أَنْتَ. اللَّهُمَّ إِنِّي أَعُوذُ بِكَ مِنَ الْكُفْرِ، وَالفَقْرِ، وَأَعُوذُ بِكَ مِنْ عَذَابِ القَبْرِ، لاَ إِلَهَ إِلاَّ أَنْتَ |ثلاثَ مرَّاتٍ" + }, + "azkarList11": "|حَسْبِيَ اللَّهُ لاَ إِلَهَ إِلاَّ هُوَ عَلَيهِ تَوَكَّلتُ وَهُوَ رَبُّ الْعَرْشِ الْعَظِيمِ |سَبْعَ مَرّاتٍ", + "@azkarList11": { + "description": "|حَسْبِيَ اللَّهُ لاَ إِلَهَ إِلاَّ هُوَ عَلَيهِ تَوَكَّلتُ وَهُوَ رَبُّ الْعَرْشِ الْعَظِيمِ |سَبْعَ مَرّاتٍ" + }, + "azkarList12": "|لاَ إِلَهَ إِلاَّ اللَّهُ وَحْدَهُ لاَ شَرِيكَ لَهُ، لَهُ الْمُلْكُ وَلَهُ الْحَمْدُ، وَهُوَ عَلَى كُلِّ شَيْءٍ قَدِيرٌ |عشرَ مرَّاتٍ", + "@azkarList12": { + "description": "|رَضِيتُ بِاللَّهِ رَبَّاً، وَبِالْإِسْلاَمِ دِيناً، وَبِمُحَمَّدٍ صلى الله عليه وسلم نَبِيّاً |ثلاثَ مرَّاتٍ" + }, + "azkarList13": "|لاَ إِلَهَ إِلاَّ اللَّهُ وَحْدَهُ لاَ شَرِيكَ لَهُ، لَهُ الْمُلْكُ وَلَهُ الْحَمْدُ، وَهُوَ عَلَى كُلِّ شَيْءٍ قَدِيرٌ |عشرَ مرَّاتٍ", + "@azkarList13": { + "description": "|لاَ إِلَهَ إِلاَّ اللَّهُ وَحْدَهُ لاَ شَرِيكَ لَهُ، لَهُ الْمُلْكُ وَلَهُ الْحَمْدُ، وَهُوَ عَلَى كُلِّ شَيْءٍ قَدِيرٌ |عشرَ مرَّات" + }, "jumuaaScreenTitle": "وقت نماز جمعه ", "jumuaaHadith": "پیامبر اکرم صلی الله علیه و آله و سلم می فرمایند: «کسی که وضو را کامل بگیرد، سپس به جمعه برود و سپس گوش کند و سکوت کند، آنچه بین آن وقت تا جمعه بعد و سه روز دیگر و یک روز دیگر است آمرزیده می شود. کسی که دست به سنگ بزند، بیهوده است.»", "shuruk": "طلوع آفتاب", @@ -263,5 +291,34 @@ } }, "chooseQuranPage": "صفحه را انتخاب کنید", - "checkingForUpdates": "Checking for updates..." -} \ No newline at end of file + "checkingForUpdates": "Checking for updates...", + "chooseQuranType": "Choose quran", + "hafs": "Hafs", + "warsh": "Warsh", + "favorites": "Favorites", + "allReciters": "All Reciters", + "reciterAddedToFavorites": "Reciter {name} added to favorites", + "@reciterAddedToFavorites": { + "description": "Message shown when a reciter is added to favorites", + "placeholders": { + "name": { + "type": "String", + "example": "Abdul Basit" + } + } + }, + "reciterRemovedFromFavorites": "Reciter {name} removed from favorites", + "@reciterRemovedFromFavorites": { + "description": "Message shown when a reciter is removed from favorites", + "placeholders": { + "name": { + "type": "String", + "example": "Abdul Basit" + } + } + }, + "noFavoriteReciters": "No favorite reciters. Try adding one to the list", + "@noFavoriteReciters": { + "description": "Message shown when there are no favorite reciters" + } +} From e6da68e32094e9730bb9df56784a17aa6133661f Mon Sep 17 00:00:00 2001 From: Yassin Nouh <70436855+YassinNouh21@users.noreply.github.com> Date: Mon, 11 Nov 2024 21:55:03 +0200 Subject: [PATCH 07/26] feat: update and add new translations (#1414) --- lib/l10n/intl_sq.arb | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/lib/l10n/intl_sq.arb b/lib/l10n/intl_sq.arb index 1efb312ba..466c7b986 100644 --- a/lib/l10n/intl_sq.arb +++ b/lib/l10n/intl_sq.arb @@ -54,7 +54,7 @@ "maghrib": "Akshami ", "isha": "Jacia", "afterAdhanHadithTitle": "Duaja pas Ezanit", - "afterSalahHadith": "O Allahu im, Zot i kësaj thirrje të plotë dhe i namazit që do të falet, jepi Muhamedit ndërmjetësimin dhe nderimin, si dhe dërgoje atë në vendin e lavdishëm të cilin ia ke premtuar. Pa dyshim, Ti e mban premtimin e dhënë. Me të vërtetë Ti nuk e thyen premtimin", + "afterSalahHadith": "O Allahu im, Zot i kësaj thirrje të plotë dhe i namazit që do të falet, jepi Muhamedit ndërmjetësimin dhe nderimin, si dhe dërgoje atë në vendin e lavdishëm të cilin ia ke premtuar.", "alIqama": "Ikameti", "alAdhan": "Ezani", "turnOfPhones": "Ju lutemi, bërë telefonin të pazëshëm!", @@ -290,6 +290,20 @@ } } }, + "quranReadingPagePortrait": "Faqe {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": "Zgjidhni faqen", "checkingForUpdates": "Po kontrollon për përditësime...", "chooseQuranType": "Zgjidhni Kuranin", @@ -320,5 +334,23 @@ "noFavoriteReciters": "Nuk ka recitues të preferuar. Provoni të shtoni një në listë", "@noFavoriteReciters": { "description": "Message shown when there are no favorite reciters" - } + }, + "noReciterSearchResult": "Nuk u gjetën rezultate për kërkimin tuaj", + "searchForReciter": "Kërko recituesin", + "downloadAllSuwarSuccessfully": "Komplet Kur'ani është shkarkuar", + "noSuwarDownload": "Nuk ka Sure të reja për t'u shkarkuar", + "connectDownloadQuran": "Ju lutem lidheni internetin për të shkarkuar", + "playInOnlineModeQuran": "Për të dëgjuar lidhuni me internetin", + "downloaded": "E shkarkuar", + "switchQuranType": "Shkoni te {name}", + "@switchQuranType": { + "description": "Message shown when a reciter is added to favorites", + "placeholders": { + "name": { + "type": "String", + "example": "Warsh" + } + } + }, + "surahSelector": "Zgjidhni Suren" } \ No newline at end of file From c231971e8bd73158752759c69493d8b3eb899e54 Mon Sep 17 00:00:00 2001 From: Yassin Nouh <70436855+YassinNouh21@users.noreply.github.com> Date: Mon, 11 Nov 2024 21:56:11 +0200 Subject: [PATCH 08/26] feat: add thai language support (#1413) --- assets/img/flag/th.png | Bin 0 -> 15781 bytes lib/l10n/intl_th.arb | 324 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 324 insertions(+) create mode 100644 assets/img/flag/th.png create mode 100644 lib/l10n/intl_th.arb diff --git a/assets/img/flag/th.png b/assets/img/flag/th.png new file mode 100644 index 0000000000000000000000000000000000000000..c31c04d311c0777d4d7bf5b9e730ecae7b9baa31 GIT binary patch literal 15781 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 literal 0 HcmV?d00001 diff --git a/lib/l10n/intl_th.arb b/lib/l10n/intl_th.arb new file mode 100644 index 000000000..e8d8e20d0 --- /dev/null +++ b/lib/l10n/intl_th.arb @@ -0,0 +1,324 @@ +{ + "home": "หน้าหลัก", + "share": "แชร์", + "about": "เกี่ยวกับ", + "rate": "Rate Us", + "languages": "ภาษา", + "appLang": "ภาษาของแอป", + "descLang": "โปรดเลือกภาษาที่คุณต้องการ", + "hadithLangDesc": "ตัวเลือกของคุณในโซลผู้ดูแลระบบ คุณสามารถเลือกภาษาอื่นตามหน้าจอได้", + "whoops": "Whoops!", + "noInternet": "ไม่มีการเชื่อมต่ออินเทอร์เน็ต", + "tryAgain": "ลองอีกครั้ง", + "closeApp": "ปิดแอป", + "quit": "ออก", + "forceStaging": "เปลี่ยนไปที่", + "disableStaging": "สลับไปที่โหมดการผลิต", + "sureCloseApp": "คุณแน่ใจหรือไม่ว่าต้องการออกจากแอปพลิเคชัน", + "ok": "OK", + "cancel": "ยกเลิก", + "darkMode": "โหมดมืด", + "lightMode": "โหมดสว่าง", + "changeMosque": "เปลี่ยนมัสยิด", + "in1": "ใน", + "sec": "วินาที", + "online": "ออนไลน์", + "missingMosqueId": "ไม่มี MAWAQIT #ID หรือ MOSQUE #ID", + "mosqueIdIsNotValid": "ขออภัย {mosqueId} ไม่ใช่รหัสมัสยิดที่ถูกต้อง", + "selectMosqueId": "กรุณากรอกรหัสมัสยิดของคุณ", + "mawaqitWelcome": "ยินดีต้อนรับสู่ MAWAQIT", + "mawaqitDesc": "السلام عليكم และ بارك الله فيكم สำหรับการเลือก MAWAQIT เครือข่ายมัสยิดอัจฉริยะแห่งแรกและอันดับ 1 ของโลก ซึ่งใช้งานโดยชาวมุสลิมหลายล้านคนทั่วโลกใน 85+ ประเทศตั้งแต่ปี 2559 เรามอบจอแสดงผลมัสยิด อัจฉริยะที่ทันสมัยที่สุด ให้กับคุณ ซึ่งพร้อมใช้งานบนอุปกรณ์หลายเครื่อง (มือถือ สมาร์ทวอทช์ หน้าจอทีวี) โดยไม่ต้องรวบรวมหรือแชร์ข้อมูลส่วนบุคคลของคุณ โปรดสนับสนุนโครงการที่ดีที่นี่: https://donate.mawaqit.net เราเป็นองค์กรไม่แสวงผลกำไร และโครงการนี้คือ “Waqf fi'sabili Allah” (การบริจาคเพื่ออัลลอฮฺ) การบริจาคของคุณทำให้ทุกคนสามารถเข้าถึงโครงการนี้ได้ทุกที่ ไม่มีค่าใช้จ่ายใด ๆ ทั้งสิ้น โดยไม่มีโฆษณา และไม่มีการสมัครสมาชิกรายเดือน โครงการนี้จะเป็นไปไม่ได้หากไม่ได้รับความช่วยเหลือจากอัลลอฮ์ที่รวบรวมชุมชนอาสาสมัครที่มีความสามารถและกระตือรือร้นมารวมตัวกัน ทำงานทั้งวันทั้งคืนเพื่อให้บริการที่ดีที่สุดแก่คุณ และสถานะของระบบสุดท้ายที่พร้อมให้บริการทุกวันตลอด 24 ชั่วโมง โปรดพิจารณาบริจาคเพื่อให้โครงการอันศักดิ์สิทธิ์นี้ดำเนินต่อไป Baraka'Allah fikom สำหรับความไว้วางใจและการสนับสนุนอย่างต่อเนื่องของคุณ", + "privacyPolicy": "นโยบายความเป็นส่วนตัว", + "termsOfService": "ข้อกำหนดในการให้บริการ", + "installationGuide": "คู่มือการติดตั้ง", + "drawerTitle": "MAWAQIT", + "drawerDesc": "การเชื่อมโยงชาวมุสลิมเข้ากับมัสยิด", + "backendError": "ขออภัย เราไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ได้ โปรดตรวจสอบการเชื่อมต่ออินเทอร์เน็ตหรือลองอีกครั้งในภายหลัง", + "selectWithMosqueId": "ลอง: 256 เป็นรหัสของ 'Grande Mosquée de Paris'", + "searchForMosque": "มัสยิดไหนที่คุณกำลังมองหา? (ID, ชื่อ, เมือง, รหัสไปรษณีย์...)", + "searchMosque": "ค้นหามัสยิด", + "mosqueNameError": "ใส่ชื่อมัสยิด", + "slugError": "ไม่ใช่มัสยิดที่ถูกต้อง", + "doYouKnowMosqueId": "คุณรู้รหัสการติดตั้งหรือรหัสมัสยิดของคุณหรือไม่?", + "yes": "ใช่", + "no": "ไม่ใช่", + "networkStatus": "สถานะเครือข่าย", + "mosqueNoMore": "ไม่มีผลลัพธ์อีกต่อไป", + "mosqueNoResults": "ไม่มีผลลัพธ์", + "offline": "ออฟไลน์", + "imsak": "อิมซาก الإمساك", + "jumua": "ยุมอะฮฺ الجمعة", + "duhr": "ซุหริ الظهر", + "fajr": "ฟัจญ์รฺ الفجر", + "asr": "อัศรี العصر", + "maghrib": "มัฆริบ المغرب", + "isha": "อีซา العشاء", + "afterAdhanHadithTitle": "ดูอาหลังอาซาน", + "afterSalahHadith": "Allahumma Rabba hadhihid-da'wati-ttammati, was-salatil-qa'imati, ati Muhammadanil-wasilata wal-fadhilata, wab'athu maqaman mahmuda nilladhi wa 'adtahu [O Allah, Rubb of this perfect call (Da'wah) and of the established prayer (As-Salat), grant Muhammad the Wasilah and superiority, and raise him up to a praiseworthy position which You have promised him]", + "alIqama": "อิกอมะฮฺ الإقامة", + "alAdhan": "อาซาน الأذان", + "turnOfPhones": "กรุณาปิดมือถือ หรือเข้าสู่โหมดปิดเสียง ขณะละหมาด", + "iqamaIn": "เวลาอีกอมะฮฺ", + "alAthkar": "อัล-อัซการ", + "azkarList0": "Astaghfiru Allah, Astaghfiru Allah, Astaghfiru Allah Allahumma anta Essalam wa mineka Essalam, tabarakta ya dhal djalali wel ikram Allahumma A`inni `ala dhikrika wa chukrika wa husni `ibadatik", + "@azkarList0": { + "description": "أَسْـتَغْفِرُ الله، أَسْـتَغْفِرُ الله، أَسْـتَغْفِرُ الله اللّهُـمَّ أَنْـتَ السَّلامُ ، وَمِـنْكَ السَّلام ، تَبارَكْتَ يا ذا الجَـلالِ وَالإِكْـرام اللَّهُمَّ أَعِنِّي عَلَى ذِكْرِكَ وَشُكْرِكَ وَحُسْنِ عِبَادَتِكَ" + }, + "azkarList1": "Subhan Allah wal hamdu lillah wallahu akbar (33 ครั้ง) La ilaha illa Allah, wahdahu la charika lah, lahu elmoulku wa lahu elhamdu, wa hua `ala kulli chay in kadir", + "@azkarList1": { + "description": "سُـبْحانَ اللهِ، والحَمْـدُ لله، واللهُ أكْـبَر 33 مرة لا إِلَٰهَ إلاّ اللّهُ وَحْـدَهُ لا شريكَ لهُ، لهُ الملكُ ولهُ الحَمْد، وهُوَ على كُلّ شَيءٍ قَـدير" + }, + "azkarList2": "", + "@azkarList2": { + "description": "بِسۡمِ ٱللَّهِ ٱلرَّحۡمَٰنِ ٱلرَّحِيمِ قُلۡ أَعُوذُ بِرَبِّ ٱلنَّاسِ ، مَلِكِ ٱلنَّاسِ ، إِلَٰهِ ٱلنَّاسِ ، مِن شَرِّ ٱلۡوَسۡوَاسِ ٱلۡخَنَّاسِ ، ٱلَّذِي يُوَسۡوِسُ فِي صُدُورِ ٱلنَّاسِ ، مِنَ ٱلۡجِنَّةِ وَٱلنَّاس" + }, + "azkarList3": "", + "@azkarList3": { + "description": "بِسۡمِ ٱللَّهِ ٱلرَّحۡمَٰنِ ٱلرَّحِيمِقُلۡ أَعُوذُ بِرَبِّ ٱلۡفَلَقِ ، مِن شَرِّ مَا خَلَقَ ، وَمِن شَرِّ غَاسِقٍ إِذَا وَقَبَ ، وَمِن شَرِ ٱلنَّفَّٰثَٰتِ فِي ٱلۡعُقَدِ ، وَمِن شَرِّ حَاسِدٍ إِذَا حَسَدَ" + }, + "azkarList4": "", + "@azkarList4": { + "description": "بِسۡمِ ٱللَّهِ ٱلرَّحۡمَٰنِ ٱلرَّحِيمِ قُلۡ هُوَ ٱللَّهُ أَحَدٌ ، ٱللَّهُ ٱلصَّمَدُ ، لَمۡ يَلِدۡ وَلَمۡ يُولَدۡ ، وَلَمۡ يَكُن لَّهُۥ كُفُوًا أَحَدُۢ" + }, + "azkarList5": "", + "@azkarList5": { + "description": "ٱللَّهُ لَآ إِلَٰهَ إِلَّا هُوَ ٱلۡحَيُّ ٱلۡقَيُّومُۚ لَا تَأۡخُذُهُۥ سِنَةٞ وَلَا نَوۡمٞۚ لَّهُۥ مَا فِي ٱلسَّمَٰوَٰتِ وَمَا فِي ٱلۡأَرۡضِۗ مَن ذَا ٱلَّذِي يَشۡفَعُ عِندَهُۥٓ إِلَّا بِإِذۡنِهِۦۚ يَعۡلَمُ مَا بَيۡنَ أَيۡدِيهِمۡ وَمَا خَلۡفَهُمۡۖ وَلَا يُحِيطُونَ بِشَيۡءٖ مِّنۡ عِلۡمِهِۦٓ إِلَّا بِمَا شَآءَۚ وَسِعَ كُرۡسِيُّهُ ٱلسَّمَٰوَٰتِ وَٱلۡأَرۡضَۖ وَلَا يَ‍ُٔودُهُۥ حِفۡظُهُمَاۚ وَهُوَ ٱلۡعَلِيُّ ٱلۡعَظِيمُ" + }, + "azkarList6": "La ilaha illa Allah, wahdahu la charika lah, lahu elmulku wa lahu elhamdu, wa hua `ala koulli chayin kadir, Allahumma la mani`a lima a`atayte, wa la mu`atia lima `ate, wa la yanefa`u dhal djaddi mineka eldjad", + "@azkarList6": { + "description": "لا إِلَٰهَ إلاّ اللّهُ وحدَهُ لا شريكَ لهُ، لهُ المُـلْكُ ولهُ الحَمْد، وهوَ على كلّ شَيءٍ قَدير، اللّهُـمَّ لا مانِعَ لِما أَعْطَـيْت، وَلا مُعْطِـيَ لِما مَنَـعْت، وَلا يَنْفَـعُ ذا الجَـدِّ مِنْـكَ الجَـد" + }, + "azkarList7": "اللهم أنت ربي، لا إله إلا أنت، خلقتني وأنا عبدُك, وأنا على عهدِك ووعدِك ما استطعتُ، أعوذ بك من شر ما صنعتُ، أبوءُ لَكَ بنعمتكَ عَلَيَّ، وأبوء بذنبي، فاغفر لي، فإنه لا يغفرُ الذنوب إلا أنت", + "@azkarList7": { + "description": "اللهم أنت ربي، لا إله إلا أنت، خلقتني وأنا عبدُك, وأنا على عهدِك ووعدِك ما استطعتُ، أعوذ بك من شر ما صنعتُ، أبوءُ لَكَ بنعمتكَ عَلَيَّ، وأبوء بذنبي، فاغفر لي، فإنه لا يغفرُ الذنوب إلا أنت" + }, + "azkarList8": "أصبحنا وأصبح الملك لله، والحمد لله ولا إله إلا الله وحده لا شريك له، له الملك وله الحمد، وهو على كل شيء قدير، أسألك خير ما في هذا اليوم، وخير ما بعده، وأعوذ بك من شر هذا اليوم، وشر ما بعده، وأعوذ بك من الكسل وسوء الكبر، وأعوذ بك من عذاب النار وعذاب القبر", + "@azkarList8": { + "description": "أصبحنا وأصبح الملك لله، والحمد لله ولا إله إلا الله وحده لا شريك له، له الملك وله الحمد، وهو على كل شيء قدير، أسألك خير ما في هذا اليوم، وخير ما بعده، وأعوذ بك من شر هذا اليوم، وشر ما بعده، وأعوذ بك من الكسل وسوء الكبر، وأعوذ بك من عذاب النار وعذاب القبر" + }, + "azkarList9": "اللَّهُمَّ إِنِّي أَصْبَحْتُ أُشْهِدُكَ، وَأُشْهِدُ حَمَلَةَ عَرْشِكَ، وَمَلاَئِكَتِكَ، وَجَمِيعَ خَلْقِكَ، أَنَّكَ أَنْتَ اللَّهُ لَا إِلَهَ إِلاَّ أَنْتَ وَحْدَكَ لاَ شَرِيكَ لَكَ، وَأَنَّ مُحَمَّداً عَبْدُكَ وَرَسُولُكَ |أربعَ مَرَّات|. [ وإذا أمسى قال: اللَّهم إني أمسيت...]", + "@azkarList9": { + "description": "اللَّهُمَّ إِنِّي أَصْبَحْتُ أُشْهِدُكَ، وَأُشْهِدُ حَمَلَةَ عَرْشِكَ، وَمَلاَئِكَتِكَ، وَجَمِيعَ خَلْقِكَ، أَنَّكَ أَنْتَ اللَّهُ لَا إِلَهَ إِلاَّ أَنْتَ وَحْدَكَ لاَ شَرِيكَ لَكَ، وَأَنَّ مُحَمَّداً عَبْدُكَ وَرَسُولُكَ |أربعَ مَرَّات|. [ وإذا أمسى قال: اللَّهم إني أمسيت...]" + }, + "azkarList10": "|اللَّهُمَّ عَافِنِي فِي بَدَنِي، اللَّهُمَّ عَافِنِي فِي سَمْعِي، اللَّهُمَّ عَافِنِي فِي بَصَرِي، لاَ إِلَهَ إِلاَّ أَنْتَ. اللَّهُمَّ إِنِّي أَعُوذُ بِكَ مِنَ الْكُفْرِ، وَالفَقْرِ، وَأَعُوذُ بِكَ مِنْ عَذَابِ القَبْرِ، لاَ إِلَهَ إِلاَّ أَنْتَ |ثلاثَ مرَّاتٍ", + "@azkarList10": { + "description": "|اللَّهُمَّ عَافِنِي فِي بَدَنِي، اللَّهُمَّ عَافِنِي فِي سَمْعِي، اللَّهُمَّ عَافِنِي فِي بَصَرِي، لاَ إِلَهَ إِلاَّ أَنْتَ. اللَّهُمَّ إِنِّي أَعُوذُ بِكَ مِنَ الْكُفْرِ، وَالفَقْرِ، وَأَعُوذُ بِكَ مِنْ عَذَابِ القَبْرِ، لاَ إِلَهَ إِلاَّ أَنْتَ |ثلاثَ مرَّاتٍ" + }, + "azkarList11": "|حَسْبِيَ اللَّهُ لاَ إِلَهَ إِلاَّ هُوَ عَلَيهِ تَوَكَّلتُ وَهُوَ رَبُّ الْعَرْشِ الْعَظِيمِ |سَبْعَ مَرّاتٍ", + "@azkarList11": { + "description": "|حَسْبِيَ اللَّهُ لاَ إِلَهَ إِلاَّ هُوَ عَلَيهِ تَوَكَّلتُ وَهُوَ رَبُّ الْعَرْشِ الْعَظِيمِ |سَبْعَ مَرّاتٍ" + }, + "azkarList12": "|رَضِيتُ بِاللَّهِ رَبَّاً، وَبِالْإِسْلاَمِ دِيناً، وَبِمُحَمَّدٍ صلى الله عليه وسلم نَبِيّاً |ثلاثَ مرَّاتٍ", + "@azkarList12": { + "description": "|رَضِيتُ بِاللَّهِ رَبَّاً، وَبِالْإِسْلاَمِ دِيناً، وَبِمُحَمَّدٍ صلى الله عليه وسلم نَبِيّاً |ثلاثَ مرَّاتٍ" + }, + "azkarList13": "|لاَ إِلَهَ إِلاَّ اللَّهُ وَحْدَهُ لاَ شَرِيكَ لَهُ، لَهُ الْمُلْكُ وَلَهُ الْحَمْدُ، وَهُوَ عَلَى كُلِّ شَيْءٍ قَدِيرٌ |عشرَ مرَّاتٍ", + "@azkarList13": { + "description": "|لاَ إِلَهَ إِلاَّ اللَّهُ وَحْدَهُ لاَ شَرِيكَ لَهُ، لَهُ الْمُلْكُ وَلَهُ الْحَمْدُ، وَهُوَ عَلَى كُلِّ شَيْءٍ قَدِيرٌ |عشرَ مرَّات" + }, + "jumuaaScreenTitle": "เวลายุมอะฮฺ", + "jumuaaHadith": "The Prophet ﷺ (peace and blessings of Allah be upon him) said “Whoever does the ablutions perfectly then goes to jumua and then listens and is silent, he is forgiven what is between that time and the following Friday and three more days and the one who touches stones has certainly made a futility”", + "shuruk": "ซุรุก الشروق", + "reset": "รีเซ็ต", + "mosqueNotFoundMessage": "ขออภัย ไม่พบมัสยิดของคุณ ไม่เช่นนั้นมัสยิดอาจหายไปหรือปิดใช้งานชั่วคราว", + "noInternetMessage": "ไม่มีอินเทอร์เน็ต โปรดตรวจสอบการเชื่อมต่ออินเทอร์เน็ตของคุณแล้วลองอีกครั้ง เชื่อมต่อ Wi-Fi หรือ Ethernet ของคุณแล้วหรือยัง?", + "error": "ข้อผิดพลาด", + "mosqueErrorMessage": "ข้อผิดพลาดของมัสยิด หากคุณเป็นผู้ดูแลมัสยิด โปรดติดต่อฝ่ายดูแลของเราเพื่อแก้ไขปัญหานี้", + "muharram": "มูฮัรรอม", + "safar": "ซอฟัร", + "rabiAlawwal": "รอบีอุลเอาวัล", + "rabiAlthani": "รอบีอุลซานีย์", + "jumadaAlula": "ญุมาดิลอูลา", + "jumadaAlakhirah": "ญุมาดิลอาคิร", + "rajab": "รอญับ", + "shaban": "ชะบาน", + "ramadan": "Screen on/off modeรอมฎอน", + "shawwal": "เชาวาล", + "dhuAlqidah": "ซุลกออีดะฮฺ", + "dhuAlhijjah": "ซุลฮิจเญาะฮฺ", + "duaaBetweenSalahAndAdhan": "Anas bin Malik said: The Messenger of Allah ﷺ said: The supplication does not return between the call to prayer and the standing for prayer.", + "salatKhayrMinaNawm": "การละหมาดดีกว่าการนอน", + "salatElEid": "ละหมาดอีด", + "webView": "เปิดใช้งานโหมดดั้งเดิม", + "developersHomeScreen": "หน้าจอหลักของผู้ดูแลระบบ", + "onlineHome": "หน้าแรกออนไลน์", + "prayerTimes": "เวลาละหมาด", + "alerts": "เตือน", + "iqamaaCountDown": "นับถอยหลังเวลาอิกอมะฮฺ", + "afterAdhanHadith": "ฮาดีษหลังอาซาน", + "afterSalahAzkar": "อัซการหลังละหมาด", + "iqama": "อิกอมะฮฺ", + "randomHadith": "ฮาดีษรอมฎอน", + "announcement": "ประกาศ", + "jumuaaLive": "ไลฟ์ ละหมาดญุมอะฮฺ", + "showSecondaryScreen": "ใช้เป็นหน้าจอรอง (สำหรับประกาศ)", + "normalScreen": "ใช้เป็นหน้าจอหลัก", + "duaaRemainder": "ดุอาอฺที่เหลือ", + "fajrWakeUp": "ตื่นละหมาดฟัจญรฺ", + "changeLanguage": "เปลี่ยนภาษา", + "forceScreen": "หน้าจอบังคับ", + "clear": "Clear", + "changeTheme": "เปลี่ยนธีม", + "next": "Next", + "mainScreenOrSecondaryScreen": "ตำแหน่งหน้าจอ", + "mainScreenOrSecondaryScreenEXPLINATION": "คุณต้องการติดตั้งหน้าจอนี้ในห้องละหมาดหลัก (ห้องละหมาดชาย) หรือไม่?", + "mainScreen": "หน้าจอหลัก", + "secondaryScreen": "Secondary screen", + "duaaElEftar": "ดูอาเปิดบวช", + "announcementOnlyMode": "โหมดประกาศ", + "normalMode": "Normal mode ", + "announcementOnlyModeEXPLINATION": "เลือกว่าหน้าจอของคุณจะแสดงประกาศตลอดเวลาหรือไม่ ซึ่งจะมีประโยชน์หากคุณติดตั้งหน้าจอที่ทางเข้า เป็นต้น", + "duaaElEftarText": "", + "@duaaElEftarText": { + "description": "اللهم اني لگ صمت وعلى رزقك افطرت واليك انبت وعليگ توكلت ذهب الظما وابتلت العروق وثبت الاجر انشاء الله" + }, + "secondaryScreenExplanation": "สำหรับห้องละหมาดรอง (เช่น ห้องสตรีหรือชั้นอื่นๆ) หน้าจอนี้จะแสดงการสตรีมสดของ Jumua", + "mainScreenExplanation": "สำหรับห้องละหมาดหลัก หน้าจอนี้จะไม่แสดงการถ่ายทอดสด Jumua", + "normalModeExplanation": "จะแสดงหน้าจอปกติพร้อมเวลาละหมาดและประกาศ", + "announcementOnlyModeExplanation": "จะแสดงประกาศตลอดเวลา", + "orientation": "ปฐมนิเทศ", + "selectYourMawaqitTvAppOrientation": "เลือกการวางแนวแอปทีวี mawaqit ของคุณ", + "deviceDefault": "กำหนดค่าการเปิด/ปิดหน้าจอค่าเริ่มต้นของอุปกรณ์", + "deviceDefaultBTNDescription": "Mawaqit จะเลือกการวางแนวเริ่มต้นโดยอัตโนมัติตามการวางแนวหน้าจอ", + "portrait": "ภาพเหมือน", + "portraitBTNDescription": "สำหรับการวางแนวตั้ง แนะนำสำหรับมัสยิดที่มีพื้นที่ขนาดเล็ก", + "landscape": "ภูมิประเทศ", + "landscapeBTNDescription": "สำหรับการวางแนวในแนวนอน รูปแบบหลักสำหรับแอปทีวี mawaqit และรูปแบบที่แนะนำสำหรับมัสยิดส่วนใหญ่", + "eidMubarak": "อีดมูบาร๊อก", + "takbeerAleidText": "Allahu Akbar, Allahu Akbar, Allahu Akbar, la ilaha illa Allah, Allahu Akbar, Allahu Akbar, wa lillahi al-hamd", + "settings": "ตั้งค่า", + "applicationModes": "โหมดการใช้งาน", + "ifYouAreFacingAnIssueWithTheAppActivateThis": "หากคุณประสบปัญหากับแอป ให้ลองเปิดใช้งานตัวเลือกนี้", + "hijriAdjustments": "การปรับเปลี่ยนฮิจเราะห์ในท้องถิ่น", + "hijriAdjustmentsDescription": "ปรับวันที่ฮิจเราะห์ในอุปกรณ์ของคุณ สิ่งนี้จะไม่ส่งผลกระทบต่อการตั้งค่ามัสยิดออนไลน์", + "backoffice_default": "ค่าเริ่มต้นของ Backoffice", + "recommended": "ที่แนะนำ", + "sabah": "Sabah", + "randomHadithLanguage": "ภาษาฮาดีษรอมฎอน", + "en": "ภาษาอังกฤษ", + "fr": "ภาษาฝรั่งเศษ", + "ar": "ภาษาอาหรับ", + "tr": "ภาษาตุรกี", + "de": "ภาษาเยรมัน", + "es": "ภาษาสเปน", + "pt": "ภาษาโปรตุเกตุ", + "nl": "ภาษาดัช", + "fr_ar": "ภาษาฝรั่งเศษ อาหรับ", + "en_ar": "ภาษาอังกฤษ อาหรับ", + "de_ar": "ภาษาเยรมัน อาหรับ", + "ta_ar": "ภาษาตามิล อาหรับ", + "tr_ar": "ภาษาตุรกี อาหรับ", + "es_ar": "ภาษาสเปน อาหรับ", + "pt_ar": "ภาษาโปตุเกตุ อาหรับ", + "nl_ar": "ภาษาดัช อาหรับ", + "connectToChangeHadith": "โปรดเชื่อมต่ออินเทอร์เน็ตเพื่อเปลี่ยนภาษาฮาดีษ", + "retry": "ลองอีกครั้ง", + "timeSetting": "การกำหนดค่าเวลา", + "timeSettingDesc": "ตั้งชื่อที่กำหนดเอง", + "selectedTime": "เวลาที่เลือกในปัจจุบัน", + "confirmation": "การยืนยัน", + "confirmationMessage": "คุณแน่ใจหรือไม่ว่าต้องการใช้เวลาในอุปกรณ์", + "useDeviceTime": "ใช้เวลาของอุปกรณ์", + "selectTime": "เลือก เวลา", + "previous": "ก่อนหน้า", + "appTimezone": "เขตเวลาของแอป", + "descTimezone": "เลือกเขตเวลาของคุณเพื่อรับเวลาละหมาดที่แม่นยำ", + "appWifi": "เชื่อมต่อกับไวไฟ", + "descWifi": "กรุณาเชื่อมต่อกับ wifi ที่คุณต้องการ", + "searchCountries": "ค้นหาประเทศ", + "scanAgain": "สแกนอีกครั้ง", + "noScannedResultsFound": "ไม่พบจุดเชื่อมต่อใกล้", + "connect": "เชื่อมต่อ", + "wifiPassword": "รหัสผ่าน", + "skip": "ข้าม", + "noSSID": "**SSID ที่ซ่อนอยู่**", + "close": "ปิด", + "search": "ค้นหา", + "wifiSuccess": "เชื่อมต่อกับ Wifi สำเร็จแล้ว", + "wifiFailure": "ไม่สามารถเชื่อมต่อกับ Wifi", + "timezoneSuccess": "ตั้งค่าเขตเวลาเรียบร้อยแล้ว", + "timezoneFailure": "ไม่สามารถตั้งค่าเขตเวลาได้", + "screenLock": "เปิด/ปิดหน้าจอ", + "screenLockConfig": "กำหนดค่าการเปิด/ปิดหน้าจอ", + "screenLockMode": "โหมดเปิด/ปิดหน้าจอ", + "screenLockDesc": "เปิด/ปิดทีวีก่อนและหลังละหมาดแต่ละครั้งเพื่อประหยัดพลังงาน", + "screenLockDesc2": "คุณสมบัตินี้เปิด/ปิดอุปกรณ์ก่อนและหลังการละหมาดอาซานแต่ละครั้ง", + "before": "นาทีก่อนเวลาละหมาดแต่ละครั้ง", + "after": "นาทีหลังเวลาละหมาดแต่ละครั้ง", + "updateAvailable": "อัปเดตพร้อมใช้งาน", + "seeMore": "ดูเพิ่มเติม", + "whatIsNew": "มีอะไรใหม่", + "update": "อัปเดต", + "automaticUpdate": "แจ้งเตื่อนการอัพเดต", + "automaticUpdateDescription": "", + "checkInternetLegacyMode": "คุณต้องเชื่อมต่ออินเทอร์เน็ตเพื่อใช้โหมดเดิม", + "powerOnScreen": "เปิดเครื่องบนหน้าจอ", + "powerOffScreen": "ปิดเครื่องบนหน้าจอ", + "deviceSettings": "ตั้งค่าอุปกรณ์", + "later": "ภายหลัง", + "downloadQuran": "ดาวน์โหลดอัลกุรอาน", + "quran": "อัลกุรอาน", + "askDownloadQuran": "คุณต้องการดาวน์โหลดอัลกุรอาน?", + "download": "ดาวน์โหลด", + "downloadingQuran": "กำลังดาวน์โหลดอัลกุรอาน", + "extractingQuran": "Extracting Quran", + "updatedQuran": "อัลกุรอานอัปเดต", + "quranLatestVersion": "อัลกุรอานข้อมูลล่าสุด", + "quranUpdatedVersion": "เวอร์ชันที่อัปเดตของอัลกุรอานคือ: {version}", + "quranIsUpdated": "เลือกหน้า", + "quranDownloaded": "ดาวน์โหลดคัมภีร์อัลกุรอานแล้ว", + "quranIsAlreadyDownloaded": "คัมภีร์อัลกุรอานถูกดาวน์โหลดแล้ว", + "chooseReciter": "เลือกผู้อ่าน", + "reciteType": "ประเภทท่อง", + "readingMode": "อ่านอัลกุรอาน", + "listeningMode": "ฟังกุรอาน", + "quranReadingPage": "หน้า{leftPage}{rightPage}{totalPages}", + "@quranReadingPage": { + "description": "Placeholder text for displaying Quran reading page numbers", + "placeholders": { + "leftPage": { + "type": "int", + "example": "1" + }, + "rightPage": { + "type": "int", + "example": "2" + }, + "totalPages": { + "type": "int", + "example": "604" + } + } + }, + "chooseQuranPage": "เลือกหน้า", + "checkingForUpdates": "กำลังตรวจสอบการอัปเดต...", + "chooseQuranType": "เลือกอัลกุรอาน", + "hafs": "กีรออาตฮัฟซ์", + "warsh": "กีรออาตวารัช", + "favorites": "รายการโปรด", + "allReciters": "ผู้อ่าน", + "reciterAddedToFavorites": "เพิ่มผู้อ่าน {name} ในรายการโปรดแล้ว", + "@reciterAddedToFavorites": { + "description": "Message shown when a reciter is added to favorites", + "placeholders": { + "name": { + "type": "String", + "example": "Abdul Basit" + } + } + }, + "reciterRemovedFromFavorites": "ผู้อ่าน {name} ถูกลบออกจากรายการโปรด", + "@reciterRemovedFromFavorites": { + "description": "Message shown when a reciter is removed from favorites", + "placeholders": { + "name": { + "type": "String", + "example": "Abdul Basit" + } + } + }, + "noFavoriteReciters": "ไม่มีผู้อ่านที่ชื่นชอบ ลองเพิ่มรายการหนึ่งรายการ", + "@noFavoriteReciters": { + "description": "Message shown when there are no favorite reciters" + } +} \ No newline at end of file From b03c24ac73bc6fc46a2a0afa8d0da1fd413f1019 Mon Sep 17 00:00:00 2001 From: Yassin Nouh <70436855+YassinNouh21@users.noreply.github.com> Date: Wed, 20 Nov 2024 15:21:33 +0200 Subject: [PATCH 09/26] Release 1.17.0 (#1417) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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) * 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> * 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 * Feat/Manual update rooted & non-rooted devices (#1427) * invert parentheses (#1406) * feat: add Ukrainian language support (#1416) * feat: add and update more persian language (#1415) * feat: update and add new translations (#1414) * feat: add thai language support (#1413) * test apk * add launching the app after install * add logs on launch * try to launch app after uninstall * create sript to launch on different process * modify execute command to correctly handle result * fix return statement * try different method * remove se linux permission * add more delay * modify script to retry * new launch script' * edit script launch * refactor update notifier to ManualUpdateNotifier for improved clarity and distinction - renamed `UpdateNotifier` to `ManualUpdateNotifier` for better differentiation of manual update functionality. - updated `updateNotifierProvider` to `manualUpdateNotifierProvider` across relevant files. - refactored `SettingScreen` to utilize the newly renamed `manualUpdateNotifierProvider`. * change script * fix missing imports * change install method * working install * higher priority receiver * add service * try bash script * try both commands * stable working update * feat: add for non-rooted update * stable non-rooted & rooted devices update * remove unused reciver & upgrade pubspec --------- Co-authored-by: Yassin Nouh <70436855+YassinNouh21@users.noreply.github.com> Co-authored-by: Yassin --------- Co-authored-by: Ghassen Ben Zahra Co-authored-by: Ibrahim ZEHHAF <97339607+ibrahim-zehhaf-mawaqit@users.noreply.github.com> --- android/app/src/main/AndroidManifest.xml | 1 + .../main/kotlin/com/flyweb/MainActivity.kt | 42 +- lib/l10n/intl_ar.arb | 14 +- lib/l10n/intl_bs.arb | 36 +- lib/l10n/intl_en.arb | 15 +- lib/l10n/intl_fr.arb | 14 +- lib/main.dart | 76 +- lib/src/const/constants.dart | 5 + lib/src/pages/SettingScreen.dart | 66 +- .../home/sub_screens/AdhanSubScreen.dart | 14 +- .../pages/home/sub_screens/JummuaLive.dart | 6 + .../pages/home/workflow/salah_workflow.dart | 13 +- .../page/quran_mode_selection_screen.dart | 55 +- .../pages/quran/page/quran_player_screen.dart | 36 +- .../quran/page/quran_reading_screen.dart | 393 ---------- .../quran/page/reciter_selection_screen.dart | 19 +- .../quran/page/surah_selection_screen.dart | 22 +- .../quran/reading/quran_reading_screen.dart | 730 ++++++++++++++++++ .../widget/quran_floating_action_buttons.dart | 369 +++++++++ .../pages/quran/widget/quran_background.dart | 15 +- .../widget/reading/quran_reading_widgets.dart | 475 +++++++----- .../widget/reading/quran_surah_selector.dart | 223 ++++-- .../pages/quran/widget/reciter_list_view.dart | 13 +- .../normal_home/landscape_normal_home.dart | 4 +- .../normal_home/portrait_normal_home.dart | 5 +- .../turkish_home/landscape_turkish_home.dart | 5 +- .../turkish_home/portrait_turkish_home.dart | 5 +- lib/src/routes/route_generator.dart | 100 +++ lib/src/routes/routes_constant.dart | 15 + .../mixins/mosque_helpers_mixins.dart | 14 + .../manual_update_notifier.dart | 256 ++++++ .../manual_update_state.dart | 59 ++ .../quran/quran/quran_notifier.dart | 8 + .../auto_reading/auto_reading_notifier.dart | 201 +++++ .../auto_reading/auto_reading_state.dart | 59 ++ .../quran/reading/quran_reading_notifer.dart | 8 + .../quran/reading/quran_reading_state.dart | 8 +- lib/src/widgets/MawaqitDrawer.dart | 14 +- lib/src/widgets/manual_update_dialog.dart | 114 +++ pubspec.yaml | 4 +- 40 files changed, 2719 insertions(+), 812 deletions(-) create mode 100644 lib/src/pages/quran/reading/quran_reading_screen.dart create mode 100644 lib/src/pages/quran/reading/widget/quran_floating_action_buttons.dart create mode 100644 lib/src/routes/route_generator.dart create mode 100644 lib/src/routes/routes_constant.dart create mode 100644 lib/src/state_management/manual_app_update/manual_update_notifier.dart create mode 100644 lib/src/state_management/manual_app_update/manual_update_state.dart create mode 100644 lib/src/state_management/quran/reading/auto_reading/auto_reading_notifier.dart create mode 100644 lib/src/state_management/quran/reading/auto_reading/auto_reading_state.dart create mode 100644 lib/src/widgets/manual_update_dialog.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 7864bcb0c..d2cc6533f 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -73,6 +73,7 @@ + { + val filePath = call.argument("filePath") + if (filePath != null) { + AsyncTask.execute { + try { + // Check if file exists + val file = java.io.File(filePath) + if (!file.exists()) { + Log.e("APK_INSTALL", "APK file not found at path: $filePath") + result.error("FILE_NOT_FOUND", "APK file not found", null) + return@execute + } + // Check if device is rooted + if (!checkRoot()) { + Log.e("APK_INSTALL", "Device is not rooted") + result.error("NOT_ROOTED", "Device is not rooted", null) + return@execute + } + + - else -> result.notImplemented() + executeCommand(listOf("pm install -r -d $filePath"), result) + + result.success("Installation initiated") + } catch (e: Exception) { + Log.e("APK_INSTALL", "Failed to install APK", e) + result.error("INSTALL_FAILED", e.message, null) + } + } + } else { + result.error("INVALID_PATH", "File path is null", null) + } +} + else -> result.notImplemented() } } } @@ -127,6 +163,7 @@ class MainActivity : FlutterActivity() { } } + private fun connectToWifi(call: MethodCall, result: MethodChannel.Result) { AsyncTask.execute { try { @@ -251,6 +288,7 @@ fun connectToNetworkWPA(call: MethodCall, result: MethodChannel.Result) { } } } + private fun sendDownArrowEvent(call: MethodCall, result: MethodChannel.Result) { AsyncTask.execute { try { diff --git a/lib/l10n/intl_ar.arb b/lib/l10n/intl_ar.arb index f55a102d1..5c8460326 100644 --- a/lib/l10n/intl_ar.arb +++ b/lib/l10n/intl_ar.arb @@ -352,5 +352,17 @@ "example": "604" } } - } + }, + "checkForUpdates": "التحقق من التحديثات", + "checkForNewVersion": "تحقق مما إذا كانت هناك نسخة جديدة متوفرة", + "wouldYouLikeToUpdate": "هل ترغب في تحديث التطبيق؟", + "updateCompleted": "تم التحديث بنجاح!", + "noUpdates": "لا توجد تحديثات", + "usingLatestVersion": "أنت تستخدم أحدث إصدار.", + "updateCancelled": "تم إلغاء التحديث", + "checkingUpdates": "جارٍ التحقق من التحديثات...", + "downloadingUpdate": "جارٍ تنزيل التحديث...", + "installingUpdate": "جارٍ تثبيت التحديث...", + "updateCompletedSuccessfully": "تم التحديث بنجاح", + "updateFailed": "فشل التحديث" } diff --git a/lib/l10n/intl_bs.arb b/lib/l10n/intl_bs.arb index 883371a7b..9f7420ab6 100644 --- a/lib/l10n/intl_bs.arb +++ b/lib/l10n/intl_bs.arb @@ -290,6 +290,20 @@ } } }, + "quranReadingPagePortrait": "Strana {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": "Odaberite stranicu", "checkingForUpdates": "Provjera za ažuriranje...", "chooseQuranType": "Izaberi Kur'an", @@ -320,5 +334,23 @@ "noFavoriteReciters": "Nema omiljenih učača. Pokušajte dodati jednog na listu", "@noFavoriteReciters": { "description": "Message shown when there are no favorite reciters" - } -} \ No newline at end of file + }, + "noReciterSearchResult": "Bez rezultata za vašu pretragu.", + "searchForReciter": "Potraži učača", + "downloadAllSuwarSuccessfully": "Kompletan Kur'an je preuzet", + "noSuwarDownload": "Nema novih sura za preuzimanje", + "connectDownloadQuran": "Da bi ste preuzeli molimo vas da se povežete na internetu", + "playInOnlineModeQuran": "Da bi ste slušali učenje Kur'ana molimo vas da se povežete na internetu", + "downloaded": "Preuzeto", + "switchQuranType": "Idi na {name}", + "@switchQuranType": { + "description": "Message shown when a reciter is added to favorites", + "placeholders": { + "name": { + "type": "String", + "example": "Warsh" + } + } + }, + "surahSelector": "Odaberi suru" +} diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 108d0ddb9..5f3d806c5 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -352,5 +352,18 @@ } } }, - "surahSelector":"Select Surah" + "surahSelector":"Select Surah", + "checkForUpdates": "Check for Updates", + "checkForNewVersion": "Check if a new version is available", + "wouldYouLikeToUpdate": "Would you like to update the app?", + "updateCompleted": "Update completed successfully!", + "noUpdates": "No Updates", + "usingLatestVersion": "You are using the latest version.", + "updateCancelled": "Update cancelled", + "checkingUpdates": "Checking updates...", + "downloadingUpdate": "Downloading update...", + "installingUpdate": "Installing update...", + "updateCompletedSuccessfully": "Update completed successfully", + "updateFailed": "Update failed" + } diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index 5e96d18d2..e3df0b33f 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -352,6 +352,18 @@ "example": "604" } } - } + }, + "checkForUpdates": "Vérifier les mises à jour", + "checkForNewVersion": "Vérifiez si une nouvelle version est disponible", + "wouldYouLikeToUpdate": "Souhaitez-vous mettre à jour l'application ?", + "updateCompleted": "Mise à jour terminée avec succès !", + "noUpdates": "Aucune mise à jour", + "usingLatestVersion": "Vous utilisez la dernière version.", + "updateCancelled": "Mise à jour annulée", + "checkingUpdates": "Vérification des mises à jour...", + "downloadingUpdate": "Téléchargement de la mise à jour...", + "installingUpdate": "Installation de la mise à jour...", + "updateCompletedSuccessfully": "Mise à jour terminée avec succès", + "updateFailed": "Échec de la mise à jour" } diff --git a/lib/main.dart b/lib/main.dart index 4ba22600e..9dffcb2c6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,12 @@ +import 'package:fast_cached_network_image/fast_cached_network_image.dart'; +import 'dart:async'; +import 'dart:developer' as developer; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_kurdish_localization/flutter_kurdish_localization.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart' - show ProviderBase, ProviderContainer, ProviderObserver, ProviderScope; +import 'package:flutter_riverpod/flutter_riverpod.dart' as riverpod; import 'package:hive_flutter/adapters.dart'; import 'package:logger/logger.dart'; import 'package:mawaqit/i18n/AppLanguage.dart'; @@ -32,6 +34,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; import 'package:sizer/sizer.dart'; import 'package:timezone/data/latest.dart' as tz; +import 'package:mawaqit/src/routes/route_generator.dart'; final logger = Logger(); @@ -42,12 +45,14 @@ Future main() async { await Firebase.initializeApp(); final directory = await getApplicationDocumentsDirectory(); Hive.init(directory.path); + await FastCachedImageConfig.init(subDir: directory.path, clearCacheAfter: const Duration(days: 60)); + tz.initializeTimeZones(); Hive.registerAdapter(SurahModelAdapter()); Hive.registerAdapter(ReciterModelAdapter()); Hive.registerAdapter(MoshafModelAdapter()); runApp( - ProviderScope( + riverpod.ProviderScope( child: MyApp(), observers: [ RiverpodLogger(), @@ -59,9 +64,9 @@ Future main() async { ); } -class MyApp extends StatelessWidget { +class MyApp extends riverpod.ConsumerWidget { @override - Widget build(BuildContext context) { + Widget build(BuildContext context, riverpod.WidgetRef ref) { return MultiProvider( providers: [ ChangeNotifierProvider(create: (context) => ThemeNotifier()), @@ -85,35 +90,40 @@ class MyApp extends StatelessWidget { return event; }), child: Consumer( - builder: (context, theme, _) => Shortcuts( - shortcuts: {SingleActivator(LogicalKeyboardKey.select): ActivateIntent()}, - child: MaterialApp( - title: kAppName, - themeMode: theme.mode, - localeResolutionCallback: (locale, supportedLocales) { - if (locale?.languageCode.toLowerCase() == 'ba') return Locale('en'); + builder: (context, theme, _) { + return Shortcuts( + shortcuts: {SingleActivator(LogicalKeyboardKey.select): ActivateIntent()}, + child: MaterialApp( + title: kAppName, + themeMode: theme.mode, + localeResolutionCallback: (locale, supportedLocales) { + if (locale?.languageCode.toLowerCase() == 'ba') return Locale('en'); - return locale; - }, - theme: theme.lightTheme, - darkTheme: theme.darkTheme, - locale: model.appLocal, - navigatorKey: AppRouter.navigationKey, - navigatorObservers: [AnalyticsWrapper.observer()], - localizationsDelegates: [ - S.delegate, - GlobalCupertinoLocalizations.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - KurdishMaterialLocalizations.delegate, - KurdishWidgetLocalizations.delegate, - KurdishCupertinoLocalizations.delegate - ], - supportedLocales: S.supportedLocales, - debugShowCheckedModeBanner: false, - home: Splash(), - ), - ), + return locale; + }, + theme: theme.lightTheme, + darkTheme: theme.darkTheme, + locale: model.appLocal, + navigatorKey: AppRouter.navigationKey, + navigatorObservers: [ + AnalyticsWrapper.observer(), + ], + localizationsDelegates: [ + S.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + KurdishMaterialLocalizations.delegate, + KurdishWidgetLocalizations.delegate, + KurdishCupertinoLocalizations.delegate + ], + supportedLocales: S.supportedLocales, + debugShowCheckedModeBanner: false, + onGenerateRoute: RouteGenerator.generateRoute, + home: Splash(), + ), + ); + }, ), ); }); diff --git a/lib/src/const/constants.dart b/lib/src/const/constants.dart index 14499c311..968afac7c 100644 --- a/lib/src/const/constants.dart +++ b/lib/src/const/constants.dart @@ -96,3 +96,8 @@ abstract class SystemFeaturesConstant { static const String kHdmi = 'android.hardware.hdmi'; static const String kEthernet = 'android.hardware.ethernet'; } + +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'; +} diff --git a/lib/src/pages/SettingScreen.dart b/lib/src/pages/SettingScreen.dart index d30d4b81f..b414cfd49 100644 --- a/lib/src/pages/SettingScreen.dart +++ b/lib/src/pages/SettingScreen.dart @@ -19,18 +19,23 @@ import 'package:mawaqit/src/pages/onBoarding/widgets/OrientationWidget.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:mawaqit/src/state_management/manual_app_update/manual_update_notifier.dart'; import 'package:mawaqit/src/state_management/on_boarding/on_boarding_notifier.dart'; import 'package:mawaqit/src/state_management/quran/recite/recite_notifier.dart'; import 'package:mawaqit/src/widgets/ScreenWithAnimation.dart'; +import 'package:mawaqit/src/widgets/manual_update_dialog.dart'; import 'package:provider/provider.dart' hide Consumer; import 'package:sizer/sizer.dart'; +import 'package:upgrader/upgrader.dart'; import '../../i18n/AppLanguage.dart'; import '../../main.dart'; +import 'package:package_info_plus/package_info_plus.dart'; import '../helpers/TimeShiftManager.dart'; import '../services/FeatureManager.dart'; import '../state_management/app_update/app_update_notifier.dart'; +import '../state_management/manual_app_update/manual_update_state.dart'; import '../state_management/quran/download_quran/download_quran_notifier.dart'; import '../state_management/random_hadith/random_hadith_notifier.dart'; import '../widgets/screen_lock_widget.dart'; @@ -73,7 +78,18 @@ class _SettingScreenState extends ConsumerState { final String hadithLanguage = S.of(context).connectToChangeHadith; TimeShiftManager timeShiftManager = TimeShiftManager(); final featureManager = Provider.of(context); - + ref.listen(manualUpdateNotifierProvider, (previous, next) { + switch (next.value?.status) { + case UpdateStatus.available: + UpdateDialog.show(context, ref); + break; + case UpdateStatus.notAvailable: + UpdateDialog.showNoUpdateAvailableDialog(context); + break; + default: + break; + } + }); return ScreenWithAnimationWidget( animation: 'settings', child: Padding( @@ -159,24 +175,6 @@ 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, - ), - ); - }, - ), - SizedBox(height: 30), Divider(), SizedBox(height: 10), Text( @@ -270,6 +268,36 @@ class _SettingScreenState extends ConsumerState { }, ), _screenLock(context, ref), + Divider(), + 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).checkForUpdates, + subtitle: S.of(context).checkForNewVersion, + icon: const Icon(Icons.system_update, size: 35), + onTap: () async { + var softwareFuture = await PackageInfo.fromPlatform(); + + ref + .read(manualUpdateNotifierProvider.notifier) + .checkForUpdates(softwareFuture.version, context.read().appLocal.languageCode); + }, + ), ], ), ), diff --git a/lib/src/pages/home/sub_screens/AdhanSubScreen.dart b/lib/src/pages/home/sub_screens/AdhanSubScreen.dart index bde9c442c..233274827 100644 --- a/lib/src/pages/home/sub_screens/AdhanSubScreen.dart +++ b/lib/src/pages/home/sub_screens/AdhanSubScreen.dart @@ -2,6 +2,7 @@ import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mawaqit/i18n/l10n.dart'; import 'package:mawaqit/main.dart'; import 'package:mawaqit/src/helpers/RelativeSizes.dart'; @@ -12,13 +13,15 @@ import 'package:mawaqit/src/pages/home/widgets/mosque_background_screen.dart'; import 'package:mawaqit/src/pages/home/widgets/salah_items/responsive_mini_salah_bar_widget.dart'; import 'package:mawaqit/src/services/audio_manager.dart'; import 'package:mawaqit/src/services/mosque_manager.dart'; +import 'package:mawaqit/src/state_management/quran/quran/quran_notifier.dart'; +import 'package:mawaqit/src/state_management/quran/recite/quran_audio_player_notifier.dart'; import 'package:mawaqit/src/themes/UIShadows.dart'; import 'package:provider/provider.dart'; import '../widgets/mosque_header.dart'; import '../widgets/salah_items/responsive_mini_salah_bar_turkish_widget.dart'; -class AdhanSubScreen extends StatefulWidget { +class AdhanSubScreen extends ConsumerStatefulWidget { const AdhanSubScreen({Key? key, this.onDone, this.forceAdhan = false}) : super(key: key); final VoidCallback? onDone; @@ -27,10 +30,10 @@ class AdhanSubScreen extends StatefulWidget { final bool forceAdhan; @override - State createState() => _AdhanSubScreenState(); + ConsumerState createState() => _AdhanSubScreenState(); } -class _AdhanSubScreenState extends State { +class _AdhanSubScreenState extends ConsumerState { AudioManager? audioManager; /// if mosque using Beb sound we will wait for minutes delay @@ -46,6 +49,11 @@ class _AdhanSubScreenState extends State { final isFajrPray = mosqueManager.salahIndex == 0; final duration = mosqueManager.getAdhanDuration(isFajrPray); + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.read(quranPlayerNotifierProvider.notifier).pause(); + ref.read(quranNotifierProvider.notifier).exitQuranMode(); + }); + Future.delayed(Duration(minutes: 5), () { closeAdhanScreen(); }); diff --git a/lib/src/pages/home/sub_screens/JummuaLive.dart b/lib/src/pages/home/sub_screens/JummuaLive.dart index 1284cdd53..e4e2cc7f3 100644 --- a/lib/src/pages/home/sub_screens/JummuaLive.dart +++ b/lib/src/pages/home/sub_screens/JummuaLive.dart @@ -6,6 +6,7 @@ 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/quran/quran/quran_notifier.dart'; import 'package:mawaqit/src/themes/UIShadows.dart'; import 'package:provider/provider.dart'; @@ -34,6 +35,11 @@ class _JummuaLiveState extends ConsumerState { @override void initState() { invalidStreamUrl = context.read().mosque?.streamUrl == null; + + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.read(quranNotifierProvider.notifier).exitQuranMode(); + }); + log('JummuaLive: invalidStreamUrl: $invalidStreamUrl'); super.initState(); } diff --git a/lib/src/pages/home/workflow/salah_workflow.dart b/lib/src/pages/home/workflow/salah_workflow.dart index 3b38fbfb1..b60a40750 100644 --- a/lib/src/pages/home/workflow/salah_workflow.dart +++ b/lib/src/pages/home/workflow/salah_workflow.dart @@ -12,13 +12,14 @@ import 'package:mawaqit/src/pages/home/sub_screens/normal_home.dart'; import 'package:mawaqit/src/pages/home/widgets/workflows/repeating_workflow_widget.dart'; import 'package:mawaqit/src/services/mosque_manager.dart'; import 'package:mawaqit/src/services/user_preferences_manager.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:provider/provider.dart'; import '../sub_screens/AdhanSubScreen.dart'; import '../widgets/workflows/WorkFlowWidget.dart'; /// handling the logic form 5min before adhan -> the last of after salah azkar -class SalahWorkflowScreen extends StatefulWidget { +class SalahWorkflowScreen extends ConsumerStatefulWidget { const SalahWorkflowScreen({ Key? key, required this.onDone, @@ -28,10 +29,16 @@ class SalahWorkflowScreen extends StatefulWidget { final void Function() onDone; @override - State createState() => _SalahWorkflowScreenState(); + ConsumerState createState() => _SalahWorkflowScreenState(); } -class _SalahWorkflowScreenState extends State { +class _SalahWorkflowScreenState extends ConsumerState { + // Changed to ConsumerState + @override + void initState() { + super.initState(); + } + calculateCurrentSalah(MosqueManager mosqueManger) { if (mosqueManger.nextSalahAfter() < Duration(minutes: 5)) return mosqueManger.nextSalahIndex(); 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 79069e79b..093ebdec1 100644 --- a/lib/src/pages/quran/page/quran_mode_selection_screen.dart +++ b/lib/src/pages/quran/page/quran_mode_selection_screen.dart @@ -2,12 +2,13 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mawaqit/i18n/l10n.dart'; -import 'package:mawaqit/src/pages/quran/page/quran_reading_screen.dart'; +import 'package:mawaqit/src/pages/quran/reading/quran_reading_screen.dart'; import 'package:mawaqit/src/pages/quran/page/reciter_selection_screen.dart'; import 'package:mawaqit/src/pages/quran/widget/quran_background.dart'; import 'package:mawaqit/src/state_management/quran/quran/quran_state.dart'; import 'package:sizer/sizer.dart'; import 'package:mawaqit/src/state_management/quran/quran/quran_notifier.dart'; +import 'package:mawaqit/src/routes/routes_constant.dart'; class QuranModeSelection extends ConsumerStatefulWidget { const QuranModeSelection({super.key}); @@ -41,7 +42,7 @@ class _QuranModeSelectionState extends ConsumerState { super.dispose(); } - void _handleKeyEvent(RawKeyEvent event) { + Future _handleKeyEvent(RawKeyEvent event) async { if (event is RawKeyDownEvent) { final isLtr = Directionality.of(context) == TextDirection.ltr; @@ -65,26 +66,30 @@ class _QuranModeSelectionState extends ConsumerState { } } else if (event.logicalKey == LogicalKeyboardKey.select) { if (_selectedIndex == 0) { - ref.read(quranNotifierProvider.notifier).selectModel(QuranMode.reading); - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => QuranReadingScreen(), - ), - ); + await ref.read(quranNotifierProvider.notifier).selectModel(QuranMode.reading); + if (mounted) { + Navigator.pushReplacementNamed(context, Routes.quranReading); + } } else { - ref.read(quranNotifierProvider.notifier).selectModel(QuranMode.listening); - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => ReciterSelectionScreen.withoutSurahName(), - ), - ); + await ref.read(quranNotifierProvider.notifier).selectModel(QuranMode.listening); + if (mounted) { + Navigator.pushReplacementNamed(context, Routes.quranReciter); + } } } } } + void _handleNavigation(int index) { + if (index == 0) { + ref.read(quranNotifierProvider.notifier).selectModel(QuranMode.reading); + Navigator.pushReplacementNamed(context, Routes.quranReading); + } else { + ref.read(quranNotifierProvider.notifier).selectModel(QuranMode.listening); + Navigator.pushReplacementNamed(context, Routes.quranReciter); + } + } + @override Widget build(BuildContext context) { return RawKeyboardListener( @@ -122,13 +127,7 @@ class _QuranModeSelectionState extends ConsumerState { setState(() { _selectedIndex = 0; }); - ref.read(quranNotifierProvider.notifier).selectModel(QuranMode.reading); - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => QuranReadingScreen(), - ), - ); + _handleNavigation(0); }, isSelected: _selectedIndex == 0, focusNode: _readingFocusNode, @@ -141,13 +140,7 @@ class _QuranModeSelectionState extends ConsumerState { setState(() { _selectedIndex = 1; }); - ref.read(quranNotifierProvider.notifier).selectModel(QuranMode.listening); - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => ReciterSelectionScreen.withoutSurahName(), - ), - ); + _handleNavigation(1); }, isSelected: _selectedIndex == 1, focusNode: _listeningFocusNode, @@ -172,7 +165,7 @@ class _QuranModeSelectionState extends ConsumerState { return Focus( focusNode: focusNode, child: GestureDetector( - onTap: onPressed, + onTap: () => _handleNavigation(isSelected ? 0 : 1), child: AnimatedContainer( duration: Duration(milliseconds: 200), width: 50.w, diff --git a/lib/src/pages/quran/page/quran_player_screen.dart b/lib/src/pages/quran/page/quran_player_screen.dart index 9d077b31c..8d51d7f8a 100644 --- a/lib/src/pages/quran/page/quran_player_screen.dart +++ b/lib/src/pages/quran/page/quran_player_screen.dart @@ -1,6 +1,6 @@ import 'dart:math' as math; -import 'package:cached_network_image/cached_network_image.dart'; import 'package:collection/collection.dart'; +import 'package:fast_cached_network_image/fast_cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; @@ -130,21 +130,33 @@ class _QuranPlayerScreenState extends ConsumerState { ClipOval buildReciterImage() { return ClipOval( child: Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Colors.transparent, - ), - child: CachedNetworkImage( - imageUrl: '${QuranConstant.kQuranReciterImagesBaseUrl}${widget.reciterId}.jpg', - fit: BoxFit.fitWidth, - placeholder: (context, url) => Container(color: Colors.transparent), - errorWidget: (context, url, error) => Container(color: Colors.transparent), - ), - ), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.transparent, + ), + child: FastCachedImage( + url: '${QuranConstant.kQuranReciterImagesBaseUrl}${widget.reciterId}.jpg', + fit: BoxFit.fitWidth, + loadingBuilder: (context, progress) => Container(color: Colors.transparent), + errorBuilder: (context, error, stackTrace) => _buildOfflineImage())), ); } } +Widget _buildOfflineImage() { + return Center( + child: Container( + width: 24.w, + height: 24.w, + padding: EdgeInsets.only(bottom: 2.h), + child: Image.asset( + R.ASSETS_SVG_RECITER_ICON_PNG, + fit: BoxFit.contain, + ), + ), + ); +} + class _QuranPlayer extends ConsumerStatefulWidget { const _QuranPlayer({ super.key, diff --git a/lib/src/pages/quran/page/quran_reading_screen.dart b/lib/src/pages/quran/page/quran_reading_screen.dart index 7b8c619b1..8b1378917 100644 --- a/lib/src/pages/quran/page/quran_reading_screen.dart +++ b/lib/src/pages/quran/page/quran_reading_screen.dart @@ -1,394 +1 @@ -import 'dart:developer'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:mawaqit/i18n/l10n.dart'; -import 'package:mawaqit/src/pages/quran/page/reciter_selection_screen.dart'; -import 'package:mawaqit/src/pages/quran/widget/reading/quran_reading_widgets.dart'; -import 'package:mawaqit/src/pages/quran/widget/reading/quran_surah_selector.dart'; - -import 'package:mawaqit/src/services/user_preferences_manager.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/quran_reading_notifer.dart'; - -import 'package:mawaqit/src/pages/quran/widget/download_quran_popup.dart'; -import 'package:mawaqit/src/state_management/quran/reading/quran_reading_state.dart'; -import 'package:provider/provider.dart' as provider; - -import 'package:sizer/sizer.dart'; - -import 'package:mawaqit/src/state_management/quran/quran/quran_state.dart'; - -import 'package:mawaqit/src/pages/quran/widget/reading/quran_reading_page_selector.dart'; - -import '../../../data/data_source/device_info_data_source.dart'; - -class QuranReadingScreen extends ConsumerStatefulWidget { - const QuranReadingScreen({super.key}); - - @override - ConsumerState createState() => _QuranReadingScreenState(); -} - -class _QuranReadingScreenState extends ConsumerState { - late FocusNode _rightSkipButtonFocusNode; - late FocusNode _leftSkipButtonFocusNode; - late FocusNode _backButtonFocusNode; - late FocusNode _switchQuranFocusNode; - late FocusNode _switchQuranModeNode; - late FocusNode _switchScreenViewFocusNode; - late FocusNode _portraitModeBackButtonFocusNode; - late FocusNode _portraitModeSwitchQuranFocusNode; - late FocusNode _portraitModePageSelectorFocusNode; - final ScrollController _gridScrollController = ScrollController(); - final GlobalKey _scaffoldKey = GlobalKey(); - - @override - void initState() { - super.initState(); - _initializeFocusNodes(); - - WidgetsBinding.instance.addPostFrameCallback((_) async { - ref.read(downloadQuranNotifierProvider); - ref.read(quranReadingNotifierProvider); - }); - } - - void _initializeFocusNodes() { - _rightSkipButtonFocusNode = FocusNode(debugLabel: 'right_skip_node'); - _leftSkipButtonFocusNode = FocusNode(debugLabel: 'left_skip_node'); - _backButtonFocusNode = FocusNode(debugLabel: 'back_button_node'); - _switchQuranFocusNode = FocusNode(debugLabel: 'switch_quran_node'); - _switchQuranModeNode = FocusNode(debugLabel: 'switch_quran_mode_node'); - _switchScreenViewFocusNode = FocusNode(debugLabel: 'switch_screen_view_node'); - _portraitModeBackButtonFocusNode = FocusNode(debugLabel: 'portrait_mode_back_button_node'); - _portraitModeSwitchQuranFocusNode = FocusNode(debugLabel: 'portrait_mode_switch_quran_node'); - _portraitModePageSelectorFocusNode = FocusNode(debugLabel: 'portrait_mode_page_selector_node'); - } - - bool _isRotated = false; - - @override - void dispose() { - _disposeFocusNodes(); - - super.dispose(); - } - - void _disposeFocusNodes() { - _leftSkipButtonFocusNode.dispose(); - _rightSkipButtonFocusNode.dispose(); - _backButtonFocusNode.dispose(); - _switchQuranFocusNode.dispose(); - _switchScreenViewFocusNode.dispose(); - _portraitModeBackButtonFocusNode.dispose(); - _portraitModeSwitchQuranFocusNode.dispose(); - _portraitModePageSelectorFocusNode.dispose(); - } - - void _toggleOrientation() { - setState(() { - _isRotated = !_isRotated; - }); - } - - @override - Widget build(BuildContext context) { - final quranReadingState = ref.watch(quranReadingNotifierProvider); - final userPrefs = context.watch(); - ref.listen(downloadQuranNotifierProvider, (previous, next) async { - if (!next.hasValue || next.value is Success) { - ref.invalidate(quranReadingNotifierProvider); - } - - // don't show dialog for them - if (next.hasValue && - (next.value is NoUpdate || - next.value is CheckingDownloadedQuran || - next.value is CheckingUpdate || - next.value is CancelDownload)) { - return; - } - - if (previous!.hasValue && previous.value != next.value) { - // Perform an action based on the new status - } - - if (!_isThereCurrentDialogShowing(context)) { - await showDialog( - context: context, - barrierDismissible: false, - builder: (context) => DownloadQuranDialog(), - ); - } - }); - - _leftSkipButtonFocusNode.onKeyEvent = (node, event) => _handleSwitcherFocusGroupNode(node, event); - _rightSkipButtonFocusNode.onKeyEvent = (node, event) => _handleSwitcherFocusGroupNode(node, event); - - _switchScreenViewFocusNode.onKeyEvent = - (node, event) => _handlePageScrollDownFocusGroupNode(node, event, _isRotated); - _portraitModePageSelectorFocusNode.onKeyEvent = - (node, event) => _handlePageScrollDownFocusGroupNode(node, event, _isRotated); - _switchQuranModeNode.onKeyEvent = (node, event) => _handlePageScrollDownFocusGroupNode(node, event, _isRotated); - _portraitModeBackButtonFocusNode.onKeyEvent = - (node, event) => _handlePageScrollUpFocusGroupNode(node, event, _isRotated); - _portraitModeSwitchQuranFocusNode.onKeyEvent = - (node, event) => _handlePageScrollUpFocusGroupNode(node, event, _isRotated); - return WillPopScope( - onWillPop: () async { - userPrefs.orientationLandscape = true; - - return true; - }, - child: RotatedBox( - quarterTurns: _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: _isRotated - ? buildFloatingPortrait(_isRotated, userPrefs, context) - : buildFloatingLandscape(_isRotated, userPrefs, context), - body: _buildBody(quranReadingState, _isRotated, userPrefs), - ), - ), - )); - } - - Widget _buildBody( - AsyncValue quranReadingState, bool isPortrait, UserPreferencesManager userPrefs) { - final color = Theme.of(context).primaryColor; - return quranReadingState.when( - loading: () => Center( - child: CircularProgressIndicator( - color: color, - ), - ), - error: (error, s) { - final errorLocalized = S.of(context).error; - return Center(child: Text('$errorLocalized: $error')); - }, - data: (quranReadingState) { - return Stack( - children: [ - isPortrait - ? buildVerticalPageView(quranReadingState, ref) - : buildHorizontalPageView(quranReadingState, ref, context), - if (!isPortrait) ...[ - buildRightSwitchButton( - context, - _rightSkipButtonFocusNode, - () => _scrollPageList(ScrollDirection.forward, isPortrait), - ), - buildLeftSwitchButton( - context, - _leftSkipButtonFocusNode, - () => _scrollPageList(ScrollDirection.reverse, isPortrait), - ), - ], - buildPageNumberIndicator( - quranReadingState, isPortrait, context, _portraitModePageSelectorFocusNode, _showPageSelector), - buildMoshafSelector( - isPortrait, - context, - isPortrait ? _portraitModeSwitchQuranFocusNode : _switchQuranFocusNode, - _isThereCurrentDialogShowing(context)), - buildBackButton( - isPortrait, userPrefs, context, isPortrait ? _portraitModeBackButtonFocusNode : _backButtonFocusNode), - isPortrait ? SizedBox() : buildShowSurah(quranReadingState), - ], - ); - }, - ); - } - - Align buildShowSurah(QuranReadingState quranReadingState) { - return Align( - alignment: Alignment.topCenter, - child: Padding( - padding: EdgeInsets.only(top: 0.3.h), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: () { - ref.read(quranReadingNotifierProvider.notifier).getAllSuwarPage(); - showSurahSelector(context, quranReadingState.currentPage); - }, - borderRadius: BorderRadius.circular(20), - child: Container( - padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.4), - borderRadius: BorderRadius.circular(20), - ), - child: Text( - quranReadingState.currentSurahName, - style: TextStyle( - color: Colors.white, - fontSize: 8.sp, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ), - ), - ); - } - - Widget buildFloatingPortrait(bool isPortrait, UserPreferencesManager userPrefs, BuildContext context) { - return Padding( - padding: EdgeInsets.only(left: 30), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - mainAxisSize: MainAxisSize.max, - children: [ - _buildOrientationToggleButton(isPortrait), - _buildQuranModeButton(isPortrait, userPrefs, context), - ], - ), - ); - } - - Widget buildFloatingLandscape(bool isPortrait, UserPreferencesManager userPrefs, BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - _buildOrientationToggleButton(isPortrait), - SizedBox(height: 10), - _buildQuranModeButton(isPortrait, userPrefs, context), - ], - ); - } - - Widget _buildOrientationToggleButton(bool isPortrait) { - return SizedBox( - width: isPortrait ? 35.sp : 30.sp, - height: isPortrait ? 35.sp : 30.sp, - child: FloatingActionButton( - focusNode: _switchScreenViewFocusNode, - backgroundColor: Colors.black.withOpacity(.3), - child: Icon( - !isPortrait ? Icons.stay_current_portrait : Icons.stay_current_landscape, - color: Colors.white, - size: isPortrait ? 20.sp : 15.sp, - ), - onPressed: () => _toggleOrientation(), - heroTag: null, - ), - ); - } - - Widget _buildQuranModeButton(bool isPortrait, UserPreferencesManager userPrefs, BuildContext context) { - return SizedBox( - width: isPortrait ? 35.sp : 30.sp, - height: isPortrait ? 35.sp : 30.sp, - child: FloatingActionButton( - focusNode: _switchQuranModeNode, - backgroundColor: Colors.black.withOpacity(.3), - child: Icon( - Icons.headset, - color: Colors.white, - size: isPortrait ? 20.sp : 15.sp, - ), - onPressed: () async { - ref.read(quranNotifierProvider.notifier).selectModel(QuranMode.listening); - if (isPortrait) { - userPrefs.orientationLandscape = true; - } - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => ReciterSelectionScreen.withoutSurahName(), - ), - ); - }, - heroTag: null, - ), - ); - } - - void _scrollPageList(ScrollDirection direction, isPortrait) { - if (direction == ScrollDirection.forward) { - ref.read(quranReadingNotifierProvider.notifier).previousPage(isPortrait: isPortrait); - } else { - ref.read(quranReadingNotifierProvider.notifier).nextPage(isPortrait: isPortrait); - } - } - - void _showPageSelector(BuildContext context, int totalPages, int currentPage, bool switcherScreen) { - showDialog( - context: context, - builder: (BuildContext context) { - return QuranReadingPageSelector( - isPortrait: switcherScreen, - currentPage: currentPage, - scrollController: _gridScrollController, - totalPages: totalPages, - ); - }, - ); - } - - FloatingActionButtonLocation _getFloatingActionButtonLocation(BuildContext context) { - final TextDirection textDirection = Directionality.of(context); - switch (textDirection) { - case TextDirection.ltr: - return FloatingActionButtonLocation.endFloat; - case TextDirection.rtl: - return FloatingActionButtonLocation.startFloat; - default: - return FloatingActionButtonLocation.endFloat; - } - } - - KeyEventResult _handleSwitcherFocusGroupNode(FocusNode node, KeyEvent event) { - if (event is KeyDownEvent) { - if (event.logicalKey == LogicalKeyboardKey.arrowRight) { - _rightSkipButtonFocusNode.requestFocus(); - return KeyEventResult.handled; - } else if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { - _leftSkipButtonFocusNode.requestFocus(); - return KeyEventResult.handled; - } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) { - _backButtonFocusNode.requestFocus(); - return KeyEventResult.handled; - } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) { - _switchQuranFocusNode.requestFocus(); - return KeyEventResult.handled; - } - } - return KeyEventResult.ignored; - } - - KeyEventResult _handlePageScrollDownFocusGroupNode(FocusNode node, KeyEvent event, bool isPortrait) { - if (event is KeyDownEvent) { - if (event.logicalKey == LogicalKeyboardKey.arrowDown && isPortrait) { - _scrollPageList(ScrollDirection.reverse, isPortrait); - return KeyEventResult.handled; - } - } - return KeyEventResult.ignored; - } - - KeyEventResult _handlePageScrollUpFocusGroupNode(FocusNode node, KeyEvent event, bool isPortrait) { - if (event is KeyDownEvent) { - if (event.logicalKey == LogicalKeyboardKey.arrowUp) { - _scrollPageList(ScrollDirection.forward, isPortrait); - - return KeyEventResult.handled; - } - } - return KeyEventResult.ignored; - } - - bool _isThereCurrentDialogShowing(BuildContext context) => ModalRoute.of(context)?.isCurrent != true; -} diff --git a/lib/src/pages/quran/page/reciter_selection_screen.dart b/lib/src/pages/quran/page/reciter_selection_screen.dart index 7b25b32fb..9929e00bb 100644 --- a/lib/src/pages/quran/page/reciter_selection_screen.dart +++ b/lib/src/pages/quran/page/reciter_selection_screen.dart @@ -23,6 +23,8 @@ import 'package:mawaqit/i18n/l10n.dart'; import 'package:mawaqit/src/pages/quran/widget/reciter_list_view.dart'; import '../../../domain/model/quran/reciter_model.dart'; +import '../reading/quran_reading_screen.dart'; +import 'package:mawaqit/src/routes/routes_constant.dart'; class ReciterSelectionScreen extends ConsumerStatefulWidget { final String surahName; @@ -108,6 +110,11 @@ class _ReciterSelectionScreenState extends ConsumerState ref.read(reciteNotifierProvider.notifier).setSearchQuery(_searchController.text, isAllReciters); } + void _navigateToReading() { + ref.read(quranNotifierProvider.notifier).selectModel(QuranMode.reading); + Navigator.pushReplacementNamed(context, Routes.quranReading); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -124,24 +131,18 @@ class _ReciterSelectionScreenState extends ConsumerState color: Colors.white, size: 15.sp, ), - onPressed: () async { - ref.read(quranNotifierProvider.notifier).selectModel(QuranMode.reading); - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => QuranReadingScreen(), - ), - ); - }, + onPressed: _navigateToReading, ), ), appBar: AppBar( + toolbarHeight: 40, backgroundColor: Color(0xFF28262F), elevation: 0, title: AutoSizeText( S.of(context).chooseReciter, style: TextStyle( color: Colors.white, + fontSize: 14.sp, fontWeight: FontWeight.bold, ), maxLines: 1, diff --git a/lib/src/pages/quran/page/surah_selection_screen.dart b/lib/src/pages/quran/page/surah_selection_screen.dart index 1b98d7a96..f26549824 100644 --- a/lib/src/pages/quran/page/surah_selection_screen.dart +++ b/lib/src/pages/quran/page/surah_selection_screen.dart @@ -23,6 +23,7 @@ import '../../../models/address_model.dart'; import '../../../services/theme_manager.dart'; import '../../../state_management/quran/recite/download_audio_quran/download_audio_quran_notifier.dart'; import '../../../state_management/quran/recite/download_audio_quran/download_audio_quran_state.dart'; +import 'package:mawaqit/src/routes/routes_constant.dart'; class SurahSelectionScreen extends ConsumerStatefulWidget { final MoshafModel selectedMoshaf; @@ -91,18 +92,17 @@ class _SurahSelectionScreenState extends ConsumerState { suwar: suwar, reciterId: widget.reciterId, ); - Navigator.push( + + Navigator.pushNamed( context, - MaterialPageRoute( - builder: (context) => QuranPlayerScreen( - reciterId: widget.reciterId, - selectedMoshaf: widget.selectedMoshaf, - surah: surah, - ), - ), - ).then((_) { - _isNavigating = false; - }); + Routes.quranPlayer, + arguments: { + 'reciterId': widget.reciterId, + 'selectedMoshaf': widget.selectedMoshaf, + 'surah': surah, + }, + ); + _isNavigating = false; } String _getKey() { diff --git a/lib/src/pages/quran/reading/quran_reading_screen.dart b/lib/src/pages/quran/reading/quran_reading_screen.dart new file mode 100644 index 000000000..74fda1aea --- /dev/null +++ b/lib/src/pages/quran/reading/quran_reading_screen.dart @@ -0,0 +1,730 @@ +import 'dart:developer'; + +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mawaqit/i18n/l10n.dart'; +import 'package:mawaqit/src/pages/quran/reading/widget/quran_floating_action_buttons.dart'; +import 'package:mawaqit/src/pages/quran/widget/reading/quran_reading_widgets.dart'; +import 'package:mawaqit/src/pages/quran/widget/reading/quran_surah_selector.dart'; + +import 'package:mawaqit/src/services/user_preferences_manager.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/quran/quran_state.dart'; +import 'package:mawaqit/src/state_management/quran/reading/auto_reading/auto_reading_notifier.dart'; +import 'package:mawaqit/src/state_management/quran/reading/auto_reading/auto_reading_state.dart'; +import 'package:mawaqit/src/state_management/quran/reading/quran_reading_notifer.dart'; + +import 'package:mawaqit/src/pages/quran/widget/download_quran_popup.dart'; +import 'package:mawaqit/src/state_management/quran/reading/quran_reading_state.dart'; +import 'package:provider/provider.dart' as provider; + +import 'package:mawaqit/src/pages/quran/widget/reading/quran_reading_page_selector.dart'; +import 'package:mawaqit/src/routes/routes_constant.dart'; + +abstract class QuranViewStrategy { + Widget buildView(QuranReadingState state, WidgetRef ref, BuildContext context); + + List buildControls( + BuildContext context, + QuranReadingState state, + UserPreferencesManager userPrefs, + bool isPortrait, + FocusNodes focusNodes, + Function(ScrollDirection, bool) onScroll, + Function(BuildContext, int, int, bool) showPageSelector, + ); +} + +// Helper class to organize focus nodes +class FocusNodes { + final FocusNode backButtonNode; + final FocusNode leftSkipNode; + final FocusNode rightSkipNode; + final FocusNode pageSelectorNode; + final FocusNode switchQuranNode; + final FocusNode surahSelectorNode; + + FocusNodes({ + required this.backButtonNode, + required this.leftSkipNode, + required this.rightSkipNode, + required this.pageSelectorNode, + required this.switchQuranNode, + required this.surahSelectorNode, + }); +} + +class AutoScrollViewStrategy implements QuranViewStrategy { + final AutoScrollState autoScrollState; + + AutoScrollViewStrategy(this.autoScrollState); + + @override + Widget buildView(QuranReadingState state, WidgetRef ref, BuildContext context) { + final scalingFactor = autoScrollState.fontSize; + + return ListView.builder( + physics: NeverScrollableScrollPhysics(), + controller: autoScrollState.scrollController, + itemCount: state.totalPages, + itemBuilder: (context, index) { + return GestureDetector( + onTap: () { + final autoScrollNotifier = ref.read(autoScrollNotifierProvider.notifier); + if (autoScrollState.isPlaying) { + autoScrollNotifier.pauseAutoScroll(); + } else { + autoScrollNotifier.resumeAutoScroll(); + } + }, + child: SizedBox( + width: MediaQuery.of(context).size.width * scalingFactor, + height: MediaQuery.of(context).size.height * scalingFactor, + child: SvgPictureWidget( + svgPicture: state.svgs[index], + ), + ), + ); + }, + ); + } + + @override + List buildControls( + BuildContext context, + QuranReadingState state, + UserPreferencesManager userPrefs, + bool isPortrait, + FocusNodes focusNodes, + Function(ScrollDirection, bool) onScroll, + Function(BuildContext, int, int, bool) showPageSelector, + ) { + return []; + } +} + +class NormalViewStrategy implements QuranViewStrategy { + final bool isPortrait; + + NormalViewStrategy(this.isPortrait); + + @override + Widget buildView(QuranReadingState state, WidgetRef ref, BuildContext context) { + return isPortrait + ? VerticalPageViewWidget( + quranReadingState: state, + ) + : HorizontalPageViewWidget( + quranReadingState: state, + ); + } + + @override + List buildControls( + BuildContext context, + QuranReadingState state, + UserPreferencesManager userPrefs, + bool isPortrait, + FocusNodes focusNodes, + Function(ScrollDirection, bool) onScroll, + Function(BuildContext, int, int, bool) showPageSelector, + ) { + if (isPortrait) { + return [ + BackButtonWidget( + isPortrait: isPortrait, + userPrefs: userPrefs, + focusNode: focusNodes.backButtonNode, + ), + SurahSelectorWidget( + isPortrait: isPortrait, + focusNode: focusNodes.surahSelectorNode, + isThereCurrentDialogShowing: false, + ), + PageNumberIndicatorWidget( + quranReadingState: state, + focusNode: focusNodes.pageSelectorNode, + isPortrait: isPortrait, + showPageSelector: showPageSelector, + ), + MoshafSelectorPositionedWidget( + isPortrait: isPortrait, + focusNode: focusNodes.switchQuranNode, + isThereCurrentDialogShowing: false, + ), + ]; + } + + return [ + BackButtonWidget( + isPortrait: isPortrait, + userPrefs: userPrefs, + focusNode: focusNodes.backButtonNode, + ), + _buildNavigationButtons( + context, + focusNodes, + onScroll, + isPortrait, + ), + SurahSelectorWidget( + isPortrait: isPortrait, + focusNode: focusNodes.surahSelectorNode, + isThereCurrentDialogShowing: false, + ), + PageNumberIndicatorWidget( + quranReadingState: state, + focusNode: focusNodes.pageSelectorNode, + isPortrait: isPortrait, + showPageSelector: showPageSelector, + ), + MoshafSelectorPositionedWidget( + isPortrait: isPortrait, + focusNode: focusNodes.switchQuranNode, + isThereCurrentDialogShowing: false, + ), + ]; + } + + Widget _buildNavigationButtons( + BuildContext context, + FocusNodes focusNodes, + Function(ScrollDirection, bool) onScroll, + bool isPortrait, + ) { + return FocusTraversalGroup( + policy: ArrowButtonsFocusTraversalPolicy( + backButtonNode: focusNodes.backButtonNode, + pageSelectorNode: focusNodes.pageSelectorNode, + ), + child: Stack( + children: [ + LeftSwitchButtonWidget( + focusNode: focusNodes.leftSkipNode, + onPressed: () => onScroll(ScrollDirection.reverse, isPortrait), + ), + RightSwitchButtonWidget( + focusNode: focusNodes.rightSkipNode, + onPressed: () => onScroll(ScrollDirection.forward, isPortrait), + ), + ], + ), + ); + } +} + +class QuranReadingScreen extends ConsumerStatefulWidget { + const QuranReadingScreen({super.key}); + + @override + ConsumerState createState() => _QuranReadingScreenState(); +} + +class _QuranReadingScreenState extends ConsumerState { + late FocusNode _rightSkipButtonFocusNode; + late FocusNode _leftSkipButtonFocusNode; + late FocusNode _backButtonFocusNode; + late FocusNode _switchQuranFocusNode; + late FocusNode _switchQuranModeNode; + late FocusNode _surahSelectorNode; + late FocusNode _switchScreenViewFocusNode; + late FocusNode _switchToPlayQuranFocusNode; + late FocusNode _portraitModeBackButtonFocusNode; + late FocusNode _portraitModeSwitchQuranFocusNode; + late FocusNode _portraitModePageSelectorFocusNode; + final ScrollController _gridScrollController = ScrollController(); + bool _isRotated = false; + + @override + void initState() { + super.initState(); + _initializeFocusNodes(); + + WidgetsBinding.instance.addPostFrameCallback((_) async { + ref.read(downloadQuranNotifierProvider); + ref.read(quranReadingNotifierProvider); + }); + } + + void _initializeFocusNodes() { + _rightSkipButtonFocusNode = FocusNode(debugLabel: 'right_skip_node'); + _leftSkipButtonFocusNode = FocusNode(debugLabel: 'left_skip_node'); + _backButtonFocusNode = FocusNode(debugLabel: 'back_button_node'); + _switchQuranFocusNode = FocusNode(debugLabel: 'switch_quran_node'); + _switchQuranModeNode = FocusNode(debugLabel: 'switch_quran_mode_node'); + _switchScreenViewFocusNode = FocusNode(debugLabel: 'switch_screen_view_node'); + _portraitModeBackButtonFocusNode = FocusNode(debugLabel: 'portrait_mode_back_button_node'); + _portraitModeSwitchQuranFocusNode = FocusNode(debugLabel: 'portrait_mode_switch_quran_node'); + _portraitModePageSelectorFocusNode = FocusNode(debugLabel: 'portrait_mode_page_selector_node'); + _switchToPlayQuranFocusNode = FocusNode(debugLabel: 'switch_to_play_quran_node'); + _surahSelectorNode = FocusNode(debugLabel: 'surah_selector_node'); + } + + @override + void dispose() { + _disposeFocusNodes(); + super.dispose(); + } + + void _disposeFocusNodes() { + _leftSkipButtonFocusNode.dispose(); + _rightSkipButtonFocusNode.dispose(); + _backButtonFocusNode.dispose(); + _switchQuranFocusNode.dispose(); + _switchScreenViewFocusNode.dispose(); + _portraitModeBackButtonFocusNode.dispose(); + _portraitModeSwitchQuranFocusNode.dispose(); + _portraitModePageSelectorFocusNode.dispose(); + _switchToPlayQuranFocusNode.dispose(); + _surahSelectorNode.dispose(); + } + + void _navigateToListeningMode() { + ref.read(quranNotifierProvider.notifier).selectModel(QuranMode.listening); + Navigator.pushReplacementNamed(context, Routes.quranReciter); + } + + @override + Widget build(BuildContext context) { + final quranReadingState = ref.watch(quranReadingNotifierProvider); + final userPrefs = context.watch(); + ref.listen(downloadQuranNotifierProvider, (previous, next) async { + if (!next.hasValue || next.value is Success) { + ref.invalidate(quranReadingNotifierProvider); + } + + // don't show dialog for them + if (next.hasValue && + (next.value is NoUpdate || + next.value is CheckingDownloadedQuran || + next.value is CheckingUpdate || + next.value is CancelDownload)) { + return; + } + + if (previous!.hasValue && previous.value != next.value) { + // Perform an action based on the new status + } + + if (!_isThereCurrentDialogShowing(context)) { + await showDialog( + context: context, + barrierDismissible: false, + builder: (context) => DownloadQuranDialog(), + ); + } + }); + + 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), + ), + ), + ); + }, + loading: () => SizedBox(), + error: (error, stack) => const Icon(Icons.error), + ), + ); + } + + Widget _buildBody( + AsyncValue quranReadingState, + bool isPortrait, + UserPreferencesManager userPrefs, + AutoScrollState autoScrollState, + ) { + return quranReadingState.when( + loading: () => _buildLoadingIndicator(), + error: (error, s) => _buildErrorIndicator(error), + data: (state) { + // Initialize the appropriate strategy + final viewStrategy = + autoScrollState.isSinglePageView ? AutoScrollViewStrategy(autoScrollState) : NormalViewStrategy(isPortrait); + + // Create focus nodes bundle + final focusNodes = FocusNodes( + backButtonNode: _backButtonFocusNode, + leftSkipNode: _leftSkipButtonFocusNode, + rightSkipNode: _rightSkipButtonFocusNode, + pageSelectorNode: _portraitModePageSelectorFocusNode, + switchQuranNode: _switchQuranFocusNode, + surahSelectorNode: _surahSelectorNode, + ); + if (isPortrait) { + return FocusTraversalGroup( + policy: PortraitModeFocusTraversalPolicy( + backButtonNode: _backButtonFocusNode, + switchToPlayQuranFocusNode: _switchToPlayQuranFocusNode, + switchQuranNode: _switchQuranFocusNode, + pageSelectorNode: _portraitModePageSelectorFocusNode, + ), + child: Stack( + children: [ + // Main content + viewStrategy.buildView(state, ref, context), + + // Controls overlay - show in both portrait and landscape + ...viewStrategy.buildControls( + context, + state, + userPrefs, + isPortrait, + focusNodes, + _scrollPageList, + _showPageSelector, + ), + ], + ), + ); + } + return Stack( + children: [ + // Main content + viewStrategy.buildView(state, ref, context), + + // Controls overlay - show in both portrait and landscape + ...viewStrategy.buildControls( + context, + state, + userPrefs, + isPortrait, + focusNodes, + _scrollPageList, + _showPageSelector, + ), + ], + ); + }, + ); + } + + Widget _buildLoadingIndicator() { + return Center( + child: CircularProgressIndicator( + color: Theme.of(context).primaryColor, + ), + ); + } + + Widget _buildErrorIndicator(Object error) { + final errorLocalized = S.of(context).error; + return Center( + child: Text('$errorLocalized: $error'), + ); + } + + Widget buildAutoScrollView( + QuranReadingState quranReadingState, + WidgetRef ref, + AutoScrollState autoScrollState, + ) { + return ListView.builder( + physics: NeverScrollableScrollPhysics(), + controller: autoScrollState.scrollController, + itemCount: quranReadingState.totalPages, + itemBuilder: (context, index) { + return LayoutBuilder( + builder: (context, constraints) { + final pageHeight = + constraints.maxHeight.isInfinite ? MediaQuery.of(context).size.height : constraints.maxHeight; + return Container( + width: constraints.maxWidth, + height: pageHeight, + child: quranReadingState.svgs[index], + ); + }, + ); + }, + ); + } + + void _scrollPageList(ScrollDirection direction, isPortrait) { + if (direction == ScrollDirection.forward) { + ref.read(quranReadingNotifierProvider.notifier).previousPage(isPortrait: isPortrait); + } else { + ref.read(quranReadingNotifierProvider.notifier).nextPage(isPortrait: isPortrait); + } + } + + void _showPageSelector(BuildContext context, int totalPages, int currentPage, bool switcherScreen) { + showDialog( + context: context, + builder: (BuildContext context) { + return QuranReadingPageSelector( + isPortrait: switcherScreen, + currentPage: currentPage, + scrollController: _gridScrollController, + totalPages: totalPages, + ); + }, + ); + } + + FloatingActionButtonLocation _getFloatingActionButtonLocation(BuildContext context) { + final TextDirection textDirection = Directionality.of(context); + switch (textDirection) { + case TextDirection.ltr: + return FloatingActionButtonLocation.endFloat; + case TextDirection.rtl: + return FloatingActionButtonLocation.startFloat; + default: + return FloatingActionButtonLocation.endFloat; + } + } + + bool _isThereCurrentDialogShowing(BuildContext context) => ModalRoute.of(context)?.isCurrent != true; +} + +class ArrowButtonsFocusTraversalPolicy extends FocusTraversalPolicy { + final FocusNode backButtonNode; + final FocusNode pageSelectorNode; + + const ArrowButtonsFocusTraversalPolicy({ + super.requestFocusCallback, + required this.backButtonNode, + required this.pageSelectorNode, + }); + + @override + FocusNode? findFirstFocus(FocusNode currentNode, {bool ignoreCurrentFocus = false}) { + final nodes = currentNode.nearestScope!.traversalDescendants; + return nodes.firstWhereOrNull((node) => node.debugLabel?.contains('left_skip_node') == true); + } + + @override + FocusNode findLastFocus(FocusNode currentNode, {bool ignoreCurrentFocus = false}) { + final nodes = currentNode.nearestScope!.traversalDescendants; + return nodes.firstWhereOrNull((node) => node.debugLabel?.contains('right_skip_node') == true) ?? currentNode; + } + + @override + FocusNode? findFirstFocusInDirection(FocusNode currentNode, TraversalDirection direction) { + switch (direction) { + case TraversalDirection.up: + return backButtonNode; + case TraversalDirection.down: + return pageSelectorNode; + case TraversalDirection.left: + case TraversalDirection.right: + return null; + } + } + + @override + Iterable sortDescendants(Iterable descendants, FocusNode currentNode) { + final arrowNodes = descendants + .where((node) => + node.debugLabel?.contains('left_skip_node') == true || node.debugLabel?.contains('right_skip_node') == true) + .toList(); + + mergeSort(arrowNodes, compare: (a, b) { + final aIsLeft = a.debugLabel?.contains('left_skip_node') == true; + final bIsLeft = b.debugLabel?.contains('left_skip_node') == true; + return aIsLeft ? -1 : 1; + }); + + return arrowNodes; + } + + @override + bool inDirection(FocusNode currentNode, TraversalDirection direction) { + final nodes = currentNode.nearestScope!.traversalDescendants; + final leftNode = nodes.firstWhereOrNull((node) => node.debugLabel?.contains('left_skip_node') == true); + final rightNode = nodes.firstWhereOrNull((node) => node.debugLabel?.contains('right_skip_node') == true); + + switch (direction) { + case TraversalDirection.left: + if (currentNode == rightNode && leftNode != null) { + requestFocusCallback(leftNode); + return true; + } + return false; + case TraversalDirection.right: + if (currentNode == leftNode && rightNode != null) { + requestFocusCallback(rightNode); + return true; + } + return false; + case TraversalDirection.up: + if ((currentNode == leftNode || currentNode == rightNode) && backButtonNode.canRequestFocus) { + requestFocusCallback(backButtonNode); + return true; + } + return false; + case TraversalDirection.down: + if ((currentNode == leftNode || currentNode == rightNode) && pageSelectorNode.canRequestFocus) { + requestFocusCallback(pageSelectorNode); + return true; + } + return false; + } + } +} + +class PortraitModeFocusTraversalPolicy extends FocusTraversalPolicy { + final FocusNode backButtonNode; + final FocusNode switchQuranNode; + final FocusNode pageSelectorNode; + final FocusNode switchToPlayQuranFocusNode; + + const PortraitModeFocusTraversalPolicy({ + super.requestFocusCallback, + required this.backButtonNode, + required this.switchQuranNode, + required this.pageSelectorNode, + required this.switchToPlayQuranFocusNode, + }); + + @override + FocusNode? findFirstFocus(FocusNode currentNode, {bool ignoreCurrentFocus = false}) { + return backButtonNode; + } + + @override + FocusNode findLastFocus(FocusNode currentNode, {bool ignoreCurrentFocus = false}) { + return pageSelectorNode; + } + + @override + bool inDirection(FocusNode currentNode, TraversalDirection direction) { + print('Current Node: ${currentNode.debugLabel}, Direction: $direction, || FocusNode: ${currentNode}'); + + if (currentNode == backButtonNode) { + switch (direction) { + case TraversalDirection.right: + if (_canFocusNode(switchQuranNode)) { + _requestFocus(switchQuranNode); + return true; + } + break; + case TraversalDirection.down: + if (_canFocusNode(pageSelectorNode)) { + _requestFocus(pageSelectorNode); + return true; + } + break; + default: + return false; + } + } else if (currentNode == switchQuranNode) { + switch (direction) { + case TraversalDirection.left: + if (_canFocusNode(backButtonNode)) { + _requestFocus(backButtonNode); + return true; + } + break; + case TraversalDirection.right: + if (_canFocusNode(pageSelectorNode)) { + _requestFocus(pageSelectorNode); + return true; + } + break; + case TraversalDirection.down: + if (_canFocusNode(pageSelectorNode)) { + _requestFocus(pageSelectorNode); + return true; + } + break; + default: + break; + } + } else if (currentNode == pageSelectorNode) { + switch (direction) { + case TraversalDirection.up: + if (_canFocusNode(switchQuranNode)) { + _requestFocus(switchQuranNode); + return true; + } + break; + case TraversalDirection.down: + if (_canFocusNode(backButtonNode)) { + _requestFocus(backButtonNode); + return true; + } + break; + case TraversalDirection.left: + if (_canFocusNode(switchQuranNode)) { + _requestFocus(switchQuranNode); + return true; + } + break; + case TraversalDirection.right: + print('Current Node: Right test'); + if (_canFocusNode(switchToPlayQuranFocusNode)) { + _requestFocus(switchToPlayQuranFocusNode); + return true; + } + break; + default: + break; + } + } else if (currentNode == switchToPlayQuranFocusNode) { + switch (direction) { + case TraversalDirection.up: + if (_canFocusNode(switchQuranNode)) { + _requestFocus(switchQuranNode); + return true; + } + break; + case TraversalDirection.left: + if (_canFocusNode(switchQuranNode)) { + _requestFocus(switchQuranNode); + return true; + } + break; + + default: + break; + } + } + return false; + } + + bool _canFocusNode(FocusNode node) { + return node.canRequestFocus; + } + + void _requestFocus(FocusNode node) { + requestFocusCallback.call(node); + } + + @override + FocusNode? findFirstFocusInDirection(FocusNode currentNode, TraversalDirection direction) { + return null; + } + + @override + Iterable sortDescendants(Iterable descendants, FocusNode currentNode) { + return [backButtonNode, switchQuranNode, pageSelectorNode].where((node) => descendants.contains(node)); + } +} diff --git a/lib/src/pages/quran/reading/widget/quran_floating_action_buttons.dart b/lib/src/pages/quran/reading/widget/quran_floating_action_buttons.dart new file mode 100644 index 000000000..3064b1944 --- /dev/null +++ b/lib/src/pages/quran/reading/widget/quran_floating_action_buttons.dart @@ -0,0 +1,369 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mawaqit/src/pages/quran/page/reciter_selection_screen.dart'; +import 'package:mawaqit/src/routes/routes_constant.dart'; +import 'package:mawaqit/src/state_management/quran/quran/quran_notifier.dart'; +import 'package:mawaqit/src/state_management/quran/quran/quran_state.dart'; +import 'package:mawaqit/src/state_management/quran/reading/auto_reading/auto_reading_notifier.dart'; +import 'package:mawaqit/src/state_management/quran/reading/quran_reading_notifer.dart'; +import 'package:mawaqit/src/state_management/quran/reading/quran_reading_state.dart'; +import 'package:sizer/sizer.dart'; + +class QuranFloatingActionControls extends ConsumerStatefulWidget { + final FocusNode switchScreenViewFocusNode; + final FocusNode switchQuranModeNode; + final FocusNode switchToPlayQuranFocusNode; + + const QuranFloatingActionControls({ + super.key, + required this.switchScreenViewFocusNode, + required this.switchQuranModeNode, + required this.switchToPlayQuranFocusNode, + }); + + @override + ConsumerState createState() => _QuranFloatingActionControlsState(); +} + +class _QuranFloatingActionControlsState extends ConsumerState { + @override + Widget build(BuildContext context) { + final quranReadingState = ref.watch(quranReadingNotifierProvider); + final autoScrollState = ref.watch(autoScrollNotifierProvider); + + return quranReadingState.when( + data: (state) { + if (autoScrollState.isAutoScrolling) { + return _AutoScrollingReadingMode( + isPortrait: state.isRotated, + quranReadingState: state, + switchToPlayQuranFocusNode: widget.switchToPlayQuranFocusNode, + ); + } + + return FocusTraversalGroup( + policy: WidgetOrderTraversalPolicy(), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _PlayPauseButton( + isPortrait: state.isRotated, + switchToPlayQuranFocusNode: widget.switchToPlayQuranFocusNode, + ), + SizedBox(height: 12), + _OrientationToggleButton( + switchScreenViewFocusNode: widget.switchScreenViewFocusNode, + ), + SizedBox(height: 12), + _QuranModeButton( + isPortrait: state.isRotated, + switchQuranModeNode: widget.switchQuranModeNode, + ), + ], + ), + ); + }, + loading: () => const CircularProgressIndicator(), + error: (_, __) => const Icon(Icons.error), + ); + } +} + +class _QuranModeButton extends ConsumerWidget { + final bool isPortrait; + final FocusNode switchQuranModeNode; + + const _QuranModeButton({ + required this.isPortrait, + required this.switchQuranModeNode, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + // Calculate relative size + double buttonSize = isPortrait + ? MediaQuery.of(context).size.width * 0.06 // Adjust as needed + : MediaQuery.of(context).size.width * 0.06; // Adjust as needed + double iconSize = buttonSize * 0.5; // Icon size relative to button size + + return SizedBox( + width: buttonSize, + height: buttonSize, + child: FloatingActionButton( + focusNode: switchQuranModeNode, + backgroundColor: Colors.black.withOpacity(.3), + child: Icon( + Icons.headset, + color: Colors.white, + size: iconSize, + ), + onPressed: () { + ref.read(quranNotifierProvider.notifier).selectModel(QuranMode.listening); + Navigator.pushReplacementNamed(context, Routes.quranReciter); + }, + heroTag: null, + ), + ); + } +} + +class _PlayPauseButton extends ConsumerWidget { + final bool isPortrait; + final FocusNode? switchToPlayQuranFocusNode; + + const _PlayPauseButton({ + required this.isPortrait, + this.switchToPlayQuranFocusNode, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final autoScrollNotifier = ref.read(autoScrollNotifierProvider.notifier); + final autoScrollState = ref.watch(autoScrollNotifierProvider); + + return _ActionButton( + isPortrait: isPortrait, + icon: !autoScrollState.isAutoScrolling + ? Icons.play_arrow + : autoScrollState.isPlaying + ? Icons.pause + : Icons.play_arrow, + onPressed: () { + if (!autoScrollState.isAutoScrolling) { + final quranReadingState = ref.watch(quranReadingNotifierProvider); + final currentPage = quranReadingState.maybeWhen( + data: (state) => state.currentPage, + orElse: () => 0, + ); + final pageHeight = MediaQuery.of(context).size.height; + autoScrollNotifier.toggleAutoScroll(currentPage, pageHeight); + } + if (autoScrollState.isPlaying) { + autoScrollNotifier.pauseAutoScroll(); + } else { + autoScrollNotifier.resumeAutoScroll(); + } + }, + focusNode: switchToPlayQuranFocusNode, + ); + } +} + +class _AutoScrollingReadingMode extends ConsumerWidget { + final bool isPortrait; + final QuranReadingState quranReadingState; + final FocusNode? switchToPlayQuranFocusNode; + + const _AutoScrollingReadingMode({ + required this.isPortrait, + required this.quranReadingState, + this.switchToPlayQuranFocusNode, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final autoScrollState = ref.watch(autoScrollNotifierProvider); + return Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + _ExitButton( + isPortrait: isPortrait, + ), + SizedBox(height: 1.h), + _FontSizeControls( + isPortrait: isPortrait, + fontSize: autoScrollState.fontSize, + ), + SizedBox(height: 1.h), + _SpeedControls( + quranReadingState: quranReadingState, + isPortrait: isPortrait, + speed: autoScrollState.autoScrollSpeed, + ), + SizedBox(height: 1.h), + _PlayPauseButton( + isPortrait: isPortrait, + switchToPlayQuranFocusNode: switchToPlayQuranFocusNode, + ), + ], + ); + } +} + +class _FontSizeControls extends ConsumerWidget { + final bool isPortrait; + final double fontSize; + + const _FontSizeControls({ + required this.isPortrait, + required this.fontSize, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return _ActionButton( + isPortrait: isPortrait, + icon: Icons.text_fields, + onPressed: () => ref.read(autoScrollNotifierProvider.notifier).cycleFontSize(), + tooltip: 'Font Size: ${(fontSize * 100).toInt()}%', + autoFocus: true, + ); + } +} + +// Add new Exit button widget +class _ExitButton extends ConsumerStatefulWidget { + final bool isPortrait; + + const _ExitButton({ + super.key, + required this.isPortrait, + }); + + @override + ConsumerState createState() => __ExitButtonState(); +} + +class __ExitButtonState extends ConsumerState<_ExitButton> { + late FocusNode exitFocusNode; + @override + void initState() { + exitFocusNode = FocusNode(debugLabel: 'exit_focus_node'); + WidgetsBinding.instance.addPostFrameCallback((_) { + exitFocusNode.requestFocus(); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return _ActionButton( + autoFocus: true, + focusNode: exitFocusNode, + isPortrait: widget.isPortrait, + icon: Icons.close, + onPressed: () { + ref.read(autoScrollNotifierProvider.notifier).stopAutoScroll(); + }, + tooltip: 'Exit Auto-Scroll', + ); + } +} + +class _SpeedControls extends ConsumerWidget { + final QuranReadingState quranReadingState; + final bool isPortrait; + final double speed; + + const _SpeedControls({ + required this.quranReadingState, + required this.isPortrait, + required this.speed, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return _ActionButton( + isPortrait: isPortrait, + icon: Icons.speed, + onPressed: () { + final pageHeight = MediaQuery.of(context).size.height; + ref.read(autoScrollNotifierProvider.notifier).cycleSpeed( + quranReadingState.currentPage, + pageHeight, + ); + }, + tooltip: 'Speed: ${(speed * 100).toInt()}%', + ); + } +} + +class _ActionButton extends StatelessWidget { + final bool isPortrait; + final IconData icon; + final VoidCallback onPressed; + final String? tooltip; + final FocusNode? focusNode; + bool autoFocus; + + _ActionButton({ + required this.isPortrait, + required this.icon, + required this.onPressed, + this.tooltip, + this.focusNode, + this.autoFocus = false, + }); + + @override + Widget build(BuildContext context) { + // Calculate relative size + double buttonSize = isPortrait + ? MediaQuery.of(context).size.width * 0.06 // Adjust as needed + : MediaQuery.of(context).size.width * 0.06; // Adjust as needed + double iconSize = buttonSize * 0.5; // Icon size relative to button size + + return SizedBox( + width: buttonSize, + height: buttonSize, + child: FloatingActionButton( + autofocus: autoFocus, + focusNode: focusNode, + backgroundColor: Colors.black.withOpacity(.3), + child: Icon( + icon, + color: Colors.white, + size: iconSize, + ), + onPressed: onPressed, + heroTag: null, + tooltip: tooltip, + ), + ); + } +} + +class _OrientationToggleButton extends ConsumerWidget { + final FocusNode switchScreenViewFocusNode; + + const _OrientationToggleButton({ + required this.switchScreenViewFocusNode, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final quranReadingState = ref.watch(quranReadingNotifierProvider); + + return quranReadingState.when( + data: (state) { + // Calculate relative size + double buttonSize = state.isRotated + ? MediaQuery.of(context).size.width * 0.06 // Adjust as needed + : MediaQuery.of(context).size.width * 0.06; // Adjust as needed + double iconSize = buttonSize * 0.5; // Icon size relative to button size + + return SizedBox( + width: buttonSize, + height: buttonSize, + child: FloatingActionButton( + focusNode: switchScreenViewFocusNode, + backgroundColor: Colors.black.withOpacity(.3), + child: Icon( + !state.isRotated ? Icons.stay_current_portrait : Icons.stay_current_landscape, + color: Colors.white, + size: iconSize, + ), + onPressed: () { + ref.read(quranReadingNotifierProvider.notifier).toggleRotation(); + }, + heroTag: null, + ), + ); + }, + loading: () => const CircularProgressIndicator(), + error: (_, __) => const Icon(Icons.error), + ); + } +} diff --git a/lib/src/pages/quran/widget/quran_background.dart b/lib/src/pages/quran/widget/quran_background.dart index 31b4618fb..a4ab8cf7e 100644 --- a/lib/src/pages/quran/widget/quran_background.dart +++ b/lib/src/pages/quran/widget/quran_background.dart @@ -3,7 +3,8 @@ import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mawaqit/const/resource.dart'; -import 'package:mawaqit/src/pages/quran/page/quran_reading_screen.dart'; +import 'package:mawaqit/src/pages/quran/reading/quran_reading_screen.dart'; +import 'package:mawaqit/src/routes/routes_constant.dart'; import 'package:mawaqit/src/services/theme_manager.dart'; import 'package:sizer/sizer.dart'; @@ -47,11 +48,9 @@ class QuranBackground extends ConsumerWidget { onPressed: () async { ref.read(quranNotifierProvider.notifier).selectModel(QuranMode.reading); log('quran: QuranBackground: Switch to reading'); - Navigator.pushReplacement( + Navigator.pushReplacementNamed( context, - MaterialPageRoute( - builder: (context) => QuranReadingScreen(), - ), + Routes.quranReading, ); }, ), @@ -90,11 +89,9 @@ class QuranBackground extends ConsumerWidget { ), onPressed: () async { ref.read(quranNotifierProvider.notifier).selectModel(QuranMode.reading); - Navigator.pushReplacement( + Navigator.pushReplacementNamed( context, - MaterialPageRoute( - builder: (context) => QuranReadingScreen(), - ), + Routes.quranReading, ); }, ), diff --git a/lib/src/pages/quran/widget/reading/quran_reading_widgets.dart b/lib/src/pages/quran/widget/reading/quran_reading_widgets.dart index 3ef138747..17fc234cb 100644 --- a/lib/src/pages/quran/widget/reading/quran_reading_widgets.dart +++ b/lib/src/pages/quran/widget/reading/quran_reading_widgets.dart @@ -1,3 +1,4 @@ +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -5,199 +6,297 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:mawaqit/i18n/l10n.dart'; import 'package:mawaqit/src/pages/quran/widget/reading/moshaf_selector.dart'; import 'package:mawaqit/src/pages/quran/widget/switch_button.dart'; -import 'package:mawaqit/src/state_management/quran/reading/quran_reading_state.dart'; import 'package:mawaqit/src/services/user_preferences_manager.dart'; +import 'package:mawaqit/src/state_management/quran/reading/quran_reading_state.dart'; import 'package:sizer/sizer.dart'; -import '../../../../state_management/quran/reading/quran_reading_notifer.dart'; - -Widget buildVerticalPageView(QuranReadingState quranReadingState, WidgetRef ref) { - return PageView.builder( - scrollDirection: Axis.vertical, - controller: quranReadingState.pageController, - onPageChanged: (index) { - if (index != quranReadingState.currentPage) { - ref.read(quranReadingNotifierProvider.notifier).updatePage(index, isPortairt: true); - } - }, - itemCount: quranReadingState.totalPages, - itemBuilder: (context, index) { - return LayoutBuilder( - builder: (context, constraints) { - final pageWidth = constraints.maxWidth; - final pageHeight = constraints.maxHeight; - - return Stack( - children: [ - Positioned.fill( - child: FittedBox( - fit: BoxFit.contain, - child: SizedBox( - width: pageWidth + 150, - height: pageHeight + 100, - child: buildSvgPicture( - quranReadingState.svgs[index % quranReadingState.svgs.length], +import 'package:mawaqit/src/state_management/quran/reading/quran_reading_notifer.dart'; + +import 'package:mawaqit/src/state_management/quran/reading/auto_reading/auto_reading_notifier.dart'; + +class VerticalPageViewWidget extends ConsumerWidget { + final QuranReadingState quranReadingState; + + const VerticalPageViewWidget({ + super.key, + required this.quranReadingState, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return PageView.builder( + scrollDirection: Axis.vertical, + controller: quranReadingState.pageController, + onPageChanged: (index) { + if (index != quranReadingState.currentPage) { + ref.read(quranReadingNotifierProvider.notifier).updatePage(index, isPortairt: true); + } + }, + itemCount: quranReadingState.totalPages, + itemBuilder: (context, index) { + return LayoutBuilder( + builder: (context, constraints) { + final pageWidth = constraints.maxWidth; + final pageHeight = constraints.maxHeight; + return Stack( + children: [ + Positioned.fill( + child: FittedBox( + fit: BoxFit.contain, + child: SizedBox( + width: pageWidth + 150, + height: pageHeight + 100, + child: SvgPictureWidget( + svgPicture: quranReadingState.svgs[index % quranReadingState.svgs.length], + ), ), ), ), - ), - ], - ); - }, - ); - }, - ); + ], + ); + }, + ); + }, + ); + } } -Widget buildHorizontalPageView(QuranReadingState quranReadingState, WidgetRef ref, BuildContext context) { - return PageView.builder( - reverse: Directionality.of(context) == TextDirection.ltr ? true : false, - controller: quranReadingState.pageController, - onPageChanged: (index) { - final actualPage = index * 2; - if (actualPage != quranReadingState.currentPage) { - ref.read(quranReadingNotifierProvider.notifier).updatePage(actualPage); - } - }, - itemCount: (quranReadingState.totalPages / 2).ceil(), - itemBuilder: (context, index) { - return LayoutBuilder( - builder: (context, constraints) { - final pageWidth = constraints.maxWidth / 2; - final pageHeight = constraints.maxHeight; - final bottomPadding = pageHeight * 0.05; - - final leftPageIndex = index * 2; - final rightPageIndex = leftPageIndex + 1; - return Stack( - children: [ - if (rightPageIndex < quranReadingState.svgs.length) - Positioned( - left: 12.w, - top: 0, - bottom: bottomPadding, - width: pageWidth * 0.9, - child: buildSvgPicture( - quranReadingState.svgs[rightPageIndex % quranReadingState.svgs.length], +class HorizontalPageViewWidget extends ConsumerWidget { + final QuranReadingState quranReadingState; + + const HorizontalPageViewWidget({ + super.key, + required this.quranReadingState, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final autoScrollState = ref.watch(autoScrollNotifierProvider); + return PageView.builder( + reverse: Directionality.of(context) == TextDirection.ltr ? true : false, + controller: quranReadingState.pageController, + onPageChanged: (index) { + final actualPage = index * 2; + if (actualPage != quranReadingState.currentPage) { + ref.read(quranReadingNotifierProvider.notifier).updatePage(actualPage); + } + }, + itemCount: (quranReadingState.totalPages / 2).ceil(), + itemBuilder: (context, index) { + return LayoutBuilder( + builder: (context, constraints) { + final pageWidth = constraints.maxWidth / 2; + final pageHeight = constraints.maxHeight; + final bottomPadding = pageHeight * 0.05; + + final leftPageIndex = index * 2; + final rightPageIndex = leftPageIndex + 1; + return Stack( + children: [ + if (rightPageIndex < quranReadingState.svgs.length) + Positioned( + left: 12.w, + top: 0, + bottom: bottomPadding, + width: pageWidth * 0.9, + child: SvgPictureWidget( + svgPicture: quranReadingState.svgs[rightPageIndex % quranReadingState.svgs.length], + ), ), - ), - if (leftPageIndex < quranReadingState.svgs.length) - Positioned( - right: 12.w, - top: 0, - bottom: bottomPadding, - width: pageWidth * 0.9, - child: buildSvgPicture( - quranReadingState.svgs[leftPageIndex % quranReadingState.svgs.length], + if (leftPageIndex < quranReadingState.svgs.length) + Positioned( + right: 12.w, + top: 0, + bottom: bottomPadding, + width: pageWidth * 0.9, + child: SvgPictureWidget( + svgPicture: quranReadingState.svgs[leftPageIndex % quranReadingState.svgs.length], + ), ), - ), - ], - ); - }, - ); - }, - ); + ], + ); + }, + ); + }, + ); + } } -Widget buildRightSwitchButton(BuildContext context, FocusNode focusNode, Function() onPressed) { - return Positioned( - right: 10, - top: 0, - bottom: 0, - child: SwitchButton( - focusNode: focusNode, - opacity: 0.7, - iconSize: 14.sp, - icon: Directionality.of(context) == TextDirection.ltr ? Icons.arrow_forward_ios : Icons.arrow_back_ios, - onPressed: onPressed, - ), - ); -} +class RightSwitchButtonWidget extends ConsumerWidget { + final FocusNode focusNode; + final VoidCallback onPressed; -Widget buildLeftSwitchButton(BuildContext context, FocusNode focusNode, Function() onPressed) { - return Positioned( - left: 10, - top: 0, - bottom: 0, - child: SwitchButton( - focusNode: focusNode, - opacity: 0.7, - iconSize: 14.sp, - icon: Directionality.of(context) != TextDirection.ltr ? Icons.arrow_forward_ios : Icons.arrow_back_ios, - onPressed: onPressed, - ), - ); + const RightSwitchButtonWidget({ + super.key, + required this.focusNode, + required this.onPressed, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return FocusTraversalOrder( + order: NumericFocusOrder(2), + child: Positioned( + right: 10, + top: 0, + bottom: 0, + child: SwitchButton( + focusNode: focusNode, + opacity: 0.7, + iconSize: 14.sp, + icon: Directionality.of(context) == TextDirection.ltr ? Icons.arrow_forward_ios : Icons.arrow_back_ios, + onPressed: onPressed, + ), + ), + ); + } } -Widget buildPageNumberIndicator(QuranReadingState quranReadingState, bool isPortrait, BuildContext context, - FocusNode focusNode, Function(BuildContext, int, int, bool) showPageSelector) { - return Positioned( - left: 15.w, - right: 15.w, - bottom: isPortrait ? 1.h : 0.5.h, - child: Center( - child: Material( - color: Colors.transparent, - child: InkWell( +class LeftSwitchButtonWidget extends ConsumerWidget { + final FocusNode focusNode; + final VoidCallback onPressed; + + const LeftSwitchButtonWidget({ + Key? key, + required this.focusNode, + required this.onPressed, + }) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return FocusTraversalOrder( + order: NumericFocusOrder(1), + child: Positioned( + left: 10, + top: 0, + bottom: 0, + child: SwitchButton( focusNode: focusNode, - autofocus: false, - onTap: () => - showPageSelector(context, quranReadingState.totalPages, quranReadingState.currentPage, isPortrait), - borderRadius: BorderRadius.circular(20), - child: Container( - padding: EdgeInsets.symmetric(horizontal: 16, vertical: isPortrait ? 8 : 4), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.4), - borderRadius: BorderRadius.circular(20), + opacity: 0.7, + iconSize: 14.sp, + icon: Directionality.of(context) != TextDirection.ltr ? Icons.arrow_forward_ios : Icons.arrow_back_ios, + onPressed: onPressed, + ), + ), + ); + } +} + +class PageNumberIndicatorWidget extends ConsumerWidget { + final QuranReadingState quranReadingState; + final bool isPortrait; + final FocusNode focusNode; + final Function(BuildContext, int, int, bool) showPageSelector; + + const PageNumberIndicatorWidget({ + super.key, + required this.quranReadingState, + required this.isPortrait, + required this.focusNode, + required this.showPageSelector, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Positioned( + left: 15.w, + right: 15.w, + bottom: isPortrait ? 1.h : 0.5.h, + child: Center( + child: Material( + color: Colors.transparent, + child: InkWell( + focusNode: focusNode, + autofocus: false, + onTap: () => showPageSelector( + context, + quranReadingState.totalPages, + quranReadingState.currentPage, + isPortrait, ), - child: Text( - isPortrait - ? S - .of(context) - .quranReadingPagePortrait(quranReadingState.currentPage + 1, quranReadingState.totalPages) - : S.of(context).quranReadingPage( - quranReadingState.currentPage + 1, - quranReadingState.currentPage + 2, - quranReadingState.totalPages, - ), - style: TextStyle( - color: Colors.white, - fontSize: 10.sp, - fontWeight: FontWeight.bold, + borderRadius: BorderRadius.circular(20), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: isPortrait ? 8 : 4), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.4), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + isPortrait + ? S.of(context).quranReadingPagePortrait( + quranReadingState.currentPage + 1, + quranReadingState.totalPages, + ) + : S.of(context).quranReadingPage( + quranReadingState.currentPage + 1, + quranReadingState.currentPage + 2, + quranReadingState.totalPages, + ), + style: TextStyle( + color: Colors.white, + fontSize: 10.sp, + fontWeight: FontWeight.bold, + ), ), ), ), ), ), - ), - ); + ); + } } -Widget buildMoshafSelector( - bool isPortrait, BuildContext context, FocusNode focusNode, bool isThereCurrentDialogShowing) { - return isPortrait - ? Positioned.directional( - end: 10, - textDirection: Directionality.of(context), - top: 1.h, - child: MoshafSelector( - isAutofocus: !isThereCurrentDialogShowing, - focusNode: focusNode, - ), - ) - : Positioned( - left: 10, - bottom: 0.5.h, - child: MoshafSelector( - isPortrait: false, - isAutofocus: !isThereCurrentDialogShowing, - focusNode: focusNode, - ), - ); +class MoshafSelectorPositionedWidget extends ConsumerWidget { + final bool isPortrait; + final FocusNode focusNode; + final bool isThereCurrentDialogShowing; + + const MoshafSelectorPositionedWidget({ + Key? key, + required this.isPortrait, + required this.focusNode, + required this.isThereCurrentDialogShowing, + }) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return isPortrait + ? Positioned.directional( + end: 10, + textDirection: Directionality.of(context), + top: 1.h, + child: MoshafSelector( + isAutofocus: !isThereCurrentDialogShowing, + focusNode: focusNode, + ), + ) + : Positioned( + left: 10, + bottom: 0.5.h, + child: MoshafSelector( + isPortrait: false, + isAutofocus: !isThereCurrentDialogShowing, + focusNode: focusNode, + ), + ); + } } -Widget buildBackButton(bool isPortrait, UserPreferencesManager userPrefs, BuildContext context, FocusNode focusNode) { - return Positioned.directional( +class BackButtonWidget extends ConsumerWidget { + final bool isPortrait; + final UserPreferencesManager userPrefs; + final FocusNode focusNode; + + const BackButtonWidget({ + Key? key, + required this.isPortrait, + required this.userPrefs, + required this.focusNode, + }) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Positioned.directional( start: 10, top: 10, textDirection: Directionality.of(context), @@ -227,19 +326,35 @@ Widget buildBackButton(bool isPortrait, UserPreferencesManager userPrefs, BuildC ), ), ), - )); + ), + ); + } } -Widget buildSvgPicture(SvgPicture svgPicture) { - return Container( - color: Colors.white, - padding: EdgeInsets.all(32.0), - child: SvgPicture( - svgPicture.bytesLoader, - fit: BoxFit.contain, - width: double.infinity, - height: double.infinity, - alignment: Alignment.center, - ), - ); +class SvgPictureWidget extends StatelessWidget { + final SvgPicture svgPicture; + final double? width; + final double? height; + + const SvgPictureWidget({ + super.key, + required this.svgPicture, + this.width = double.infinity, + this.height = double.infinity, + }); + + @override + Widget build(BuildContext context) { + return Container( + color: Colors.white, + padding: EdgeInsets.all(32.0), + child: SvgPicture( + svgPicture.bytesLoader, + fit: BoxFit.contain, + width: width, + height: height, + alignment: Alignment.center, + ), + ); + } } diff --git a/lib/src/pages/quran/widget/reading/quran_surah_selector.dart b/lib/src/pages/quran/widget/reading/quran_surah_selector.dart index b6dcb84d4..aee95d62f 100644 --- a/lib/src/pages/quran/widget/reading/quran_surah_selector.dart +++ b/lib/src/pages/quran/widget/reading/quran_surah_selector.dart @@ -1,96 +1,163 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mawaqit/i18n/l10n.dart'; -import 'package:mawaqit/src/state_management/quran/quran/quran_notifier.dart'; import 'package:mawaqit/src/state_management/quran/reading/quran_reading_notifer.dart'; import 'package:sizer/sizer.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; -void showSurahSelector(BuildContext context, int currentPage) { - final AutoScrollController controller = AutoScrollController(); +class SurahSelectorWidget extends ConsumerWidget { + final bool isPortrait; + final FocusNode focusNode; + final bool isThereCurrentDialogShowing; - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Text( - S.of(context).surahSelector, - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.bold, + const SurahSelectorWidget({ + super.key, + required this.isPortrait, + required this.focusNode, + required this.isThereCurrentDialogShowing, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + // Don't show the widget in portrait mode + if (isPortrait) { + return const SizedBox.shrink(); + } + + final quranReadingState = ref.watch(quranReadingNotifierProvider); + + return Positioned( + top: 8, + left: 0, + right: 0, + child: Center( + child: quranReadingState.when( + loading: () => const CircularProgressIndicator(), + error: (error, stackTrace) => const SizedBox.shrink(), + data: (state) => Material( + color: Colors.transparent, + child: InkWell( + focusNode: focusNode, + onTap: () { + if (!isThereCurrentDialogShowing) { + ref.read(quranReadingNotifierProvider.notifier).getAllSuwarPage(); + _showSurahSelector(context, ref); + } + }, + borderRadius: BorderRadius.circular(20), + child: Builder( + builder: (context) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.4), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + state.currentSurahName, + style: TextStyle( + color: Colors.white, + fontSize: 8.sp, + fontWeight: FontWeight.bold, + ), + ), + ); + }, + ), + ), ), - textAlign: TextAlign.center, ), - content: Container( - width: double.maxFinite, - height: MediaQuery.of(context).size.height * 0.8, - child: Consumer( - builder: (context, ref, _) { - final suwarState = ref.watch(quranReadingNotifierProvider); - return suwarState.when( - loading: () => Center(child: CircularProgressIndicator()), - error: (err, stack) => Center(child: Text('Error: $err')), - data: (quranState) { - final suwar = quranState.suwar; - final currentSurahIndex = suwar.indexWhere((element) => element.name == quranState.currentSurahName); + ), + ); + } - WidgetsBinding.instance.addPostFrameCallback((_) { - controller.scrollToIndex(currentSurahIndex, preferPosition: AutoScrollPosition.begin); - }); + void _showSurahSelector(BuildContext context, WidgetRef ref) { + final AutoScrollController controller = AutoScrollController(); - return GridView.builder( - controller: controller, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 4, - childAspectRatio: 2.5 / 1, - crossAxisSpacing: 8, - mainAxisSpacing: 8, - ), - itemCount: suwar.length, - itemBuilder: (BuildContext context, int index) { - final surah = suwar[index]; - final page = surah.startPage % 2 == 0 ? surah.startPage - 1 : surah.startPage; - return AutoScrollTag( - key: ValueKey(index), - controller: controller, - index: index, - child: InkWell( - autofocus: index == currentSurahIndex, - onTap: () { - ref.read(quranReadingNotifierProvider.notifier).updatePage(page); - Navigator.of(context).pop(); - }, - child: Container( - height: 40.h, - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(6.0), - border: Border.all( - color: Theme.of(context).dividerColor, - width: 1, + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text( + S.of(context).surahSelector, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + content: Container( + width: double.maxFinite, + height: MediaQuery.of(context).size.height * 0.8, + child: Consumer( + builder: (context, ref, _) { + final suwarState = ref.watch(quranReadingNotifierProvider); + return suwarState.when( + loading: () => Center(child: CircularProgressIndicator()), + error: (err, stack) => Center(child: Text('Error: $err')), + data: (quranState) { + final suwar = quranState.suwar; + final currentSurahIndex = + suwar.indexWhere((element) => element.name == quranState.currentSurahName); + + WidgetsBinding.instance.addPostFrameCallback((_) { + controller.scrollToIndex(currentSurahIndex, preferPosition: AutoScrollPosition.begin); + }); + + return GridView.builder( + controller: controller, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + childAspectRatio: 2.5 / 1, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + ), + itemCount: suwar.length, + itemBuilder: (BuildContext context, int index) { + final surah = suwar[index]; + final page = surah.startPage % 2 == 0 ? surah.startPage - 1 : surah.startPage; + return AutoScrollTag( + key: ValueKey(index), + controller: controller, + index: index, + child: InkWell( + autofocus: index == currentSurahIndex, + onTap: () { + ref.read(quranReadingNotifierProvider.notifier).updatePage(page); + Navigator.of(context).pop(); + }, + child: Container( + height: 40.h, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6.0), + border: Border.all( + color: Theme.of(context).dividerColor, + width: 1, + ), ), - ), - child: Text( - "${surah.id}- ${surah.name}", - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 10.sp, - fontWeight: FontWeight.normal, + child: Text( + "${surah.id}- ${surah.name}", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 10.sp, + fontWeight: FontWeight.normal, + ), + overflow: TextOverflow.ellipsis, + maxLines: 2, ), - overflow: TextOverflow.ellipsis, - maxLines: 2, ), ), - ), - ); - }, - ); - }, - ); - }, + ); + }, + ); + }, + ); + }, + ), ), - ), - ); - }, - ); + ); + }, + ); + } } diff --git a/lib/src/pages/quran/widget/reciter_list_view.dart b/lib/src/pages/quran/widget/reciter_list_view.dart index 6ea5b8a1c..7ffa1f003 100644 --- a/lib/src/pages/quran/widget/reciter_list_view.dart +++ b/lib/src/pages/quran/widget/reciter_list_view.dart @@ -1,4 +1,4 @@ -import 'package:cached_network_image/cached_network_image.dart'; +import 'package:fast_cached_network_image/fast_cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -140,12 +140,11 @@ class ReciterCard extends ConsumerWidget { fit: StackFit.expand, children: [ // CachedNetworkImage with different sizes for online and offline images - CachedNetworkImage( - imageUrl: '${QuranConstant.kQuranReciterImagesBaseUrl}${reciter.id}.jpg', - fit: BoxFit.cover, - placeholder: (context, url) => _buildOfflineImage(), // Use a smaller image - errorWidget: (context, url, error) => _buildOfflineImage(), // Use a smaller image on error - ), + FastCachedImage( + url: '${QuranConstant.kQuranReciterImagesBaseUrl}${reciter.id}.jpg', + fit: BoxFit.fitWidth, + loadingBuilder: (context, progress) => Container(color: Colors.transparent), + errorBuilder: (context, error, stackTrace) => _buildOfflineImage()), // Gradient overlay Container( decoration: BoxDecoration( diff --git a/lib/src/pages/times/normal_home/landscape_normal_home.dart b/lib/src/pages/times/normal_home/landscape_normal_home.dart index b218ee8e7..2b44c478b 100644 --- a/lib/src/pages/times/normal_home/landscape_normal_home.dart +++ b/lib/src/pages/times/normal_home/landscape_normal_home.dart @@ -88,7 +88,9 @@ class _LandscapeNormalHomeState extends riverpod.ConsumerState generateRoute(RouteSettings settings) { + // Special handling for QuranModeSelection + if (settings.name == Routes.quranModeSelection) { + return MaterialPageRoute( + builder: (context) => const QuranModeSelection(), + ); + } + + // Check if the route is a Quran screen + if (Routes.quranScreens.contains(settings.name)) { + return MaterialPageRoute( + builder: (context) { + return Consumer( + builder: (context, ref, child) { + final quranState = ref.watch(quranNotifierProvider); + if (quranState.value?.mode == QuranMode.none) { + WidgetsBinding.instance.addPostFrameCallback((_) { + // Get the navigator state + final navigator = Navigator.of(context); + // Pop until we reach the root or can't pop anymore + while (navigator.canPop()) { + // Pop both dialog and screen if in listening mode + if (Routes.quranScreens.contains(settings.name)) { + navigator.pop(); + } + } + }); + return const SizedBox(); // Return empty widget while popping + } + return _buildQuranScreen(settings, context); + }, + ); + }, + ); + } + + // Handle non-Quran routes + switch (settings.name) { + case '/': + return MaterialPageRoute(builder: (_) => Splash()); + default: + return _errorRoute(); + } + } + + static Widget _buildQuranScreen(RouteSettings settings, BuildContext context) { + switch (settings.name) { + case Routes.quranModeSelection: + return const QuranModeSelection(); + + case Routes.quranReading: + return const QuranReadingScreen(); + + case Routes.quranReciter: + return const ReciterSelectionScreen.withoutSurahName(); + + case Routes.quranSurah: + final args = settings.arguments as Map; + return SurahSelectionScreen( + selectedMoshaf: args['selectedMoshaf'] as MoshafModel, + reciterId: args['reciterId'] as String, + ); + + case Routes.quranPlayer: + final args = settings.arguments as Map; + return QuranPlayerScreen( + reciterId: args['reciterId'] as String, + selectedMoshaf: args['selectedMoshaf'] as MoshafModel, + surah: args['surah'] as SurahModel, + ); + + default: + return const SizedBox(); + } + } + + static Route _errorRoute() { + return MaterialPageRoute( + builder: (_) => Scaffold( + appBar: AppBar(title: const Text('Error')), + body: const Center(child: Text('Route not found')), + ), + ); + } +} diff --git a/lib/src/routes/routes_constant.dart b/lib/src/routes/routes_constant.dart new file mode 100644 index 000000000..97d8516e7 --- /dev/null +++ b/lib/src/routes/routes_constant.dart @@ -0,0 +1,15 @@ +class Routes { + static const String quranModeSelection = '/quran'; + static const String quranReading = '/quran/reading'; + static const String quranReciter = '/quran/reciter'; + static const String quranSurah = '/quran/surah'; + static const String quranPlayer = '/quran/player'; + + static const List quranScreens = [ + quranModeSelection, + quranReading, + quranReciter, + quranSurah, + quranPlayer, + ]; +} diff --git a/lib/src/services/mixins/mosque_helpers_mixins.dart b/lib/src/services/mixins/mosque_helpers_mixins.dart index e02c80e70..f74508ab9 100644 --- a/lib/src/services/mixins/mosque_helpers_mixins.dart +++ b/lib/src/services/mixins/mosque_helpers_mixins.dart @@ -178,6 +178,20 @@ mixin MosqueHelpersMixin on ChangeNotifier { return salahIndex; } + /// return the upcoming salah index + /// return -1 in case of issue(invalid times format) + int nextSalahAfterIqamaIndex() { + final now = mosqueDate(); + final nextSalah = actualIqamaTimes().firstWhere( + (element) => element.isAfter(now), + orElse: () => actualIqamaTimes().first, + ); + var salahIndex = actualIqamaTimes().indexOf(nextSalah); + if (salahIndex > 4) salahIndex = 0; + if (salahIndex < 0) salahIndex = 4; + return salahIndex; + } + /// the duration until the next salah Duration nextSalahAfter() { final now = mosqueDate(); diff --git a/lib/src/state_management/manual_app_update/manual_update_notifier.dart b/lib/src/state_management/manual_app_update/manual_update_notifier.dart new file mode 100644 index 000000000..34b671b97 --- /dev/null +++ b/lib/src/state_management/manual_app_update/manual_update_notifier.dart @@ -0,0 +1,256 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/widgets.dart'; +import 'package:mawaqit/src/const/constants.dart'; +import 'package:mawaqit/src/state_management/manual_app_update/manual_update_state.dart'; +import 'package:mawaqit/src/state_management/on_boarding/on_boarding_notifier.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter/services.dart'; +import 'dart:io'; + +import 'package:upgrader/upgrader.dart'; + +final manualUpdateNotifierProvider = AsyncNotifierProvider(() { + return ManualUpdateNotifier(); +}); + +class ManualUpdateNotifier extends AsyncNotifier { + static const platform = MethodChannel(TurnOnOffTvConstant.kNativeMethodsChannel); + late final Dio _dio; + CancelToken? _cancelToken; + + @override + Future build() async { + _dio = Dio(); + return const UpdateState(); + } + + void cancelUpdate() { + _cancelToken?.cancel('Update cancelled by user'); + _cancelToken = null; + + _cleanupDownloadedFile(); + + state = const AsyncValue.data(UpdateState( + status: UpdateStatus.cancelled, + message: 'Update cancelled', + )); + } + + void _cleanupDownloadedFile() { + final filePath = state.value?.filePath; + if (filePath != null) { + try { + final file = File(filePath); + if (file.existsSync()) file.deleteSync(); + } catch (e) { + debugPrint('Error cleaning up file: $e'); + } + } + } + + Future checkForUpdates( + String currentVersion, + String languageCode, { + bool? isDeviceRooted, + }) async { + try { + state = const AsyncValue.data(UpdateState(status: UpdateStatus.checking, message: 'Checking updates...')); + + final hasUpdate = isDeviceRooted ?? false + ? await _isUpdateAvailableForRootedDevice(currentVersion) + : await _isUpdateAvailableStandard(languageCode); + + if (hasUpdate) { + final downloadUrl = await _getLatestReleaseUrl(); + state = AsyncValue.data(state.value!.copyWith( + status: UpdateStatus.available, + message: 'Update available', + downloadUrl: downloadUrl, + )); + } else { + state = const AsyncValue.data(UpdateState( + status: UpdateStatus.notAvailable, + message: 'You are using the latest version', + )); + } + } catch (e, st) { + state = AsyncValue.error('Failed to check updates', st); + } + } + + Future _isUpdateAvailableStandard(String languageCode) async { + final upgrader = Upgrader( + messages: UpgraderMessages(code: languageCode), + ); + await upgrader.initialize(); + return upgrader.isUpdateAvailable(); + } + + Future _isUpdateAvailableForRootedDevice(String currentVersion) async { + final releases = await _fetchReleases(); + final latestRelease = releases.firstWhere( + (release) => release['prerelease'] == false, + orElse: () => throw Exception('No stable release found'), + ); + + final latestVersion = latestRelease['tag_name'].toString(); + return _compareVersions(latestVersion, currentVersion) > 0; + } + + Future> _fetchReleases() async { + final response = await _dio.get( + ManualUpdateConstant.githubApiBaseUrl, + options: Options( + headers: {'Accept': ManualUpdateConstant.githubAcceptHeader}, + ), + ); + + if (response.statusCode != 200) { + throw Exception('Failed to fetch releases: ${response.statusCode}'); + } + + return response.data as List; + } + + Future downloadAndInstallUpdate() async { + final downloadUrl = state.value?.downloadUrl; + if (downloadUrl == null) return; + + try { + _cancelToken = CancelToken(); + + state = AsyncValue.data(state.value!.copyWith( + status: UpdateStatus.downloading, + message: 'Downloading update...', + )); + + final filePath = await _downloadApk(downloadUrl); + + // Check if cancelled after download + if (_cancelToken?.isCancelled ?? false) return; + + state = AsyncValue.data(state.value!.copyWith( + status: UpdateStatus.installing, + message: 'Installing update...', + filePath: filePath, + )); + + await _installApk(filePath); + + state = AsyncValue.data(state.value!.copyWith( + status: UpdateStatus.completed, + message: 'Update completed successfully', + )); + } catch (e, st) { + if (e is DioException && e.type == DioExceptionType.cancel) { + return; // Already handled by cancelUpdate + } + + _handleUpdateError(e, st); + } finally { + _cancelToken = null; + } + } + + void _handleUpdateError(Object e, StackTrace st) { + _cleanupDownloadedFile(); + + state = AsyncValue.data(state.value!.copyWith( + status: UpdateStatus.error, + message: 'Update failed: ${e.toString()}', + )); + } + + Future _getLatestReleaseUrl() async { + try { + final response = await _dio.get( + ManualUpdateConstant.githubApiBaseUrl, + options: Options( + headers: {'Accept': ManualUpdateConstant.githubAcceptHeader}, + validateStatus: (status) => status! < 500, + ), + ); + + if (response.statusCode == 200) { + final releases = response.data as List; + final latestRelease = releases.firstWhere( + (release) => release['prerelease'] == false, + orElse: () => throw Exception('No stable release found'), + ); + + final assets = latestRelease['assets'] as List; + final apkAsset = assets.firstWhere( + (asset) => asset['name'].toString().endsWith('.apk'), + orElse: () => throw Exception('No APK found in latest release'), + ); + + return apkAsset['browser_download_url']; + } + throw Exception('Failed to fetch releases: ${response.statusCode}'); + } catch (e) { + throw Exception('Error fetching latest release: $e'); + } + } + + Future _downloadApk(String url) async { + try { + final dir = await getTemporaryDirectory(); + final filePath = '${dir.path}/update.apk'; + + await _dio.download( + url, + filePath, + cancelToken: _cancelToken, + onReceiveProgress: (received, total) { + if (total != -1) { + final progress = received / total; + state = AsyncValue.data(state.value!.copyWith(progress: progress)); + } + }, + ); + + return filePath; + } catch (e) { + if (e is DioException && e.type == DioExceptionType.cancel) { + throw e; // Rethrow cancellation to be handled in downloadAndInstallUpdate + } + throw Exception('Error downloading APK: $e'); + } + } + + Future _installApk(String filePath) async { + try { + final file = File(filePath); + if (!await file.exists()) { + throw Exception('APK file not found'); + } + + final result = await platform.invokeMethod('installApk', { + 'filePath': filePath, + }); + + await file.delete(); + + if (result != true) { + throw Exception('Installation failed'); + } + } catch (e) { + throw Exception('Error installing APK: $e'); + } + } + + int _compareVersions(String v1, String v2) { + final version1 = v1.replaceAll('v', '').split('.'); + final version2 = v2.replaceAll('v', '').split('.'); + + for (var i = 0; i < version1.length && i < version2.length; i++) { + final num1 = int.parse(version1[i]); + final num2 = int.parse(version2[i]); + if (num1 != num2) { + return num1 - num2; + } + } + return version1.length - version2.length; + } +} diff --git a/lib/src/state_management/manual_app_update/manual_update_state.dart b/lib/src/state_management/manual_app_update/manual_update_state.dart new file mode 100644 index 000000000..3dc37ee3d --- /dev/null +++ b/lib/src/state_management/manual_app_update/manual_update_state.dart @@ -0,0 +1,59 @@ +import 'package:equatable/equatable.dart'; + +enum UpdateStatus { + initial, + checking, + available, + notAvailable, + downloading, + installing, + completed, + error, + cancelled, +} + +class UpdateState extends Equatable { + final UpdateStatus status; + final double? progress; + final String? message; + final String? error; + final String? downloadUrl; + final String? filePath; + + const UpdateState({ + this.status = UpdateStatus.initial, + this.progress, + this.message, + this.error, + this.downloadUrl, + this.filePath, + }); + + UpdateState copyWith({ + UpdateStatus? status, + double? progress, + String? message, + String? error, + String? downloadUrl, + String? filePath, + }) { + return UpdateState( + status: status ?? this.status, + progress: progress ?? this.progress, + message: message ?? this.message, + error: error ?? this.error, + downloadUrl: downloadUrl ?? this.downloadUrl, + filePath: filePath ?? this.filePath, + ); + } + + @override + List get props => [ + status, + progress, + message, + error, + downloadUrl, + filePath, + ]; +} diff --git a/lib/src/state_management/quran/quran/quran_notifier.dart b/lib/src/state_management/quran/quran/quran_notifier.dart index 62e24a97d..c3c0892fb 100644 --- a/lib/src/state_management/quran/quran/quran_notifier.dart +++ b/lib/src/state_management/quran/quran/quran_notifier.dart @@ -83,6 +83,14 @@ class QuranNotifier extends AsyncNotifier { } }); } + + void exitQuranMode() { + try { + state = AsyncData(state.value!.copyWith(mode: QuranMode.none)); + } catch (err, stack) { + state = AsyncError(err, stack); + } + } } final quranNotifierProvider = AsyncNotifierProvider(QuranNotifier.new); diff --git a/lib/src/state_management/quran/reading/auto_reading/auto_reading_notifier.dart b/lib/src/state_management/quran/reading/auto_reading/auto_reading_notifier.dart new file mode 100644 index 000000000..1ebe00963 --- /dev/null +++ b/lib/src/state_management/quran/reading/auto_reading/auto_reading_notifier.dart @@ -0,0 +1,201 @@ +import 'dart:async'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:mawaqit/src/state_management/quran/reading/auto_reading/auto_reading_state.dart'; + +class AutoScrollNotifier extends AutoDisposeNotifier { + Timer? _autoScrollTimer; + Timer? _hideTimer; + late final ScrollController scrollController; + + @override + AutoScrollState build() { + scrollController = ScrollController(); + ref.onDispose(() { + _autoScrollTimer?.cancel(); + _hideTimer?.cancel(); + scrollController.dispose(); + }); + return AutoScrollState( + scrollController: scrollController, + ); + } + + Future jumpToCurrentPage(int currentPage, double pageHeight) async { + if (scrollController.hasClients) { + final offset = (currentPage - 1) * pageHeight; + scrollController.jumpTo(offset); + } + } + + void toggleAutoScroll(int currentPage, double pageHeight) { + if (state.isAutoScrolling) { + stopAutoScroll(); + } else { + startAutoScroll(currentPage, pageHeight); + } + } + + Future startAutoScroll(int currentPage, double pageHeight) async { + _autoScrollTimer?.cancel(); + + // Store the current scroll position before making changes + double? currentScrollPosition; + if (scrollController.hasClients) { + currentScrollPosition = scrollController.offset; + } + + state = state.copyWith( + isSinglePageView: true, + ); + + // Ensure the ListView is built + await Future.delayed(Duration(milliseconds: 50)); + + // Restore the previous scroll position if it exists, + // otherwise jump to the current page + if (scrollController.hasClients) { + if (currentScrollPosition != null) { + scrollController.jumpTo(currentScrollPosition); + } else { + final offset = (currentPage - 1) * pageHeight; + scrollController.jumpTo(offset); + } + } + + _startScrolling(); + } + + void _startScrolling() { + // Only start scrolling if we're in playing state + if (!state.isPlaying) return; + + state = state.copyWith( + isPlaying: true, + ); + + const duration = Duration(milliseconds: 50); + _autoScrollTimer = Timer.periodic(duration, (timer) { + // Check if we should be scrolling + if (!state.isPlaying) { + timer.cancel(); + return; + } + + if (scrollController.hasClients) { + final maxScroll = scrollController.position.maxScrollExtent; + final currentScroll = scrollController.offset; + final delta = state.autoScrollSpeed; + + if (currentScroll >= maxScroll) { + stopAutoScroll(); + } else { + scrollController.jumpTo(currentScroll + delta); + } + } + }); + } + + void stopAutoScroll() { + _autoScrollTimer?.cancel(); + _autoScrollTimer = null; + state = state.copyWith( + isSinglePageView: false, + ); + } + + void changeSpeed(double newSpeed) { + state = state.copyWith(autoScrollSpeed: newSpeed.clamp(0.1, 5.0)); + } + + void showControls() { + state = state.copyWith(isVisible: true); + startHideTimer(); + } + + void startHideTimer() { + _hideTimer?.cancel(); + _hideTimer = Timer(Duration(seconds: 12), () { + state = state.copyWith(isVisible: false); + }); + } + + void changeFontSize() { + double newFontSize = state.fontSize + 0.2; + if (newFontSize > state.maxFontSize) newFontSize = 1.0; + state = state.copyWith(fontSize: newFontSize); + } + + void increaseSpeed(int currentPage, double pageHeight) { + double newSpeed = state.autoScrollSpeed + 0.1; + if (newSpeed > 5.0) newSpeed = 5.0; + state = state.copyWith(autoScrollSpeed: newSpeed); + if (state.isAutoScrolling) { + _startScrolling(); // Only restart the scrolling timer + } + } + + void decreaseSpeed(int currentPage, double pageHeight) { + double newSpeed = state.autoScrollSpeed - 0.1; + if (newSpeed < 0.1) newSpeed = 0.1; + state = state.copyWith(autoScrollSpeed: newSpeed); + if (state.isAutoScrolling) { + _startScrolling(); // Only restart the scrolling timer + } + } + + void increaseFontSize() { + double newFontSize = state.fontSize + 0.2; + if (newFontSize > state.maxFontSize) newFontSize = state.maxFontSize; + state = state.copyWith(fontSize: newFontSize); + } + + void decreaseFontSize() { + double newFontSize = state.fontSize - 0.2; + if (newFontSize < 1.0) newFontSize = 1.0; + state = state.copyWith(fontSize: newFontSize); + } + + void cycleFontSize() { + if (state.fontSize >= state.maxFontSize) { + state = state.copyWith(fontSize: 1.0); + } else { + state = state.copyWith(fontSize: state.fontSize + 0.2); + } + } + + void cycleSpeed(int currentPage, double pageHeight) { + double newSpeed; + if (state.autoScrollSpeed >= 0.5) { + newSpeed = 0.1; + } else { + newSpeed = state.autoScrollSpeed + 0.1; + } + state = state.copyWith(autoScrollSpeed: newSpeed); + if (state.isAutoScrolling) { + _startScrolling(); // Only restart the scrolling timer + } + } + + void pauseAutoScroll() { + _autoScrollTimer?.cancel(); + _autoScrollTimer = null; + state = state.copyWith( + isPlaying: false, + ); + } + + void resumeAutoScroll() { + if (!state.isPlaying) { + state = state.copyWith( + isPlaying: true, + ); + _startScrolling(); + } + } +} + +final autoScrollNotifierProvider = AutoDisposeNotifierProvider( + AutoScrollNotifier.new, +); diff --git a/lib/src/state_management/quran/reading/auto_reading/auto_reading_state.dart b/lib/src/state_management/quran/reading/auto_reading/auto_reading_state.dart new file mode 100644 index 000000000..e237e9379 --- /dev/null +++ b/lib/src/state_management/quran/reading/auto_reading/auto_reading_state.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; + +class AutoScrollState { + final bool isSinglePageView; + final double autoScrollSpeed; + final bool isVisible; + final double fontSize; + final double maxFontSize; + final ScrollController scrollController; + final bool isPlaying; + + AutoScrollState({ + required this.scrollController, + this.isSinglePageView = false, + this.autoScrollSpeed = 0.1, + this.isVisible = true, + this.fontSize = 1.0, + this.maxFontSize = 3.0, + this.isPlaying = false, + }); + + bool get isAutoScrolling => isSinglePageView; + + bool get showSpeedControl => !isSinglePageView; + + AutoScrollState copyWith({ + bool? isSinglePageView, + double? autoScrollSpeed, + bool? isVisible, + double? fontSize, + double? maxFontSize, + ScrollController? scrollController, + bool? isPlaying, + }) { + return AutoScrollState( + isSinglePageView: isSinglePageView ?? this.isSinglePageView, + autoScrollSpeed: autoScrollSpeed ?? this.autoScrollSpeed, + isVisible: isVisible ?? this.isVisible, + fontSize: fontSize ?? this.fontSize, + maxFontSize: maxFontSize ?? this.maxFontSize, + scrollController: scrollController ?? this.scrollController, + isPlaying: isPlaying ?? this.isPlaying, + ); + } + + @override + String toString() { + return 'AutoScrollState(' + 'isSinglePageView: $isSinglePageView, ' + 'autoScrollSpeed: $autoScrollSpeed, ' + 'isVisible: $isVisible, ' + 'fontSize: $fontSize, ' + 'maxFontSize: $maxFontSize, ' + 'isAutoScrolling: $isAutoScrolling, ' + 'showSpeedControl: $showSpeedControl,' + 'isPlaying: $isPlaying' + ')'; + } +} 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 6c4cfaa5e..d6fd02e25 100644 --- a/lib/src/state_management/quran/reading/quran_reading_notifer.dart +++ b/lib/src/state_management/quran/reading/quran_reading_notifer.dart @@ -165,6 +165,14 @@ class QuranReadingNotifier extends AutoDisposeAsyncNotifier { return ""; } + Future toggleRotation() async { + state = await AsyncValue.guard(() async { + return state.value!.copyWith( + isRotated: !state.value!.isRotated, + ); + }); + } + Future> _loadSvgs({required MoshafType moshafType}) async { final repository = await ref.read(quranReadingRepositoryProvider.future); return repository.loadAllSvgs(moshafType); diff --git a/lib/src/state_management/quran/reading/quran_reading_state.dart b/lib/src/state_management/quran/reading/quran_reading_state.dart index e7f795038..cb797dacf 100644 --- a/lib/src/state_management/quran/reading/quran_reading_state.dart +++ b/lib/src/state_management/quran/reading/quran_reading_state.dart @@ -11,6 +11,7 @@ class QuranReadingState extends Equatable { final PageController pageController; final List suwar; final String currentSurahName; + final bool isRotated; QuranReadingState({ required this.currentJuz, @@ -20,6 +21,7 @@ class QuranReadingState extends Equatable { required this.pageController, required this.suwar, required this.currentSurahName, + this.isRotated = false, }); QuranReadingState copyWith({ @@ -31,6 +33,7 @@ class QuranReadingState extends Equatable { PageController? pageController, List? suwar, String? currentSurahName, + bool? isRotated, }) { return QuranReadingState( currentJuz: currentJuz ?? this.currentJuz, @@ -40,6 +43,7 @@ class QuranReadingState extends Equatable { pageController: pageController ?? this.pageController, suwar: suwar ?? this.suwar, currentSurahName: currentSurahName ?? this.currentSurahName, + isRotated: isRotated ?? this.isRotated, ); } @@ -52,6 +56,7 @@ class QuranReadingState extends Equatable { pageController, suwar, currentSurahName, + isRotated, ]; int get totalPages => svgs.length; @@ -59,6 +64,7 @@ class QuranReadingState extends Equatable { @override String toString() { return 'QuranReadingState{currentJuz: $currentJuz, currentSurah: $currentSurah, currentPage: $currentPage, ' - 'svgs: ${svgs.length}, totalPages: $totalPages}'; + 'svgs: ${svgs.length}, totalPages: $totalPages, pageController: $pageController, suwar: ${suwar.length}, ' + 'isRotated: $isRotated}'; } } diff --git a/lib/src/widgets/MawaqitDrawer.dart b/lib/src/widgets/MawaqitDrawer.dart index bfd7fddc9..6940a800d 100644 --- a/lib/src/widgets/MawaqitDrawer.dart +++ b/lib/src/widgets/MawaqitDrawer.dart @@ -20,6 +20,7 @@ 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/routes/routes_constant.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'; @@ -32,7 +33,7 @@ import 'package:mawaqit/src/pages/SettingScreen.dart'; import 'package:mawaqit/src/state_management/quran/quran/quran_notifier.dart'; import '../pages/quran/page/quran_mode_selection_screen.dart'; -import '../pages/quran/page/quran_reading_screen.dart'; +import 'package:mawaqit/src/pages/quran/reading/quran_reading_screen.dart'; import '../state_management/quran/quran/quran_state.dart'; class MawaqitDrawer extends ConsumerWidget { @@ -174,20 +175,19 @@ class MawaqitDrawer extends ConsumerWidget { onTap: () async { await ref.read(quranNotifierProvider.notifier).getSelectedMode(); final state = ref.read(quranNotifierProvider); + Navigator.pop(context); + switch (state.value!.mode) { case QuranMode.reading: log('quran: MawaqitDrawer: build: quranNotifierProvider: mode: reading'); - Navigator.pop(context); - AppRouter.push(QuranReadingScreen()); + Navigator.pushNamed(context, Routes.quranReading); break; case QuranMode.listening: log('quran: MawaqitDrawer: build: quranNotifierProvider: mode: listening'); - Navigator.pop(context); - AppRouter.push(ReciterSelectionScreen.withoutSurahName()); + Navigator.pushNamed(context, Routes.quranReciter); break; case QuranMode.none: - Navigator.pop(context); - AppRouter.push(QuranModeSelection()); + Navigator.pushNamed(context, Routes.quranModeSelection); break; } }, diff --git a/lib/src/widgets/manual_update_dialog.dart b/lib/src/widgets/manual_update_dialog.dart new file mode 100644 index 000000000..3e3d05cc3 --- /dev/null +++ b/lib/src/widgets/manual_update_dialog.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mawaqit/i18n/l10n.dart'; +import 'package:mawaqit/src/state_management/app_update/app_update_notifier.dart'; +import 'package:mawaqit/src/state_management/manual_app_update/manual_update_notifier.dart'; +import 'package:mawaqit/src/state_management/on_boarding/on_boarding_notifier.dart'; + +import '../state_management/manual_app_update/manual_update_state.dart'; + +class UpdateDialogMessages { + static Map getLocalizedMessage(BuildContext context) { + return { + UpdateStatus.checking: S.of(context).checkingForUpdates, + UpdateStatus.available: S.of(context).updateAvailable, + UpdateStatus.notAvailable: S.of(context).usingLatestVersion, + UpdateStatus.downloading: S.of(context).downloadingUpdate, + UpdateStatus.installing: S.of(context).installingUpdate, + UpdateStatus.completed: S.of(context).updateCompleted, + UpdateStatus.cancelled: S.of(context).updateCancelled, + UpdateStatus.error: S.of(context).updateFailed, + }; + } +} + +class UpdateDialog { + static void show(BuildContext context, WidgetRef ref) { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => Consumer( + builder: (context, ref, child) { + final updateState = ref.watch(manualUpdateNotifierProvider); + return AlertDialog( + title: Text(S.of(context).updateAvailable), + content: _buildDialogContent(context, updateState), + actions: _buildDialogActions(context, ref), + ); + }, + ), + ); + } + + static Widget _buildDialogContent(BuildContext context, AsyncValue updateState) { + final localizedMessages = UpdateDialogMessages.getLocalizedMessage(context); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(localizedMessages[updateState.value?.status] ?? S.of(context).wouldYouLikeToUpdate), + if (updateState.value?.progress != null) ...[ + const SizedBox(height: 16), + LinearProgressIndicator( + value: updateState.value?.progress, + backgroundColor: Theme.of(context).colorScheme.background, + valueColor: AlwaysStoppedAnimation( + Theme.of(context).colorScheme.primary, + ), + ), + const SizedBox(height: 8), + Text( + '${(updateState.value!.progress! * 100).toStringAsFixed(1)}%', + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ], + ); + } + + static List _buildDialogActions(BuildContext context, WidgetRef ref) { + return [ + TextButton( + onPressed: () { + ref.read(manualUpdateNotifierProvider.notifier).cancelUpdate(); + Navigator.pop(context); + }, + child: Text(S.of(context).cancel), + ), + TextButton( + onPressed: () => _handleUpdateAction(context, ref), + child: Text(S.of(context).update), + ), + ]; + } + + static void _handleUpdateAction(BuildContext context, WidgetRef ref) { + final isDeviceRooted = ref.read(onBoardingProvider).maybeWhen( + orElse: () => false, + data: (value) => value.isRootedDevice, + ); + + if (isDeviceRooted) { + ref.read(manualUpdateNotifierProvider.notifier).downloadAndInstallUpdate(); + } else { + ref.read(appUpdateProvider.notifier).openStore(); + Navigator.pop(context); + } + } + + static void showNoUpdateAvailableDialog(BuildContext context) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(S.of(context).noUpdates), + content: Text(S.of(context).usingLatestVersion), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(S.of(context).ok), + ), + ], + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index d9028a45a..1d5d7ad68 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -68,7 +68,7 @@ dependencies: store_redirect: ^2.0.1 connectivity_plus: ^4.0.0 uni_links: ^0.5.1 - + # TOOLS google_fonts: ^4.0.4 device_info_plus: ^9.0.2 @@ -143,6 +143,7 @@ dependencies: # automatic scrolling scroll_to_index: ^3.0.1 + fast_cached_network_image: ^1.2.0 dev_dependencies: flutter_test: @@ -165,7 +166,6 @@ dev_dependencies: dependency_overrides: win32: 5.0.5 rive: ^0.9.1 - # The following section is specific to Flutter. flutter: generate: true From 8819c9226f9ff64a84350e4220cd6d577133f3d7 Mon Sep 17 00:00:00 2001 From: Ibrahim ZEHHAF <97339607+ibrahim-zehhaf-mawaqit@users.noreply.github.com> Date: Wed, 20 Nov 2024 21:08:26 +0100 Subject: [PATCH 10/26] Update pubspec.yaml --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 1d5d7ad68..6e56dc273 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.16.1+1 +version: 1.17.0+1 environment: From 073c99c06914e11e14b603d46168cc2cd3e25b71 Mon Sep 17 00:00:00 2001 From: Ibrahim ZEHHAF <97339607+ibrahim-zehhaf-mawaqit@users.noreply.github.com> Date: Thu, 21 Nov 2024 09:52:22 +0100 Subject: [PATCH 11/26] New Crowdin updates (#1433) * New translations intl_en.arb (fr) * New translations intl_en.arb (fr) --- lib/l10n/intl_fr.arb | 77 ++++++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 39 deletions(-) diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index e3df0b33f..d2040c0b0 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": "{mosqueId} n'est pas un id de mosquée valide", + "mosqueIdIsNotValid": "Désolé, le {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 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.", + "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.", "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": "Essayez: 256, c'est l'ID de 'Grande Mosquée de Paris'", + "selectWithMosqueId": "Votre ID se trouve dans votre espace utilisateur mawaqit.net", "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 ﷺ 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é\".", + "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é\".", "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-akhir", - "jumadaAlula": "Jumada al-Ula", + "rabiAlthani": "Rabi' al-thani", + "jumadaAlula": "Joumada al-oula", "jumadaAlakhirah": "Joumada al-akhirah", "rajab": "Rajab", "shaban": "Chaabane", "ramadan": "Ramadan", - "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\"", + "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\"", "salatKhayrMinaNawm": "Assalatou khayroun mina nawm", "salatElEid": "Salat Al Aïd", "webView": "Forcer l'ancienne version (Online)", @@ -290,13 +290,27 @@ } } }, + "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": "Warsh", + "warsh": "Guerrier", + "favorites": "Favoris", "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", @@ -321,11 +335,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}", @@ -338,21 +352,7 @@ } } }, - "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" - } - } - }, + "surahSelector": "Sélectionner une sourat", "checkForUpdates": "Vérifier les mises à jour", "checkForNewVersion": "Vérifiez si une nouvelle version est disponible", "wouldYouLikeToUpdate": "Souhaitez-vous mettre à jour l'application ?", @@ -365,5 +365,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 From e441f537c8ee27fe788da903c494330f962c84a8 Mon Sep 17 00:00:00 2001 From: Ibrahim ZEHHAF <97339607+ibrahim-zehhaf-mawaqit@users.noreply.github.com> Date: Thu, 21 Nov 2024 09:52:59 +0100 Subject: [PATCH 12/26] Update pubspec.yaml --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 6e56dc273..2a6ac6fbf 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.0+1 +version: 1.17.1+1 environment: 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 13/26] 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: Tue, 26 Nov 2024 15:17:03 +0100 Subject: [PATCH 14/26] Fix/ Manual update rooted (#1439) * fix rooted update check & add spiner * change version for test * show error failed * add internet check & disable update button * fix trailing space --- .../main/kotlin/com/flyweb/MainActivity.kt | 62 +++++++++---------- lib/l10n/intl_ar.arb | 17 ++++- lib/l10n/intl_en.arb | 18 +++++- lib/l10n/intl_fr.arb | 17 ++++- lib/src/pages/SettingScreen.dart | 54 +++++++++++++--- .../manual_update_notifier.dart | 33 ++++++---- .../manual_update_state.dart | 8 +++ lib/src/widgets/manual_update_dialog.dart | 25 +++++++- pubspec.yaml | 2 +- 9 files changed, 177 insertions(+), 59 deletions(-) diff --git a/android/app/src/main/kotlin/com/flyweb/MainActivity.kt b/android/app/src/main/kotlin/com/flyweb/MainActivity.kt index c2334df43..f4e5da4fd 100644 --- a/android/app/src/main/kotlin/com/flyweb/MainActivity.kt +++ b/android/app/src/main/kotlin/com/flyweb/MainActivity.kt @@ -63,39 +63,35 @@ class MainActivity : FlutterActivity() { val isSuccess = clearDataRestart() result.success(isSuccess) } - "installApk" -> { - val filePath = call.argument("filePath") - if (filePath != null) { - AsyncTask.execute { - try { - // Check if file exists - val file = java.io.File(filePath) - if (!file.exists()) { - Log.e("APK_INSTALL", "APK file not found at path: $filePath") - result.error("FILE_NOT_FOUND", "APK file not found", null) - return@execute - } - // Check if device is rooted - if (!checkRoot()) { - Log.e("APK_INSTALL", "Device is not rooted") - result.error("NOT_ROOTED", "Device is not rooted", null) - return@execute - } - - - - executeCommand(listOf("pm install -r -d $filePath"), result) - - result.success("Installation initiated") - } catch (e: Exception) { - Log.e("APK_INSTALL", "Failed to install APK", e) - result.error("INSTALL_FAILED", e.message, null) - } - } - } else { - result.error("INVALID_PATH", "File path is null", null) - } -} + "installApk" -> { + val filePath = call.argument("filePath") + if (filePath != null) { + AsyncTask.execute { + try { + // Check if file exists + val file = java.io.File(filePath) + if (!file.exists()) { + Log.e("APK_INSTALL", "APK file not found at path: $filePath") + result.error("FILE_NOT_FOUND", "APK file not found", null) + return@execute + } + // Check if device is rooted + if (!checkRoot()) { + Log.e("APK_INSTALL", "Device is not rooted") + result.error("NOT_ROOTED", "Device is not rooted", null) + return@execute + } + val commands = listOf("pm install -r -d $filePath") + executeCommand(commands, result) + } catch (e: Exception) { + Log.e("APK_INSTALL", "Failed to install APK", e) + result.error("INSTALL_FAILED", e.message, null) + } + } + } else { + result.error("INVALID_PATH", "File path is null", null) + } + } else -> result.notImplemented() } } diff --git a/lib/l10n/intl_ar.arb b/lib/l10n/intl_ar.arb index 5c8460326..54e7020c4 100644 --- a/lib/l10n/intl_ar.arb +++ b/lib/l10n/intl_ar.arb @@ -364,5 +364,20 @@ "downloadingUpdate": "جارٍ تنزيل التحديث...", "installingUpdate": "جارٍ تثبيت التحديث...", "updateCompletedSuccessfully": "تم التحديث بنجاح", - "updateFailed": "فشل التحديث" + "updateFailed": "فشل التحديث", + "checkInternetUpdate": "يجب عليك الاتصال بالإنترنت للتحقق من وجود تحديثات جديدة", + "appUpdateAvailable": "تطبيقك يعمل بالإصدار {currentVersion}. تحديث جديد (الإصدار {updatedVersion}) متوفر مع أحدث الميزات والتحسينات.", + "@appUpdateAvailable": { + "description": "نص بديل لعرض رسالة التحديث المتاح", + "placeholders": { + "currentVersion": { + "type": "String", + "example": "1" + }, + "updatedVersion": { + "type": "String", + "example": "604" + } + } + } } diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 5f3d806c5..51c34c6a4 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -364,6 +364,22 @@ "downloadingUpdate": "Downloading update...", "installingUpdate": "Installing update...", "updateCompletedSuccessfully": "Update completed successfully", - "updateFailed": "Update failed" + "updateFailed": "Update failed", + "checkInternetUpdate": "You must connect to internet to check for new updates", + "appUpdateAvailable": "Your app is running version {currentVersion}. A new update (version {updatedVersion}) is available with the latest features and improvements.", + "@appUpdateAvailable": { + "description": "Placeholder text for displaying update available message", + "placeholders": { + "currentVersion": { + "type": "String", + "example": "1" + }, + "updatedVersion": { + "type": "String", + "example": "604" + } + } + } + } diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index d2040c0b0..2bce6e640 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -364,5 +364,20 @@ "downloadingUpdate": "Téléchargement de la mise à jour...", "installingUpdate": "Installation de la mise à jour...", "updateCompletedSuccessfully": "Mise à jour terminée avec succès", - "updateFailed": "Échec de la mise à jour" + "updateFailed": "Échec de la mise à jour", + "checkInternetUpdate": "Vous devez être connecté à Internet pour vérifier les nouvelles mises à jour", + "appUpdateAvailable": "Votre application utilise la version {currentVersion}. Une nouvelle mise à jour (version {updatedVersion}) est disponible avec les dernières fonctionnalités et améliorations.", + "@appUpdateAvailable": { + "description": "Texte de remplacement pour afficher le message de mise à jour disponible", + "placeholders": { + "currentVersion": { + "type": "String", + "example": "1" + }, + "updatedVersion": { + "type": "String", + "example": "604" + } + } + } } \ No newline at end of file diff --git a/lib/src/pages/SettingScreen.dart b/lib/src/pages/SettingScreen.dart index b414cfd49..01e6a6080 100644 --- a/lib/src/pages/SettingScreen.dart +++ b/lib/src/pages/SettingScreen.dart @@ -289,14 +289,52 @@ class _SettingScreenState extends ConsumerState { _SettingItem( title: S.of(context).checkForUpdates, subtitle: S.of(context).checkForNewVersion, - icon: const Icon(Icons.system_update, size: 35), - onTap: () async { - var softwareFuture = await PackageInfo.fromPlatform(); - - ref - .read(manualUpdateNotifierProvider.notifier) - .checkForUpdates(softwareFuture.version, context.read().appLocal.languageCode); - }, + icon: ref.watch(manualUpdateNotifierProvider).isLoading + ? const SizedBox( + width: 35, + height: 35, + child: CircularProgressIndicator(), + ) + : const Icon(Icons.system_update, size: 35), + onTap: ref.watch(manualUpdateNotifierProvider).isLoading + ? null + : () async { + await ref.read(connectivityProvider.notifier).checkInternetConnection(); + ref.watch(connectivityProvider).maybeWhen( + orElse: () { + showCheckInternetDialog( + context: context, + onRetry: () { + AppRouter.pop(); + }, + title: checkInternet, + content: S.of(context).checkInternetUpdate, + ); + }, + data: (isConnectedToInternet) async { + if (isConnectedToInternet == ConnectivityStatus.disconnected) { + showCheckInternetDialog( + context: context, + onRetry: () { + AppRouter.pop(); + }, + title: checkInternet, + content: S.of(context).checkInternetUpdate, + ); + } else { + var softwareFuture = await PackageInfo.fromPlatform(); + final isDeviceRooted = ref.watch(onBoardingProvider).maybeWhen( + orElse: () => false, + data: (value) => value.isRootedDevice, + ); + ref.read(manualUpdateNotifierProvider.notifier).checkForUpdates( + softwareFuture.version, + context.read().appLocal.languageCode, + isDeviceRooted); + } + }, + ); + }, ), ], ), diff --git a/lib/src/state_management/manual_app_update/manual_update_notifier.dart b/lib/src/state_management/manual_app_update/manual_update_notifier.dart index 34b671b97..916474047 100644 --- a/lib/src/state_management/manual_app_update/manual_update_notifier.dart +++ b/lib/src/state_management/manual_app_update/manual_update_notifier.dart @@ -51,31 +51,35 @@ class ManualUpdateNotifier extends AsyncNotifier { Future checkForUpdates( String currentVersion, - String languageCode, { - bool? isDeviceRooted, - }) async { + String languageCode, + bool isDeviceRooted, + ) async { + state = const AsyncLoading(); try { - state = const AsyncValue.data(UpdateState(status: UpdateStatus.checking, message: 'Checking updates...')); - - final hasUpdate = isDeviceRooted ?? false + final hasUpdate = isDeviceRooted ? await _isUpdateAvailableForRootedDevice(currentVersion) : await _isUpdateAvailableStandard(languageCode); if (hasUpdate) { final downloadUrl = await _getLatestReleaseUrl(); - state = AsyncValue.data(state.value!.copyWith( + final latestVersion = await _getLatestVersion(); + + state = AsyncData(state.value!.copyWith( status: UpdateStatus.available, message: 'Update available', downloadUrl: downloadUrl, + currentVersion: currentVersion, + availableVersion: latestVersion, )); } else { - state = const AsyncValue.data(UpdateState( + state = AsyncData(UpdateState( status: UpdateStatus.notAvailable, message: 'You are using the latest version', + currentVersion: currentVersion, )); } } catch (e, st) { - state = AsyncValue.error('Failed to check updates', st); + state = AsyncError(e, st); } } @@ -93,7 +97,6 @@ class ManualUpdateNotifier extends AsyncNotifier { (release) => release['prerelease'] == false, orElse: () => throw Exception('No stable release found'), ); - final latestVersion = latestRelease['tag_name'].toString(); return _compareVersions(latestVersion, currentVersion) > 0; } @@ -113,6 +116,15 @@ class ManualUpdateNotifier extends AsyncNotifier { return response.data as List; } + Future _getLatestVersion() async { + final releases = await _fetchReleases(); + final latestRelease = releases.firstWhere( + (release) => release['prerelease'] == false, + orElse: () => throw Exception('No stable release found'), + ); + return latestRelease['tag_name'].toString(); + } + Future downloadAndInstallUpdate() async { final downloadUrl = state.value?.downloadUrl; if (downloadUrl == null) return; @@ -243,7 +255,6 @@ class ManualUpdateNotifier extends AsyncNotifier { int _compareVersions(String v1, String v2) { final version1 = v1.replaceAll('v', '').split('.'); final version2 = v2.replaceAll('v', '').split('.'); - for (var i = 0; i < version1.length && i < version2.length; i++) { final num1 = int.parse(version1[i]); final num2 = int.parse(version2[i]); diff --git a/lib/src/state_management/manual_app_update/manual_update_state.dart b/lib/src/state_management/manual_app_update/manual_update_state.dart index 3dc37ee3d..57437b57b 100644 --- a/lib/src/state_management/manual_app_update/manual_update_state.dart +++ b/lib/src/state_management/manual_app_update/manual_update_state.dart @@ -19,6 +19,8 @@ class UpdateState extends Equatable { final String? error; final String? downloadUrl; final String? filePath; + final String? currentVersion; + final String? availableVersion; const UpdateState({ this.status = UpdateStatus.initial, @@ -27,6 +29,8 @@ class UpdateState extends Equatable { this.error, this.downloadUrl, this.filePath, + this.currentVersion, + this.availableVersion, }); UpdateState copyWith({ @@ -36,6 +40,8 @@ class UpdateState extends Equatable { String? error, String? downloadUrl, String? filePath, + String? currentVersion, + String? availableVersion, }) { return UpdateState( status: status ?? this.status, @@ -44,6 +50,8 @@ class UpdateState extends Equatable { error: error ?? this.error, downloadUrl: downloadUrl ?? this.downloadUrl, filePath: filePath ?? this.filePath, + currentVersion: currentVersion ?? this.currentVersion, + availableVersion: availableVersion ?? this.availableVersion, ); } diff --git a/lib/src/widgets/manual_update_dialog.dart b/lib/src/widgets/manual_update_dialog.dart index 3e3d05cc3..6e0367e96 100644 --- a/lib/src/widgets/manual_update_dialog.dart +++ b/lib/src/widgets/manual_update_dialog.dart @@ -11,7 +11,8 @@ class UpdateDialogMessages { static Map getLocalizedMessage(BuildContext context) { return { UpdateStatus.checking: S.of(context).checkingForUpdates, - UpdateStatus.available: S.of(context).updateAvailable, +/* UpdateStatus.available: S.of(context).updateAvailable, + */ UpdateStatus.notAvailable: S.of(context).usingLatestVersion, UpdateStatus.downloading: S.of(context).downloadingUpdate, UpdateStatus.installing: S.of(context).installingUpdate, @@ -46,7 +47,21 @@ class UpdateDialog { return Column( mainAxisSize: MainAxisSize.min, children: [ - Text(localizedMessages[updateState.value?.status] ?? S.of(context).wouldYouLikeToUpdate), + if (updateState.value?.currentVersion != null && updateState.value?.availableVersion != null) ...[ + Text(S.of(context).appUpdateAvailable( + updateState.value?.currentVersion as String, updateState.value?.availableVersion as String)), + const SizedBox(height: 8), + ], + if (updateState.value?.status == UpdateStatus.error) + Text( + updateState.value?.message ?? S.of(context).updateFailed, + style: TextStyle(color: Theme.of(context).colorScheme.error), + textAlign: TextAlign.center, + ) + else + Text( + localizedMessages[updateState.value?.status] ?? S.of(context).wouldYouLikeToUpdate, + ), if (updateState.value?.progress != null) ...[ const SizedBox(height: 16), LinearProgressIndicator( @@ -67,6 +82,10 @@ class UpdateDialog { } static List _buildDialogActions(BuildContext context, WidgetRef ref) { + final updateState = ref.watch(manualUpdateNotifierProvider); + final isUpdating = + updateState.value?.status == UpdateStatus.downloading || updateState.value?.status == UpdateStatus.installing; + return [ TextButton( onPressed: () { @@ -76,7 +95,7 @@ class UpdateDialog { child: Text(S.of(context).cancel), ), TextButton( - onPressed: () => _handleUpdateAction(context, ref), + onPressed: isUpdating ? null : () => _handleUpdateAction(context, ref), child: Text(S.of(context).update), ), ]; diff --git a/pubspec.yaml b/pubspec.yaml index 2a6ac6fbf..6e56dc273 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.1+1 +version: 1.17.0+1 environment: From ad76a398b8d243f17ebece21d92f4fac76522f95 Mon Sep 17 00:00:00 2001 From: Ibrahim ZEHHAF <97339607+ibrahim-zehhaf-mawaqit@users.noreply.github.com> Date: Wed, 27 Nov 2024 07:18:23 +0100 Subject: [PATCH 15/26] Update pubspec.yaml --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 6e56dc273..4c05a4b3c 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.0+1 +version: 1.17.2+1 environment: From 76dfbce9a0b5e85a0233c3df60fb2e961bd070c2 Mon Sep 17 00:00:00 2001 From: Ibrahim ZEHHAF <97339607+ibrahim-zehhaf-mawaqit@users.noreply.github.com> Date: Wed, 27 Nov 2024 07:35:42 +0100 Subject: [PATCH 16/26] Update pubspec.yaml --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 4c05a4b3c..f797dae85 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.2+1 +version: 1.17.3+1 environment: From a651f91b250aa5357e15fc9acc339ab07e63fab8 Mon Sep 17 00:00:00 2001 From: Ibrahim ZEHHAF <97339607+ibrahim-zehhaf-mawaqit@users.noreply.github.com> Date: Wed, 27 Nov 2024 07:51:40 +0100 Subject: [PATCH 17/26] New Crowdin updates (#1435) * New translations intl_en.arb (ar) * New translations intl_en.arb (en) * New translations intl_en.arb (ar) * New translations intl_en.arb (pt) * New translations intl_en.arb (ar) * New translations intl_en.arb (fr) --- lib/l10n/intl_ar.arb | 58 +++++++++++++++++++++--------------------- lib/l10n/intl_en.arb | 11 ++++---- lib/l10n/intl_fr.arb | 2 +- lib/l10n/intl_pt.arb | 60 ++++++++++++++++++++++++++++++++++++++------ 4 files changed, 87 insertions(+), 44 deletions(-) diff --git a/lib/l10n/intl_ar.arb b/lib/l10n/intl_ar.arb index 54e7020c4..91efe82db 100644 --- a/lib/l10n/intl_ar.arb +++ b/lib/l10n/intl_ar.arb @@ -68,19 +68,19 @@ "@azkarList1": { "description": "سُـبْحانَ اللهِ، والحَمْـدُ لله، واللهُ أكْـبَر 33 مرة لا إِلَٰهَ إلاّ اللّهُ وَحْـدَهُ لا شريكَ لهُ، لهُ الملكُ ولهُ الحَمْد، وهُوَ على كُلّ شَيءٍ قَـدير" }, - "azkarList2": "بِسۡمِ ٱللَّهِ ٱلرَّحۡمَٰنِ ٱلرَّحِيمِ قُلۡ أَعُوذُ بِرَبِّ ٱلنَّاسِ ، مَلِكِ ٱلنَّاسِ ، إِلَٰهِ ٱلنَّاسِ ، مِن شَرِّ ٱلۡوَسۡوَاسِ ٱلۡخَنَّاسِ ، ٱلَّذِي يُوَسۡوِسُ فِي صُدُورِ ٱلنَّاسِ ، مِنَ ٱلۡجِنَّةِ وَٱلنَّاس", + "azkarList2": "", "@azkarList2": { "description": "بِسۡمِ ٱللَّهِ ٱلرَّحۡمَٰنِ ٱلرَّحِيمِ قُلۡ أَعُوذُ بِرَبِّ ٱلنَّاسِ ، مَلِكِ ٱلنَّاسِ ، إِلَٰهِ ٱلنَّاسِ ، مِن شَرِّ ٱلۡوَسۡوَاسِ ٱلۡخَنَّاسِ ، ٱلَّذِي يُوَسۡوِسُ فِي صُدُورِ ٱلنَّاسِ ، مِنَ ٱلۡجِنَّةِ وَٱلنَّاس" }, - "azkarList3": "بِسۡمِ ٱللَّهِ ٱلرَّحۡمَٰنِ ٱلرَّحِيمِ قُلۡ أَعُوذُ بِرَبِّ ٱلۡفَلَقِ ، مِن شَرِّ مَا خَلَقَ ، وَمِن شَرِّ غَاسِقٍ إِذَا وَقَبَ ، وَمِن شَرِ ٱلنَّفَّٰثَٰتِ فِي ٱلۡعُقَدِ ، وَمِن شَرِّ حَاسِدٍ إِذَا حَسَدَ", + "azkarList3": "", "@azkarList3": { "description": "بِسۡمِ ٱللَّهِ ٱلرَّحۡمَٰنِ ٱلرَّحِيمِقُلۡ أَعُوذُ بِرَبِّ ٱلۡفَلَقِ ، مِن شَرِّ مَا خَلَقَ ، وَمِن شَرِّ غَاسِقٍ إِذَا وَقَبَ ، وَمِن شَرِ ٱلنَّفَّٰثَٰتِ فِي ٱلۡعُقَدِ ، وَمِن شَرِّ حَاسِدٍ إِذَا حَسَدَ" }, - "azkarList4": "بِسۡمِ ٱللَّهِ ٱلرَّحۡمَٰنِ ٱلرَّحِيمِ قُلۡ هُوَ ٱللَّهُ أَحَدٌ ، ٱللَّهُ ٱلصَّمَدُ ، لَمۡ يَلِدۡ وَلَمۡ يُولَدۡ ، وَلَمۡ يَكُن لَّهُۥ كُفُوًا أَحَدُۢ", + "azkarList4": "", "@azkarList4": { "description": "بِسۡمِ ٱللَّهِ ٱلرَّحۡمَٰنِ ٱلرَّحِيمِ قُلۡ هُوَ ٱللَّهُ أَحَدٌ ، ٱللَّهُ ٱلصَّمَدُ ، لَمۡ يَلِدۡ وَلَمۡ يُولَدۡ ، وَلَمۡ يَكُن لَّهُۥ كُفُوًا أَحَدُۢ" }, - "azkarList5": "ٱللَّهُ لَآ إِلَٰهَ إِلَّا هُوَ ٱلۡحَيُّ ٱلۡقَيُّومُۚ لَا تَأۡخُذُهُۥ سِنَةٞ وَلَا نَوۡمٞۚ لَّهُۥ مَا فِي ٱلسَّمَٰوَٰتِ وَمَا فِي ٱلۡأَرۡضِۗ مَن ذَا ٱلَّذِي يَشۡفَعُ عِندَهُۥٓ إِلَّا بِإِذۡنِهِۦۚ يَعۡلَمُ مَا بَيۡنَ أَيۡدِيهِمۡ وَمَا خَلۡفَهُمۡۖ وَلَا يُحِيطُونَ بِشَيۡءٖ مِّنۡ عِلۡمِهِۦٓ إِلَّا بِمَا شَآءَۚ وَسِعَ كُرۡسِيُّهُ ٱلسَّمَٰوَٰتِ وَٱلۡأَرۡضَۖ وَلَا يَ‍ُٔودُهُۥ حِفۡظُهُمَاۚ وَهُوَ ٱلۡعَلِيُّ ٱلۡعَظِيمُ", + "azkarList5": "", "@azkarList5": { "description": "ٱللَّهُ لَآ إِلَٰهَ إِلَّا هُوَ ٱلۡحَيُّ ٱلۡقَيُّومُۚ لَا تَأۡخُذُهُۥ سِنَةٞ وَلَا نَوۡمٞۚ لَّهُۥ مَا فِي ٱلسَّمَٰوَٰتِ وَمَا فِي ٱلۡأَرۡضِۗ مَن ذَا ٱلَّذِي يَشۡفَعُ عِندَهُۥٓ إِلَّا بِإِذۡنِهِۦۚ يَعۡلَمُ مَا بَيۡنَ أَيۡدِيهِمۡ وَمَا خَلۡفَهُمۡۖ وَلَا يُحِيطُونَ بِشَيۡءٖ مِّنۡ عِلۡمِهِۦٓ إِلَّا بِمَا شَآءَۚ وَسِعَ كُرۡسِيُّهُ ٱلسَّمَٰوَٰتِ وَٱلۡأَرۡضَۖ وَلَا يَ‍ُٔودُهُۥ حِفۡظُهُمَاۚ وَهُوَ ٱلۡعَلِيُّ ٱلۡعَظِيمُ" }, @@ -128,7 +128,7 @@ "safar": "صفر", "rabiAlawwal": "ربيع الأول", "rabiAlthani": "ربيع الثاني", - "jumadaAlula": "جماد الأول", + "jumadaAlula": "جمادى الأولى", "jumadaAlakhirah": "جمادة الآخرة", "rajab": "رجب", "shaban": "شعبان", @@ -168,7 +168,7 @@ "announcementOnlyMode": "وضع الإعلانات", "normalMode": "الوضع العادي", "announcementOnlyModeEXPLINATION": "اختر إذا كنت تود أن تعرض شاشة الإعلانات طوال الوقت، هذا يمكن أن يكون مفيداً إذا قمت بتثبيت الشاشة على سبيل المثال في المدخل.", - "duaaElEftarText": "اللهُمَّ إِنِّي لَكَ صُمْتُ وَعَلَى رِزْقِكَ أَفْطَرْتُ، وَإِلَيْكَ انْبَتُّ، وَعَلَيْكَ تَوَكَّلْتُ، ذَهَبَ الظَّمَأُ وَابْتلّت الْعُرُوِقُ، وَثَبَتَ الْأَجْرُ إِنْ شَاءَ اللَّهُ.", + "duaaElEftarText": "", "@duaaElEftarText": { "description": "اللهم اني لگ صمت وعلى رزقك افطرت واليك انبت وعليگ توكلت ذهب الظما وابتلت العروق وثبت الاجر انشاء الله" }, @@ -243,8 +243,8 @@ "screenLockMode": "وضع تشغيل/إيقاف الشاشة", "screenLockDesc": "تشغيل/إيقاف الشاشة قبل وبعد كل صلاة لتوفير الطاقة", "screenLockDesc2": "هذه ميزة تشغيل/إيقاف الشاشة قبل وبعد كل صلاة أذان", - "before": "دقائق قبل كل وقت للصلاة", - "after": "دقائق بعد كل وقت للصلاة", + "before": "الدقائق قبل كل صلاة", + "after": "الدقائق بعد كل صلاة", "updateAvailable": "التحديث متاح", "seeMore": "مشاهدة المزيد", "whatIsNew": "ما الجديد", @@ -290,6 +290,20 @@ } } }, + "quranReadingPagePortrait": "الصفحة {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": "اختر الصفحة", "checkingForUpdates": "التحقق من وجود تحديث...", "chooseQuranType": "إختر الرواية", @@ -323,36 +337,22 @@ }, "noReciterSearchResult": "لم يتم العثور على نتائج لبحثك", "searchForReciter": "ابحث عن قارئ", - "downloadAllSuwarSuccessfully": "تم تحميل القرآن بالكامل", + "downloadAllSuwarSuccessfully": "تم تحميل القرآن كاملاً", "noSuwarDownload": "لا يوجد سور لتحميلها", - "connectDownloadQuran": "الرجاء الاتصال بالإنترنت لتنزيل القرآن", - "playInOnlineModeQuran": "الرجاء الاتصال بالإنترنت لتشغيل القرآن", + "connectDownloadQuran": "الرجاء الاتصال بالإنترنت للتنزيل", + "playInOnlineModeQuran": "الرجاء الاتصال بالإنترنت للتشغيل", "downloaded": "تم التحميل", "switchQuranType": "انتقل إلى {name}", "@switchQuranType": { - "description": "رسالة تظهر عند إضافة قارئ إلى المفضلة", + "description": "Message shown when a reciter is added to favorites", "placeholders": { "name": { "type": "String", - "example": "ورش" + "example": "Warsh" } } }, "surahSelector": "اختر السورة", - "quranReadingPagePortrait": "الصفحة {currentPage} / {totalPages}", - "@quranReadingPagePortrait": { - "description": "Placeholder text for displaying Quran reading page portrait numbers", - "placeholders": { - "currentPage": { - "type": "int", - "example": "1" - }, - "totalPages": { - "type": "int", - "example": "604" - } - } - }, "checkForUpdates": "التحقق من التحديثات", "checkForNewVersion": "تحقق مما إذا كانت هناك نسخة جديدة متوفرة", "wouldYouLikeToUpdate": "هل ترغب في تحديث التطبيق؟", @@ -362,7 +362,7 @@ "updateCancelled": "تم إلغاء التحديث", "checkingUpdates": "جارٍ التحقق من التحديثات...", "downloadingUpdate": "جارٍ تنزيل التحديث...", - "installingUpdate": "جارٍ تثبيت التحديث...", + "installingUpdate": "جارٍ تنزيل التحديث...", "updateCompletedSuccessfully": "تم التحديث بنجاح", "updateFailed": "فشل التحديث", "checkInternetUpdate": "يجب عليك الاتصال بالإنترنت للتحقق من وجود تحديثات جديدة", @@ -380,4 +380,4 @@ } } } -} +} \ No newline at end of file diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 51c34c6a4..bf9130cd8 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -335,11 +335,11 @@ "@noFavoriteReciters": { "description": "Message shown when there are no favorite reciters" }, - "noReciterSearchResult": "No results found for your search", - "searchForReciter": "Search for a reciter", + "noReciterSearchResult": "No results found for your search", + "searchForReciter": "Search for a reciter", "downloadAllSuwarSuccessfully": "The whole quran is downloaded", "noSuwarDownload": "No new suwars to download", - "connectDownloadQuran":"Please connect to Internet to download", + "connectDownloadQuran": "Please connect to Internet to download", "playInOnlineModeQuran": "Please connect to internet to play", "downloaded": "Downloaded", "switchQuranType": "Go to {name}", @@ -352,7 +352,7 @@ } } }, - "surahSelector":"Select Surah", + "surahSelector": "Select Surah", "checkForUpdates": "Check for Updates", "checkForNewVersion": "Check if a new version is available", "wouldYouLikeToUpdate": "Would you like to update the app?", @@ -380,6 +380,5 @@ } } } - - + } diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index 2bce6e640..b565369e0 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -308,7 +308,7 @@ "checkingForUpdates": "Vérification des mises à jour...", "chooseQuranType": "Choisir quran", "hafs": "Hafs", - "warsh": "Guerrier", + "warsh": "Warch", "favorites": "Favoris", "allReciters": "Tous les Réciteurs", "reciterAddedToFavorites": "Le réciteur {name} a été ajouté aux favoris", diff --git a/lib/l10n/intl_pt.arb b/lib/l10n/intl_pt.arb index 8ca4a1d94..92f2df2e5 100644 --- a/lib/l10n/intl_pt.arb +++ b/lib/l10n/intl_pt.arb @@ -20,9 +20,9 @@ "darkMode": "Modo escuro", "lightMode": "Modo claro", "changeMosque": "Alterar Mesquita", - "in1": "em", + "in1": "depois", "sec": "Seg", - "online": "On-line", + "online": "Disponível", "missingMosqueId": "MAWAQIT #ID ou MOSQUE #ID em falta", "mosqueIdIsNotValid": "{mosqueId} não é um ID de Mesquita válido", "selectMosqueId": "Por favor, insira o ID da Mesquita", @@ -68,15 +68,15 @@ "@azkarList1": { "description": "سُـبْحانَ اللهِ، والحَمْـدُ لله، واللهُ أكْـبَر 33 مرة لا إِلَٰهَ إلاّ اللّهُ وَحْـدَهُ لا شريكَ لهُ، لهُ الملكُ ولهُ الحَمْد، وهُوَ على كُلّ شَيءٍ قَـدير" }, - "azkarList2": "Em nome de Deus, o Clemente, o Misericordioso 1. Diz: «Procuro refúgio no Senhor dos humanos, 2. O Rei dos humanos, 3. O Deus dos humanos, 4. Contra o mal do murmurador que recua, 5. Que murmura nos peitos dos humanos, 6. dos jinns ou dos humanos.»", + "azkarList2": "", "@azkarList2": { "description": "بِسۡمِ ٱللَّهِ ٱلرَّحۡمَٰنِ ٱلرَّحِيمِ قُلۡ أَعُوذُ بِرَبِّ ٱلنَّاسِ ، مَلِكِ ٱلنَّاسِ ، إِلَٰهِ ٱلنَّاسِ ، مِن شَرِّ ٱلۡوَسۡوَاسِ ٱلۡخَنَّاسِ ، ٱلَّذِي يُوَسۡوِسُ فِي صُدُورِ ٱلنَّاسِ ، مِنَ ٱلۡجِنَّةِ وَٱلنَّاس" }, - "azkarList3": "Em nome de Deus, o Clemente, o Misericordioso 1. Diz: «Procuro refúgio no Senhor da manhã, 2 Contra o mal que criou, 3. E contra o mal da escuridão quando entra, 4. E contra o mal das que sopram nos nós, 5. E contra o mal do invejoso quando inveja.»", + "azkarList3": "", "@azkarList3": { "description": "بِسۡمِ ٱللَّهِ ٱلرَّحۡمَٰنِ ٱلرَّحِيمِقُلۡ أَعُوذُ بِرَبِّ ٱلۡفَلَقِ ، مِن شَرِّ مَا خَلَقَ ، وَمِن شَرِّ غَاسِقٍ إِذَا وَقَبَ ، وَمِن شَرِ ٱلنَّفَّٰثَٰتِ فِي ٱلۡعُقَدِ ، وَمِن شَرِّ حَاسِدٍ إِذَا حَسَدَ" }, - "azkarList4": "Em nome de Deus, o Clemente, o Misericordioso 1. Diz: «Ele é Allah, o Único! 2 Allah é o Independente; 3. Não gerou e nem foi gerado; 4. E não há ninguém igual a Ele.»", + "azkarList4": "", "@azkarList4": { "description": "بِسۡمِ ٱللَّهِ ٱلرَّحۡمَٰنِ ٱلرَّحِيمِ قُلۡ هُوَ ٱللَّهُ أَحَدٌ ، ٱللَّهُ ٱلصَّمَدُ ، لَمۡ يَلِدۡ وَلَمۡ يُولَدۡ ، وَلَمۡ يَكُن لَّهُۥ كُفُوًا أَحَدُۢ" }, @@ -128,7 +128,7 @@ "safar": "Safar", "rabiAlawwal": "Rabi al-Awwal", "rabiAlthani": "Rabi al-Thani", - "jumadaAlula": "Jumada al-Ula", + "jumadaAlula": "Jumada al-Awwal", "jumadaAlakhirah": "Jumada al-Thaniyah", "rajab": "Rajab", "shaban": "Sha'aban", @@ -168,7 +168,7 @@ "announcementOnlyMode": "Modo de anúncios", "normalMode": "Modo normal ", "announcementOnlyModeEXPLINATION": "Escolha se pretende que o ecrã mostre apenas anúncios, isso pode ser útil se você instalar o ecrã na entrada, por exemplo.", - "duaaElEftarText": "Ó Allah, por Ti jejuei, com o Teu sustento quebrei o jejum, em Ti acreditei e em Ti confiei. A sede foi saciada, as veias foram molhadas e a recompensa está garantida, se Deus quiser.", + "duaaElEftarText": "", "@duaaElEftarText": { "description": "اللهم اني لگ صمت وعلى رزقك افطرت واليك انبت وعليگ توكلت ذهب الظما وابتلت العروق وثبت الاجر انشاء الله" }, @@ -290,6 +290,20 @@ } } }, + "quranReadingPagePortrait": "Página {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": "Escolha a página", "checkingForUpdates": "A verificar atualizações...", "chooseQuranType": "Escolher Alcorão", @@ -320,5 +334,35 @@ "noFavoriteReciters": "Sem recitadores favoritos. Tente adicionar um à lista.", "@noFavoriteReciters": { "description": "Message shown when there are no favorite reciters" - } + }, + "noReciterSearchResult": "Nenhum resultado encontrado para a sua pesquisa", + "searchForReciter": "Pesquisar por um recitador", + "downloadAllSuwarSuccessfully": "O Alcorão completo foi descarregado", + "noSuwarDownload": "Não há novos surah para descarregar", + "connectDownloadQuran": "Por favor, conecte-se à Internet para descarregar", + "playInOnlineModeQuran": "Por favor, conecte-se à internet para reproduzir", + "downloaded": "Descarregado", + "switchQuranType": "Ir para {name}", + "@switchQuranType": { + "description": "Message shown when a reciter is added to favorites", + "placeholders": { + "name": { + "type": "String", + "example": "Warsh" + } + } + }, + "surahSelector": "Selecionar Surah", + "checkForUpdates": "Verificar Atualizações", + "checkForNewVersion": "Verificar se uma nova versão está disponível", + "wouldYouLikeToUpdate": "Gostaria de atualizar a aplicação?", + "updateCompleted": "Atualização concluída com sucesso!", + "noUpdates": "Sem Atualizações", + "usingLatestVersion": "Está a utilizar a versão mais recente.", + "updateCancelled": "Atualização cancelada", + "checkingUpdates": "A verificar atualizações...", + "downloadingUpdate": "A descarregar a atualização...", + "installingUpdate": "A instalar a atualização...", + "updateCompletedSuccessfully": "Atualização concluída com sucesso", + "updateFailed": "Falha na atualização" } \ No newline at end of file From b0d1683f0f415b449d20a7d7eec940040bc92350 Mon Sep 17 00:00:00 2001 From: Ghassen Ben Zahra Date: Wed, 27 Nov 2024 15:09:01 +0100 Subject: [PATCH 18/26] 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 19/26] 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 20/26] 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 21/26] 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 22/26] 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); From 63901324671f85ab1032c9ff19b1beaef8169790 Mon Sep 17 00:00:00 2001 From: Yassin Nouh <70436855+YassinNouh21@users.noreply.github.com> Date: Thu, 12 Dec 2024 10:31:53 +0200 Subject: [PATCH 23/26] Fix/incorrect next prayer v2 (#1476) * fix: Isha prayer after midnight wasn't being selected correctly as the next prayer. * extend packages --- .../mixins/mosque_helpers_mixins.dart | 79 ++++++++++++++----- 1 file changed, 59 insertions(+), 20 deletions(-) diff --git a/lib/src/services/mixins/mosque_helpers_mixins.dart b/lib/src/services/mixins/mosque_helpers_mixins.dart index f74508ab9..c91180ae4 100644 --- a/lib/src/services/mixins/mosque_helpers_mixins.dart +++ b/lib/src/services/mixins/mosque_helpers_mixins.dart @@ -8,10 +8,10 @@ import 'package:mawaqit/src/models/announcement.dart'; import 'package:mawaqit/src/models/calendar/MawaqitHijriCalendar.dart'; import 'package:mawaqit/src/services/mosque_manager.dart'; -import '../../../i18n/l10n.dart'; -import '../../models/mosque.dart'; -import '../../models/mosqueConfig.dart'; -import '../../models/times.dart'; +import 'package:mawaqit/i18n/l10n.dart'; +import 'package:mawaqit/src/models/mosque.dart'; +import 'package:mawaqit/src/models/mosqueConfig.dart'; +import 'package:mawaqit/src/models/times.dart'; mixin MosqueHelpersMixin on ChangeNotifier { abstract Mosque? mosque; @@ -168,28 +168,67 @@ mixin MosqueHelpersMixin on ChangeNotifier { /// return -1 in case of issue(invalid times format) int nextSalahIndex() { final now = mosqueDate(); - final nextSalah = actualTimes().firstWhere( - (element) => element.isAfter(now), - orElse: () => actualTimes().first, - ); - var salahIndex = actualTimes().indexOf(nextSalah); - if (salahIndex > 4) salahIndex = 0; - if (salahIndex < 0) salahIndex = 4; - return salahIndex; + final times = actualTimes(); + final fajrTime = times[0]; + + // Convert time to minutes since start of day + int toMinutes(DateTime time) { + // If time is after midnight but before Fajr, add 24 hours + if (time.hour < fajrTime.hour || (time.hour == fajrTime.hour && time.minute < fajrTime.minute)) { + return (time.hour + 24) * 60 + time.minute; + } + return time.hour * 60 + time.minute; + } + + // Get minutes for current time + int nowMinutes = toMinutes(now); + + // Convert prayer times to minutes and handle after-midnight cases + List timeMinutes = times.mapIndexed((index, time) { + // For Isha prayer (index 4), if it's very early (e.g. 1:00), treat it as next day + if (index == 4 && time.hour < fajrTime.hour) { + return (time.hour + 24) * 60 + time.minute; + } + return time.hour * 60 + time.minute; + }).toList(); + + // Find next prayer + int nextIndex = timeMinutes.indexWhere((minutes) => minutes > nowMinutes); + + // If no next prayer found today, return Fajr (0) + return nextIndex == -1 ? 0 : nextIndex; } /// return the upcoming salah index /// return -1 in case of issue(invalid times format) int nextSalahAfterIqamaIndex() { final now = mosqueDate(); - final nextSalah = actualIqamaTimes().firstWhere( - (element) => element.isAfter(now), - orElse: () => actualIqamaTimes().first, - ); - var salahIndex = actualIqamaTimes().indexOf(nextSalah); - if (salahIndex > 4) salahIndex = 0; - if (salahIndex < 0) salahIndex = 4; - return salahIndex; + final times = actualIqamaTimes(); + + // Convert time to minutes since midnight for easier comparison + int toMinutes(DateTime time, DateTime fajrTime) { + // If current time is before fajr and after midnight, + // or if prayer time is before fajr, add 24 hours + bool isAfterMidnight = time.hour < fajrTime.hour || (time.hour == fajrTime.hour && time.minute < fajrTime.minute); + + int hours = isAfterMidnight ? time.hour + 24 : time.hour; + return hours * 60 + time.minute; + } + + // Get minutes since midnight for current time + int nowMinutes = toMinutes(now, times[0]); + + // Convert prayer times to minutes + List timeMinutes = times.map((t) { + bool isBeforeFajr = t.hour < times[0].hour || (t.hour == times[0].hour && t.minute <= times[0].minute); + return isBeforeFajr ? toMinutes(t, times[0]) : t.hour * 60 + t.minute; + }).toList(); + + // Find the next prayer time + final nextIndex = timeMinutes.indexWhere((t) => t > nowMinutes); + + // If no next prayer found today, return first prayer (Fajr) + return nextIndex == -1 ? 0 : nextIndex; } /// the duration until the next salah From cc845c3156d35871611c2d6c9db9378a20870299 Mon Sep 17 00:00:00 2001 From: Ghassen Ben Zahra Date: Thu, 12 Dec 2024 09:35:19 +0100 Subject: [PATCH 24/26] Feat/on off for tablet (#1464) * 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 * add support of on/off feature to tablet * fix: remove open bracket intl_en.arb --------- 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 --- .../main/kotlin/com/flyweb/MainActivity.kt | 26 ++++++ lib/l10n/intl_ar.arb | 6 +- lib/l10n/intl_en.arb | 27 +++--- lib/l10n/intl_fr.arb | 6 +- lib/src/const/constants.dart | 4 +- lib/src/pages/SettingScreen.dart | 24 +++-- lib/src/services/mosque_manager.dart | 9 +- .../toggle_screen_feature_manager.dart | 93 ++++++++++++++++--- .../screen_lock/screen_lock_notifier.dart | 5 +- .../screen_lock/screen_lock_state.dart | 5 + lib/src/widgets/screen_lock_widget.dart | 26 +++++- pubspec.yaml | 1 + 12 files changed, 185 insertions(+), 47 deletions(-) diff --git a/android/app/src/main/kotlin/com/flyweb/MainActivity.kt b/android/app/src/main/kotlin/com/flyweb/MainActivity.kt index f4e5da4fd..9a1b137c8 100644 --- a/android/app/src/main/kotlin/com/flyweb/MainActivity.kt +++ b/android/app/src/main/kotlin/com/flyweb/MainActivity.kt @@ -54,6 +54,8 @@ class MainActivity : FlutterActivity() { "checkRoot" -> result.success(checkRoot()) "toggleBoxScreenOff" -> toggleBoxScreenOff(call, result) "toggleBoxScreenOn" -> toggleBoxScreenOn(call, result) + "toggleTabletScreenOff" -> toggleTabletScreenOff(call, result) + "toggleTabletScreenOn" -> toggleTabletScreenOn(call, result) "connectToNetworkWPA" -> connectToNetworkWPA(call, result) "addLocationPermission" -> addLocationPermission(call, result) "grantFineLocationPermission" -> grantFineLocationPermission(call, result) @@ -255,6 +257,30 @@ fun connectToNetworkWPA(call: MethodCall, result: MethodChannel.Result) { } } } + private fun toggleTabletScreenOff(call: MethodCall, result: MethodChannel.Result) { + AsyncTask.execute { + try { + val commands = listOf( +"input keyevent 26" + ) + executeCommand(commands, result) // Lock the device + } catch (e: Exception) { + handleCommandException(e, result) + } + } + } + private fun toggleTabletScreenOn(call: MethodCall, result: MethodChannel.Result) { + AsyncTask.execute { + try { + val commands = listOf( +"input keyevent 82" + ) + executeCommand(commands, result) // Lock the device + } catch (e: Exception) { + handleCommandException(e, result) + } + } + } private fun grantFineLocationPermission(call: MethodCall, result: MethodChannel.Result) { AsyncTask.execute { try { diff --git a/lib/l10n/intl_ar.arb b/lib/l10n/intl_ar.arb index 37677ffdc..0b138b6a5 100644 --- a/lib/l10n/intl_ar.arb +++ b/lib/l10n/intl_ar.arb @@ -394,5 +394,9 @@ "example": "604" } } - } + }, + "ishaAndFajrOnly": "فقط صلاتي الفجر و العشاء", + "minutesBeforeFajrPrayer": "دقائق قبل وقت صلاة الفجر", + "minutesAfterIshaPrayer": "دقائق بعد وقت صلاة العشاء" + } \ No newline at end of file diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index aa41e2135..ce9bde755 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -365,17 +365,17 @@ "installingUpdate": "Installing update...", "updateCompletedSuccessfully": "Update completed successfully", "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...", + "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", @@ -409,5 +409,8 @@ "example": "2.0" } } - } + }, + "ishaAndFajrOnly": "Fajr and Isha prayers only", + "minutesBeforeFajrPrayer": "minutes before fajr prayer time", + "minutesAfterIshaPrayer": "minutes afer isha prayer time" } diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index 4cdadd2c7..68a55d4f7 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -396,5 +396,9 @@ "example": "604" } } - } + }, + "ishaAndFajrOnly": "Seulement les prières Fajr et d'Isha", + "minutesBeforeFajrPrayer": "minutes avant l'heure de la prière de Fajr", + "minutesAfterIshaPrayer": "minutes après l'heure de la prière d'Isha" + } \ No newline at end of file diff --git a/lib/src/const/constants.dart b/lib/src/const/constants.dart index 5dc154d59..be2acbfb4 100644 --- a/lib/src/const/constants.dart +++ b/lib/src/const/constants.dart @@ -51,7 +51,9 @@ class TurnOnOffTvConstant { static const String kCheckRoot = "checkRoot"; static const String kToggleBoxScreenOff = "toggleBoxScreenOff"; static const String kToggleBoxScreenOn = "toggleBoxScreenOn"; - + static const String kToggleTabletScreenOn = "toggleTabletScreenOn"; + static const String kToggleTabletScreenOff = "toggleTabletScreenOff"; + static const String kisFajrIshaOnly = "isIshaFajrOnly"; static const String kMinuteBeforeKey = 'selectedMinuteBefore'; static const String kMinuteAfterKey = 'selectedMinuteAfter'; } diff --git a/lib/src/pages/SettingScreen.dart b/lib/src/pages/SettingScreen.dart index c50332ff3..a3a5af004 100644 --- a/lib/src/pages/SettingScreen.dart +++ b/lib/src/pages/SettingScreen.dart @@ -416,19 +416,17 @@ class _SettingScreenState extends ConsumerState { style: theme.textTheme.headlineSmall, textAlign: TextAlign.center, ), - timeShiftManager.isLauncherInstalled - ? _SettingItem( - title: S.of(context).screenLock, - subtitle: S.of(context).screenLockDesc, - icon: Icon(Icons.power_settings_new, size: 35), - onTap: () => showDialog( - context: context, - builder: (context) => ScreenLockModal( - timeShiftManager: timeShiftManager, - ), - ), - ) - : SizedBox(), + _SettingItem( + title: S.of(context).screenLock, + subtitle: S.of(context).screenLockDesc, + icon: Icon(Icons.power_settings_new, size: 35), + onTap: () => showDialog( + context: context, + builder: (context) => ScreenLockModal( + timeShiftManager: timeShiftManager, + ), + ), + ), _SettingItem( title: S.of(context).appTimezone, subtitle: S.of(context).descTimezone, diff --git a/lib/src/services/mosque_manager.dart b/lib/src/services/mosque_manager.dart index eac4896d8..ceaa0df51 100644 --- a/lib/src/services/mosque_manager.dart +++ b/lib/src/services/mosque_manager.dart @@ -81,6 +81,7 @@ class MosqueManager extends ChangeNotifier with WeatherMixin, AudioMixin, Mosque bool isToggleScreenActivated = false; int minuteBefore = 0; int minuteAfter = 0; + bool isIshaFajrOnly = false; /// get current home url String buildUrl(String languageCode) { @@ -102,6 +103,11 @@ class MosqueManager extends ChangeNotifier with WeatherMixin, AudioMixin, Mosque return prefs.getInt(_minuteAfterKey) ?? 10; } + static Future getisIshaFajr() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getBool(TurnOnOffTvConstant.kisFajrIshaOnly) ?? false; + } + static Future checkRoot() async { try { final result = await MethodChannel(TurnOnOffTvConstant.kNativeMethodsChannel).invokeMethod( @@ -123,7 +129,7 @@ class MosqueManager extends ChangeNotifier with WeatherMixin, AudioMixin, Mosque isEventsSet = await ToggleScreenFeature.checkEventsScheduled(); minuteBefore = await getMinuteBefore(); minuteAfter = await getMinuteAfter(); - + isIshaFajrOnly = await getisIshaFajr(); notifyListeners(); } @@ -220,6 +226,7 @@ class MosqueManager extends ChangeNotifier with WeatherMixin, AudioMixin, Mosque ToggleScreenFeature.checkEventsScheduled().then((_) { if (!isEventsSet) { ToggleScreenFeature.scheduleToggleScreen( + isIshaFajrOnly, e.dayTimesStrings(today, salahOnly: false), minuteBefore, minuteAfter, diff --git a/lib/src/services/toggle_screen_feature_manager.dart b/lib/src/services/toggle_screen_feature_manager.dart index c8a0e3d04..4038ec0f2 100644 --- a/lib/src/services/toggle_screen_feature_manager.dart +++ b/lib/src/services/toggle_screen_feature_manager.dart @@ -5,6 +5,7 @@ import 'package:flutter/services.dart'; import 'package:mawaqit/main.dart'; import 'package:mawaqit/src/const/constants.dart'; import 'package:mawaqit/src/helpers/AppDate.dart'; +import 'package:mawaqit/src/helpers/TimeShiftManager.dart'; import 'package:shared_preferences/shared_preferences.dart'; class ToggleScreenFeature { @@ -18,11 +19,14 @@ class ToggleScreenFeature { static final Map> _scheduledTimers = {}; static Future scheduleToggleScreen( - List timeStrings, int beforeDelayMinutes, int afterDelayMinutes) async { - for (String timeString in timeStrings) { - final parts = timeString.split(':'); - final hour = int.parse(parts[0]); - final minute = int.parse(parts[1]); + bool isfajrIshaonly, List timeStrings, int beforeDelayMinutes, int afterDelayMinutes) async { + final timeShiftManager = TimeShiftManager(); + + if (isfajrIshaonly) { + String fajrTime = timeStrings[0]; + List parts = fajrTime.split(':'); + int hour = int.parse(parts[0]); + int minute = int.parse(parts[1]); final now = AppDateTime.now(); DateTime scheduledDateTime = DateTime(now.year, now.month, now.day, hour, minute); @@ -32,19 +36,60 @@ class ToggleScreenFeature { } final beforeDelay = scheduledDateTime.difference(now) - Duration(minutes: beforeDelayMinutes); - if (beforeDelay.isNegative) { - continue; + + if (!beforeDelay.isNegative) { + final beforeTimer = Timer(beforeDelay, () { + timeShiftManager.isLauncherInstalled ? _toggleBoxScreenOn() : _toggleTabletScreenOn(); + }); + _scheduledTimers[fajrTime] = [beforeTimer]; + } + + String ishaTime = timeStrings[5]; + parts = ishaTime.split(':'); + hour = int.parse(parts[0]); + minute = int.parse(parts[1]); + + scheduledDateTime = DateTime(now.year, now.month, now.day, hour, minute); + + if (scheduledDateTime.isBefore(now)) { + scheduledDateTime = scheduledDateTime.add(Duration(days: 1)); } - final beforeTimer = Timer(beforeDelay, () { - _toggleBoxScreenOn(); - }); final afterDelay = scheduledDateTime.difference(now) + Duration(minutes: afterDelayMinutes); + final afterTimer = Timer(afterDelay, () { - _toggleBoxScreenOff(); + timeShiftManager.isLauncherInstalled ? _toggleBoxScreenOff() : _toggleTabletScreenOff(); }); + _scheduledTimers[ishaTime] = [afterTimer]; + } else { + // Original logic for all prayer times + for (String timeString in timeStrings) { + final parts = timeString.split(':'); + final hour = int.parse(parts[0]); + final minute = int.parse(parts[1]); + + final now = AppDateTime.now(); + DateTime scheduledDateTime = DateTime(now.year, now.month, now.day, hour, minute); + + if (scheduledDateTime.isBefore(now)) { + scheduledDateTime = scheduledDateTime.add(Duration(days: 1)); + } - _scheduledTimers[timeString] = [beforeTimer, afterTimer]; + final beforeDelay = scheduledDateTime.difference(now) - Duration(minutes: beforeDelayMinutes); + if (beforeDelay.isNegative) { + continue; + } + final beforeTimer = Timer(beforeDelay, () { + _toggleBoxScreenOn(); + }); + + final afterDelay = scheduledDateTime.difference(now) + Duration(minutes: afterDelayMinutes); + final afterTimer = Timer(afterDelay, () { + _toggleBoxScreenOff(); + }); + + _scheduledTimers[timeString] = [beforeTimer, afterTimer]; + } } await _saveScheduledTimersToPrefs(); } @@ -76,7 +121,6 @@ class ToggleScreenFeature { }).toList(), ); }); - await prefs.setString(_scheduledTimersKey, json.encode(timersMap)); } @@ -90,6 +134,11 @@ class ToggleScreenFeature { return prefs.getBool(TurnOnOffTvConstant.kActivateToggleFeature) ?? false; } + static Future getToggleFeatureishaFajrState() async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + return prefs.getBool(TurnOnOffTvConstant.kisFajrIshaOnly) ?? false; + } + static Future cancelAllScheduledTimers() async { _scheduledTimers.forEach((timeString, timers) { for (final timer in timers) { @@ -120,6 +169,24 @@ class ToggleScreenFeature { } } + static Future _toggleTabletScreenOn() async { + try { + await MethodChannel(TurnOnOffTvConstant.kNativeMethodsChannel) + .invokeMethod(TurnOnOffTvConstant.kToggleTabletScreenOn); + } on PlatformException catch (e) { + logger.e(e); + } + } + + static Future _toggleTabletScreenOff() async { + try { + await MethodChannel(TurnOnOffTvConstant.kNativeMethodsChannel) + .invokeMethod(TurnOnOffTvConstant.kToggleTabletScreenOff); + } on PlatformException catch (e) { + logger.e(e); + } + } + static Future checkEventsScheduled() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); logger.d("value${prefs.getBool("isEventsSet")}"); diff --git a/lib/src/state_management/screen_lock/screen_lock_notifier.dart b/lib/src/state_management/screen_lock/screen_lock_notifier.dart index dcf2b9c77..61a0fed17 100644 --- a/lib/src/state_management/screen_lock/screen_lock_notifier.dart +++ b/lib/src/state_management/screen_lock/screen_lock_notifier.dart @@ -21,6 +21,7 @@ class ScreenLockNotifier extends AsyncNotifier { return ScreenLockState( selectedTime: DateTime.now().add(Duration(hours: timeShiftManager.shift, minutes: timeShiftManager.shiftInMinutes)), + isfajrIshaonly: prefs.getBool(TurnOnOffTvConstant.kisFajrIshaOnly) ?? false, isActive: isActive, selectedMinuteBefore: prefs.getInt(TurnOnOffTvConstant.kMinuteBeforeKey) ?? 10, selectedMinuteAfter: prefs.getInt(TurnOnOffTvConstant.kMinuteAfterKey) ?? 30, @@ -63,9 +64,10 @@ class ScreenLockNotifier extends AsyncNotifier { state = AsyncValue.data(state.value!.copyWith(selectedMinuteAfter: newMinute < 10 ? 59 : newMinute)); } - Future saveSettings(List times) async { + Future saveSettings(List times, bool isIshaFajrOnly) async { await ToggleScreenFeature.cancelAllScheduledTimers(); ToggleScreenFeature.scheduleToggleScreen( + isIshaFajrOnly, times, state.value!.selectedMinuteBefore, state.value!.selectedMinuteAfter, @@ -75,6 +77,7 @@ class ScreenLockNotifier extends AsyncNotifier { final prefs = await SharedPreferences.getInstance(); prefs.setInt(TurnOnOffTvConstant.kMinuteBeforeKey, state.value!.selectedMinuteBefore); prefs.setInt(TurnOnOffTvConstant.kMinuteAfterKey, state.value!.selectedMinuteAfter); + prefs.setBool(TurnOnOffTvConstant.kisFajrIshaOnly, isIshaFajrOnly); } } diff --git a/lib/src/state_management/screen_lock/screen_lock_state.dart b/lib/src/state_management/screen_lock/screen_lock_state.dart index d81ab07f2..d6cac306d 100644 --- a/lib/src/state_management/screen_lock/screen_lock_state.dart +++ b/lib/src/state_management/screen_lock/screen_lock_state.dart @@ -5,12 +5,14 @@ class ScreenLockState extends Equatable { final bool isActive; final int selectedMinuteBefore; final int selectedMinuteAfter; + final bool isfajrIshaonly; ScreenLockState({ required this.selectedTime, required this.isActive, required this.selectedMinuteBefore, required this.selectedMinuteAfter, + required this.isfajrIshaonly, }); ScreenLockState copyWith({ @@ -18,12 +20,14 @@ class ScreenLockState extends Equatable { bool? isActive, int? selectedMinuteBefore, int? selectedMinuteAfter, + bool? isfajrIshaonly, }) { return ScreenLockState( selectedTime: selectedTime ?? this.selectedTime, isActive: isActive ?? this.isActive, selectedMinuteBefore: selectedMinuteBefore ?? this.selectedMinuteBefore, selectedMinuteAfter: selectedMinuteAfter ?? this.selectedMinuteAfter, + isfajrIshaonly: isfajrIshaonly ?? this.isfajrIshaonly, ); } @@ -32,5 +36,6 @@ class ScreenLockState extends Equatable { isActive, selectedMinuteBefore, selectedMinuteAfter, + isfajrIshaonly, ]; } diff --git a/lib/src/widgets/screen_lock_widget.dart b/lib/src/widgets/screen_lock_widget.dart index 37505914f..3b5d8a78c 100644 --- a/lib/src/widgets/screen_lock_widget.dart +++ b/lib/src/widgets/screen_lock_widget.dart @@ -42,7 +42,7 @@ class __TimePickerState extends ConsumerState<_TimePicker> { final TimeShiftManager timeManager = TimeShiftManager(); late DateTime selectedTime; bool value = false; - + bool isIshaFajrOnly = false; @override void initState() { super.initState(); @@ -50,8 +50,10 @@ class __TimePickerState extends ConsumerState<_TimePicker> { WidgetsBinding.instance.addPostFrameCallback((_) async { ref.read(screenLockNotifierProvider.notifier); value = await ToggleScreenFeature.getToggleFeatureState(); + isIshaFajrOnly = await ToggleScreenFeature.getToggleFeatureishaFajrState(); setState(() { value = value; + isIshaFajrOnly = isIshaFajrOnly; }); }); } @@ -114,13 +116,29 @@ class __TimePickerState extends ConsumerState<_TimePicker> { ), child: Column( children: [ + Card( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(100)), + margin: const EdgeInsets.symmetric(vertical: 5), + clipBehavior: Clip.antiAlias, + child: SwitchListTile( + autofocus: false, + secondary: const Icon(Icons.nightlight, size: 35), + title: Text(S.of(context).ishaAndFajrOnly), + value: isIshaFajrOnly, + onChanged: (newValue) { + setState(() { + isIshaFajrOnly = newValue; + }); + }, + ), + ), _buildTimeSelector( context, S.of(context).powerOnScreen, selectedMinuteBefore, ref.read(screenLockNotifierProvider.notifier).selectNextMinuteBefore, ref.read(screenLockNotifierProvider.notifier).selectPreviousMinuteBefore, - S.of(context).before, + isIshaFajrOnly ? S.of(context).minutesBeforeFajrPrayer : S.of(context).before, ), const SizedBox(height: 16), _buildTimeSelector( @@ -129,7 +147,7 @@ class __TimePickerState extends ConsumerState<_TimePicker> { selectedMinuteAfter, ref.read(screenLockNotifierProvider.notifier).selectNextMinuteAfter, ref.read(screenLockNotifierProvider.notifier).selectPreviousMinuteAfter, - S.of(context).after, + isIshaFajrOnly ? S.of(context).minutesAfterIshaPrayer : S.of(context).after, ), ], ), @@ -173,7 +191,7 @@ class __TimePickerState extends ConsumerState<_TimePicker> { Widget _buildSaveButton(BuildContext context, List times) { return OutlinedButton( onPressed: () async { - await ref.read(screenLockNotifierProvider.notifier).saveSettings(times); + await ref.read(screenLockNotifierProvider.notifier).saveSettings(times, isIshaFajrOnly); Navigator.pop(context); }, child: Text(S.current.ok), diff --git a/pubspec.yaml b/pubspec.yaml index d07b2d35b..c9d336e4f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,6 +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.18.0+1 From d2b32b6c16b9560e3c82c41ba38d0faa7d775de3 Mon Sep 17 00:00:00 2001 From: Yassin Nouh <70436855+YassinNouh21@users.noreply.github.com> Date: Sun, 15 Dec 2024 18:43:47 +0200 Subject: [PATCH 25/26] Fix/arabic quotation marks hadith (#1426) * feat(random-hadith): Add multilingual support and enhance theming - Introduced `language` field in `RandomHadithState` for language tracking - Updated `RandomHadithNotifier` to handle the new `language` property - Integrated `google_fonts` and created `LocalizedTextStyle` for dynamic fonts based on locale - Converted `HadithWidget` to `ConsumerWidget` using Riverpod for state management - Applied localized text styles in `HadithScreen.dart` - Added dependencies: `google_fonts`, `flutter_riverpod` * format * revert changes at the random hadith * fix: Random Hadith Caching and Language Support - Added `ensureHadithsAreCached` method to `RandomHadithRepository` and its implementation. - Implemented `_checkCachedHadiths` to verify cached hadiths for single and dual-language formats. - Improved `getLanguage` and `isTwoLanguage` in `RandomHadithHelper` to handle `-` and `_` delimiters. - Added `hasHadithsForLanguage` and `hasAnyHadiths` methods in `RandomHadithLocalDataSource`. - Integrated `ensureHadithLanguage` in `RandomHadithNotifier` for initialization. - Triggered hadith caching during `NormalWorkflow` initialization. * fix: implement responsive text display - Use LayoutBuilder to detect small screen sizes * fix formatting --- .../random_hadith_local_data_source.dart | 19 +++++++ .../data/repository/random_hadith_impl.dart | 23 ++++++++ .../repository/random_hadith_repository.dart | 1 + lib/src/helpers/random_hadith_helper.dart | 5 +- lib/src/pages/home/widgets/HadithScreen.dart | 53 +++++++++++++++---- .../pages/home/workflow/normal_workflow.dart | 4 ++ lib/src/services/theme_manager.dart | 31 +++++++++++ .../random_hadith/random_hadith_notifier.dart | 12 ++++- .../random_hadith/random_hadith_state.dart | 8 ++- 9 files changed, 141 insertions(+), 15 deletions(-) diff --git a/lib/src/data/data_source/random_hadith_local_data_source.dart b/lib/src/data/data_source/random_hadith_local_data_source.dart index 55303f2a6..6910a1376 100644 --- a/lib/src/data/data_source/random_hadith_local_data_source.dart +++ b/lib/src/data/data_source/random_hadith_local_data_source.dart @@ -74,6 +74,25 @@ class RandomHadithLocalDataSource { throw e; } } + + /// [hasHadithsForLanguage] Checks if hadiths are available in local storage for the specified language. + bool hasHadithsForLanguage(String language) { + try { + final hadithList = box.get(language) as List?; + return hadithList != null && hadithList.isNotEmpty; + } catch (e) { + return false; + } + } + + /// [hasAnyHadiths] Checks if any hadiths are available in local storage. + bool hasAnyHadiths() { + try { + return box.isNotEmpty; + } catch (e) { + return false; + } + } } /// This provider is responsible for initializing and providing an instance of diff --git a/lib/src/data/repository/random_hadith_impl.dart b/lib/src/data/repository/random_hadith_impl.dart index eb99cd4de..026cfa771 100644 --- a/lib/src/data/repository/random_hadith_impl.dart +++ b/lib/src/data/repository/random_hadith_impl.dart @@ -176,6 +176,29 @@ class RandomHadithImpl implements RandomHadithRepository { return hadith ?? ''; } } + + Future _checkCachedHadiths(String language) async { + log('random_hadith: RandomHadithImpl: Checking cached hadiths for language: $language'); + + if (RandomHadithHelper.isTwoLanguage(language)) { + final languageList = RandomHadithHelper.getLanguage(language); + // Check if both languages have cached hadiths + return languageList.every((lang) => localDataSource.hasHadithsForLanguage(lang)); + } else { + return localDataSource.hasHadithsForLanguage(language); + } + } + + @override + Future ensureHadithsAreCached(String language) async { + final hasCachedHadiths = await _checkCachedHadiths(language); + final isConnected = await connectivityService.connectionStatus == ConnectivityStatus.connected; + + if (!hasCachedHadiths && isConnected) { + log('random_hadith: RandomHadithImpl: No cached hadiths found, fetching from remote $language'); + await fetchAndCacheHadith(language); + } + } } /// [randomHadithRepositoryProvider] Riverpod provider for RandomHadithImpl. diff --git a/lib/src/domain/repository/random_hadith_repository.dart b/lib/src/domain/repository/random_hadith_repository.dart index 2de854d42..d07af322c 100644 --- a/lib/src/domain/repository/random_hadith_repository.dart +++ b/lib/src/domain/repository/random_hadith_repository.dart @@ -1,4 +1,5 @@ abstract class RandomHadithRepository { Future getRandomHadith({required String language}); Future fetchAndCacheHadith(String language); + Future ensureHadithsAreCached(String language); } diff --git a/lib/src/helpers/random_hadith_helper.dart b/lib/src/helpers/random_hadith_helper.dart index 94235bcf1..268cfbc55 100644 --- a/lib/src/helpers/random_hadith_helper.dart +++ b/lib/src/helpers/random_hadith_helper.dart @@ -4,10 +4,11 @@ class RandomHadithHelper { } static bool isTwoLanguage(String language) { - return language.contains('-'); + // Check if the language is in the format of 'language1-language2' or 'language1_language2'. + return language.contains('-') || language.contains('_'); } static List getLanguage(String language) { - return language.split('-'); + return language.split(RegExp(r'[-_]')); } } diff --git a/lib/src/pages/home/widgets/HadithScreen.dart b/lib/src/pages/home/widgets/HadithScreen.dart index 57fca7cbb..52a6a75bf 100644 --- a/lib/src/pages/home/widgets/HadithScreen.dart +++ b/lib/src/pages/home/widgets/HadithScreen.dart @@ -1,12 +1,19 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:mawaqit/i18n/AppLanguage.dart'; import 'package:mawaqit/src/helpers/RelativeSizes.dart'; import 'package:mawaqit/src/helpers/repaint_boundaries.dart'; +import 'package:mawaqit/src/services/mosque_manager.dart'; +import 'package:mawaqit/src/services/theme_manager.dart'; +import 'package:mawaqit/src/state_management/random_hadith/random_hadith_notifier.dart'; import 'package:mawaqit/src/themes/UIShadows.dart'; +import 'package:provider/provider.dart'; /// this screen made to show of the hadith screen -class HadithWidget extends StatelessWidget { +class HadithWidget extends ConsumerWidget { HadithWidget({ Key? key, this.title, @@ -44,7 +51,23 @@ class HadithWidget extends StatelessWidget { final double maxHeight; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final hadithLang = ref.watch( + randomHadithNotifierProvider.select((state) { + try { + final lang = state.maybeWhen( + orElse: () => 'ar', + data: (state) { + return state.language; + }, + ); + return Locale(lang); + } catch (e) { + return Locale('ar'); + } + }), + ); + return Padding( padding: padding ?? EdgeInsets.all(1.vwr), child: Column( @@ -58,6 +81,8 @@ class HadithWidget extends StatelessWidget { if (arabicText != null && arabicText != '') contentText( arabicText!, + context, + hadithLang, textDirection: TextDirection.rtl, delay: .1.seconds, ), @@ -70,6 +95,8 @@ class HadithWidget extends StatelessWidget { if (translatedText != null && translatedText != arabicText && translatedText != '') contentText( translatedText!, + context, + hadithLang, textDirection: textDirection, delay: .3.seconds, ), @@ -97,24 +124,30 @@ class HadithWidget extends StatelessWidget { } Widget contentText( - String text, { + String text, + BuildContext context, + Locale hadithLanguage, { TextDirection? textDirection, Duration? delay, }) { return Flexible( - fit: FlexFit.loose, + fit: FlexFit.tight, child: Container( constraints: BoxConstraints(maxHeight: maxHeight.vh), child: Padding( key: ValueKey(text), - padding: const EdgeInsets.symmetric(horizontal: 8.0), + padding: const EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 16, + ), child: AutoSizeText( text, - style: TextStyle( - fontSize: 600, - color: Colors.white, - shadows: kIqamaCountDownTextShadow, - ), + style: context.getLocalizedTextStyle(locale: hadithLanguage).copyWith( + color: Colors.white, + shadows: kIqamaCountDownTextShadow, + fontWeight: FontWeight.bold, + fontSize: 600, + ), textAlign: TextAlign.center, textDirection: textDirection, ).animate().fadeIn(delay: delay).addRepaintBoundary(), diff --git a/lib/src/pages/home/workflow/normal_workflow.dart b/lib/src/pages/home/workflow/normal_workflow.dart index f90b71c15..a6f374c89 100644 --- a/lib/src/pages/home/workflow/normal_workflow.dart +++ b/lib/src/pages/home/workflow/normal_workflow.dart @@ -9,6 +9,7 @@ import 'package:mawaqit/src/pages/home/sub_screens/takberat_aleid_screen.dart'; import 'package:mawaqit/src/pages/home/widgets/workflows/repeating_workflow_widget.dart'; import 'package:mawaqit/src/services/mosque_manager.dart'; import 'package:mawaqit/src/services/user_preferences_manager.dart'; +import 'package:mawaqit/src/state_management/random_hadith/random_hadith_notifier.dart'; import 'package:provider/provider.dart'; const _HadithDuration = Duration(seconds: 90); @@ -27,6 +28,9 @@ class _NormalWorkflowScreenState extends ConsumerState { @override void initState() { super.initState(); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + ref.read(randomHadithNotifierProvider.notifier).ensureHadithLanguage(); + }); } @override diff --git a/lib/src/services/theme_manager.dart b/lib/src/services/theme_manager.dart index 1e7ebaeaa..1e8e911bd 100644 --- a/lib/src/services/theme_manager.dart +++ b/lib/src/services/theme_manager.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; import 'package:mawaqit/src/helpers/StringUtils.dart'; import './storage_manager.dart'; @@ -133,3 +134,33 @@ class ThemeNotifier with ChangeNotifier { notifyListeners(); } } + +extension LocalizedTextStyle on BuildContext { + TextStyle getLocalizedTextStyle({ + Locale? locale, + double fontSize = 16.0, + FontWeight fontWeight = FontWeight.normal, + Color color = Colors.black, + }) { + // Define a map for font configurations based on locale + final fontMap = { + 'ar': GoogleFonts.notoKufiArabic, + // Add more language codes and corresponding GoogleFonts functions as needed + }; + + // Use the provided locale or fall back to the context's locale + final effectiveLocale = locale ?? Localizations.localeOf(this); + final languageCode = effectiveLocale.languageCode; + + // Get the appropriate font function for the locale + final fontFunction = fontMap[languageCode] ?? GoogleFonts.roboto; + // Return a TextStyle with dynamic customization + return fontFunction( + textStyle: TextStyle( + fontSize: fontSize, + fontWeight: fontWeight, + color: color, + ), + ); + } +} diff --git a/lib/src/state_management/random_hadith/random_hadith_notifier.dart b/lib/src/state_management/random_hadith/random_hadith_notifier.dart index 96feb9860..6dfadbb72 100644 --- a/lib/src/state_management/random_hadith/random_hadith_notifier.dart +++ b/lib/src/state_management/random_hadith/random_hadith_notifier.dart @@ -12,7 +12,7 @@ import 'package:mawaqit/src/data/repository/random_hadith_impl.dart'; class RandomHadithNotifier extends AsyncNotifier { @override FutureOr build() { - return RandomHadithState(hadith: ''); + return RandomHadithState(hadith: '', language: ''); } Future getRandomHadith({ @@ -22,7 +22,7 @@ class RandomHadithNotifier extends AsyncNotifier { state = await AsyncValue.guard(() async { final randomHadithUseCase = await ref.read(randomHadithUseCaseProvider.future); final hadith = await randomHadithUseCase.getRandomHadith(language: language); - return RandomHadithState(hadith: hadith); + return RandomHadithState(hadith: hadith, language: language); }); } @@ -42,6 +42,14 @@ class RandomHadithNotifier extends AsyncNotifier { return Future.value(state.value); }); } + + Future ensureHadithLanguage() async { + final prefs = await SharedPreferences.getInstance(); + final language = prefs.getString(RandomHadithConstant.kHadithLanguage) ?? AppLanguage().hadithLanguage; + // Ensure hadiths are cached during initialization + final randomHadithRepository = await ref.read(randomHadithRepositoryProvider.future); + await randomHadithRepository.ensureHadithsAreCached(language); + } } final randomHadithNotifierProvider = diff --git a/lib/src/state_management/random_hadith/random_hadith_state.dart b/lib/src/state_management/random_hadith/random_hadith_state.dart index 608c53ff5..daaeb43a5 100644 --- a/lib/src/state_management/random_hadith/random_hadith_state.dart +++ b/lib/src/state_management/random_hadith/random_hadith_state.dart @@ -2,18 +2,24 @@ import 'package:equatable/equatable.dart'; class RandomHadithState extends Equatable { final String hadith; + final String language; RandomHadithState({ required this.hadith, + required this.language, }); - RandomHadithState.initial() : hadith = ''; + RandomHadithState.initial() + : hadith = '', + language = ''; RandomHadithState copyWith({ String? hadith, + String? language, }) { return RandomHadithState( hadith: hadith ?? this.hadith, + language: language ?? this.language, ); } From f0a3b63473ded08092f135ac40200a359a3f043a Mon Sep 17 00:00:00 2001 From: Ibrahim ZEHHAF <97339607+ibrahim-zehhaf-mawaqit@users.noreply.github.com> Date: Sun, 15 Dec 2024 18:02:54 +0100 Subject: [PATCH 26/26] New Crowdin updates (#1467) * 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 * New translations intl_en.arb (pt) * New translations intl_en.arb (pt) * New translations intl_en.arb (Portuguese, Brazilian) * New translations intl_en.arb (Italian) * New translations intl_en.arb (Italian) * New translations intl_en.arb (Italian) * New translations intl_en.arb (Italian) * fix merge conflict --------- Co-authored-by: Yassin Nouh <70436855+YassinNouh21@users.noreply.github.com> Co-authored-by: Ghassen Ben Zahra --- lib/l10n/intl_it.arb | 310 ++++++++++++++--- lib/l10n/intl_pt.arb | 317 +++++++++--------- .../quran/widget/download_quran_popup.dart | 9 + pubspec.yaml | 2 + 4 files changed, 446 insertions(+), 192 deletions(-) diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index 01253c689..37ad0a401 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -1,50 +1,51 @@ { "home": "Pagina iniziale", - "share": "Condividi", + "share": "Condividere", "about": "Info su", "rate": "Votaci", "languages": "Lingue", "appLang": "Lingua Dell'Applicazione", "descLang": "Per favore scegli la lingua preferita", + "hadithLangDesc": "Questo sovrascrive la tua scelta nella console di amministrazione, puoi scegliere una lingua diversa dallo schermo", "whoops": "Cavolo!", "noInternet": "Nessuna connessione internet", - "tryAgain": "Prova di nuovo", - "closeApp": "Chiudi app", - "quit": "Uscire", + "tryAgain": "Riprova", + "closeApp": "Chiudi l'app", + "quit": "Esci", "forceStaging": "Passa alla messa in scena", - "disableStaging": "Passare alla produzione", - "sureCloseApp": "Sei sicuro di che voler uscire dall'applicazione?", + "disableStaging": "Passa alla modalità di produzione", + "sureCloseApp": "Sei sicuro di voler uscire dall'applicazione?", "ok": "OK", - "cancel": "Annullare", + "cancel": "Annulla", "darkMode": "Modalità scura", - "lightMode": "Modalità luce", + "lightMode": "Modalità chiara", "changeMosque": "Cambia Moschea", - "in1": "dopo", + "in1": "in", "sec": "Sec", "online": "Online", - "missingMosqueId": "Manca l'ID MAWAQIT o l'ID MOSCHEA", - "mosqueIdIsNotValid": "{mosqueId} non è un ID moschea valido", - "selectMosqueId": "Per favore inserisci il tuo Mosque ID", + "missingMosqueId": "Manca l'ID MAWAQIT o l'ID della MOSCHEA", + "mosqueIdIsNotValid": "Siamo spiacenti, ma {mosqueId} non è un ID valido per la moschea.", + "selectMosqueId": "Per favore inserisci il tuo ID Moschea", "mawaqitWelcome": "Benvenuto a MAWAQIT", - "mawaqitDesc": "Assalamu Alaikom e Baraka'Allah fikom per aver scelto MAWAQIT, la prima rete di moschee intelligenti al mondo, utilizzata da milioni di musulmani in tutto il mondo in oltre 85 Paesi dal 2016.\n\nLe offriamo la più avanzata visualizzazione di moschee intelligenti, disponibile su più dispositivi (cellulari, smartwatch, schermi TV), senza raccogliere o condividere i suoi dati personali.\n\nSostenga questo progetto benedetto qui: https://donate.mawaqit.net\n\nSiamo un'organizzazione senza scopo di lucro e questo progetto è un \"Waqf fi'sabili Allah\" (dotazione dedicata).\n\nLe sue donazioni mantengono questo progetto a disposizione di chiunque, ovunque, in modo totalmente GRATUITO, senza pubblicità e senza abbonamenti mensili.\n\nQuesto progetto non sarebbe possibile senza l'aiuto di Allah, che ha riunito una comunità di volontari appassionati e di talento, che lavorano giorno e notte per offrirle il miglior servizio possibile e un sistema all'avanguardia disponibile 24 ore su 24, 7 giorni su 7.\n\nPrenda in considerazione l'idea di fare una donazione per mantenere in vita questo progetto benedetto. Baraka'Allah fikom per la sua fiducia e il suo sostegno costanti.", + "mawaqitDesc": "Assalamu Alaikum wa Baraka'Allah fikum per aver scelto MAWAQIT, la prima rete di moschee intelligenti al mondo, utilizzata da milioni di musulmani in oltre 85 Paesi dal 2016.\n\nMAWAQIT offre la più avanzata visualizzazione delle moschee intelligenti, disponibile su diversi dispositivi (cellulari, smartwatch, schermi TV), senza raccogliere né condividere i dati personali dell'utente.\n\nSostieni questo progetto benedetto qui: https://donate.mawaqit.net\n\nSi tratta di un'organizzazione senza scopo di lucro e questo progetto è un \"Waqf fi'sabili Allah\" (dotazione dedicata).\n\nLe donazioni permettono di mantenere questo progetto a disposizione di chiunque, ovunque, in modo totalmente GRATUITO, senza pubblicità né abbonamenti mensili.\n\nQuesto progetto non sarebbe possibile senza l'aiuto di Allah, che ha riunito una comunità di volontari appassionati e di talento, impegnati giorno e notte per offrire il miglior servizio possibile e un sistema all'avanguardia, disponibile 24 ore su 24, 7 giorni su 7.\n\nÈ possibile contribuire con una donazione per mantenere vivo questo progetto benedetto. Baraka'Allah fikum per la fiducia e il costante sostegno.", "privacyPolicy": "Politica sulla Riservatezza", "termsOfService": "Condizioni del Servizio", "installationGuide": "Guida all'installazione", "drawerTitle": "MAWAQIT", - "drawerDesc": "Connecting Muslims to Mosques", - "backendError": "Spiacenti, non siamo riusciti a connetterci al server.\nVerifica la connettività Internet o riprova più tardi.", + "drawerDesc": "Connettere i musulmani alle moschee", + "backendError": "Siamo spiacenti, non siamo riusciti a connetterci al server. Verifica la connessione a Internet o riprova più tardi.", "selectWithMosqueId": "Provi: 256, è l'ID della 'Grande Mosquée de Paris'", "searchForMosque": "Quale Moschea sta cercando? (ID, Nome, Città, Codice postale...)", "searchMosque": "Cerca una Moschea", "mosqueNameError": "Inserisci il nome della Moschea", - "slugError": "Non è uno slug di moschea valido", - "doYouKnowMosqueId": "Conosci il tuo ID di installazione o il tuo ID della Moschea?", - "yes": "Si", + "slugError": "Slug della moschea non valido", + "doYouKnowMosqueId": "Conosci il tuo ID d'installazione o l'ID della Moschea?", + "yes": "Sì", "no": "No", "networkStatus": "Stato della Rete", "mosqueNoMore": "Nessun altro risultato", "mosqueNoResults": "Nessun risultato", - "offline": "Non in linea", + "offline": "Offline", "imsak": "Imsak", "jumua": "Jumua", "duhr": "Dhohr", @@ -52,14 +53,14 @@ "asr": "Asr", "maghrib": "Maghrib", "isha": "Isha", - "afterAdhanHadithTitle": "Dopo l'adhan Du`aa", + "afterAdhanHadithTitle": "Du`aa dopo l'adhan", "afterSalahHadith": "O Allah, Signore di questa chiamata perfetta e della preghiera che sta per essere celebrata,\n accorda a Muhammad il mezzo della intercessione (al-Wasilah) e l’eccellenza (al-fadîla) ed\n elevalo al nobile rango che Tu gli hai promesso", "alIqama": "Al Iqama", "alAdhan": "Al-Athan", - "turnOfPhones": "La preghiamo di mettere i suoi telefoni in modalità silenziosa", - "iqamaIn": "Iqama in", + "turnOfPhones": "Per favore, metti il tuo cellulare in modalità silenziosa.", + "iqamaIn": "Iqama tra", "alAthkar": "Al-Athkar", - "azkarList0": "Chiedo perdono a Dio, chiedo perdono a Dio, chiedo perdono a Dio. Grazie e adorarti bene", + "azkarList0": "Astaghfiru Allah, Astaghfiru Allah, Astaghfiru Allah Allahumma anta Essalam wa mineka Essalam, tabarakta ya dhal djalali wel ikram Allahumma A`inni `ala dhikrika wa chukrika wa husni `ibadatik", "@azkarList0": { "description": "أَسْـتَغْفِرُ الله، أَسْـتَغْفِرُ الله، أَسْـتَغْفِرُ الله اللّهُـمَّ أَنْـتَ السَّلامُ ، وَمِـنْكَ السَّلام ، تَبارَكْتَ يا ذا الجَـلالِ وَالإِكْـرام اللَّهُمَّ أَعِنِّي عَلَى ذِكْرِكَ وَشُكْرِكَ وَحُسْنِ عِبَادَتِكَ" }, @@ -67,19 +68,19 @@ "@azkarList1": { "description": "سُـبْحانَ اللهِ، والحَمْـدُ لله، واللهُ أكْـبَر 33 مرة لا إِلَٰهَ إلاّ اللّهُ وَحْـدَهُ لا شريكَ لهُ، لهُ الملكُ ولهُ الحَمْد، وهُوَ على كُلّ شَيءٍ قَـدير" }, - "azkarList2": "", + "azkarList2": "بِسۡمِ ٱللَّهِ ٱلرَّحۡمَٰنِ ٱلرَّحِيمِ قُلۡ أَعُوذُ بِرَبِّ ٱلنَّاسِ ، مَلِكِ ٱلنَّاسِ ، إِلَٰهِ ٱلنَّاسِ ، مِن شَرِّ ٱلۡوَسۡوَاسِ ٱلۡخَنَّاسِ ، ٱلَّذِي يُوَسۡوِسُ فِي صُدُورِ ٱلنَّاسِ ، مِنَ ٱلۡجِنَّةِ وَٱلنَّاس", "@azkarList2": { "description": "بِسۡمِ ٱللَّهِ ٱلرَّحۡمَٰنِ ٱلرَّحِيمِ قُلۡ أَعُوذُ بِرَبِّ ٱلنَّاسِ ، مَلِكِ ٱلنَّاسِ ، إِلَٰهِ ٱلنَّاسِ ، مِن شَرِّ ٱلۡوَسۡوَاسِ ٱلۡخَنَّاسِ ، ٱلَّذِي يُوَسۡوِسُ فِي صُدُورِ ٱلنَّاسِ ، مِنَ ٱلۡجِنَّةِ وَٱلنَّاس" }, - "azkarList3": "", + "azkarList3": "بِسۡمِ ٱللَّهِ ٱلرَّحۡمَٰنِ ٱلرَّحِيمِ قُلۡ أَعُوذُ بِرَبِّ ٱلۡفَلَقِ ، مِن شَرِّ مَا خَلَقَ ، وَمِن شَرِّ غَاسِقٍ إِذَا وَقَبَ ، وَمِن شَرِ ٱلنَّفَّٰثَٰتِ فِي ٱلۡعُقَدِ ، وَمِن شَرِّ حَاسِدٍ إِذَا حَسَدَ", "@azkarList3": { "description": "بِسۡمِ ٱللَّهِ ٱلرَّحۡمَٰنِ ٱلرَّحِيمِقُلۡ أَعُوذُ بِرَبِّ ٱلۡفَلَقِ ، مِن شَرِّ مَا خَلَقَ ، وَمِن شَرِّ غَاسِقٍ إِذَا وَقَبَ ، وَمِن شَرِ ٱلنَّفَّٰثَٰتِ فِي ٱلۡعُقَدِ ، وَمِن شَرِّ حَاسِدٍ إِذَا حَسَدَ" }, - "azkarList4": "", + "azkarList4": "بِسۡمِ ٱللَّهِ ٱلرَّحۡمَٰنِ ٱلرَّحِيمِ قُلۡ هُوَ ٱللَّهُ أَحَدٌ ، ٱللَّهُ ٱلصَّمَدُ ، لَمۡ يَلِدۡ وَلَمۡ يُولَدۡ ، وَلَمۡ يَكُن لَّهُۥ كُفُوًا أَحَدُۢ", "@azkarList4": { "description": "بِسۡمِ ٱللَّهِ ٱلرَّحۡمَٰنِ ٱلرَّحِيمِ قُلۡ هُوَ ٱللَّهُ أَحَدٌ ، ٱللَّهُ ٱلصَّمَدُ ، لَمۡ يَلِدۡ وَلَمۡ يُولَدۡ ، وَلَمۡ يَكُن لَّهُۥ كُفُوًا أَحَدُۢ" }, - "azkarList5": "", + "azkarList5": "ٱللَّهُ لَآ إِلَٰهَ إِلَّا هُوَ ٱلۡحَيُّ ٱلۡقَيُّومُۚ لَا تَأۡخُذُهُۥ سِنَةٞ وَلَا نَوۡمٞۚ لَّهُۥ مَا فِي ٱلسَّمَٰوَٰتِ وَمَا فِي ٱلۡأَرۡضِۗ مَن ذَا ٱلَّذِي يَشۡفَعُ عِندَهُۥٓ إِلَّا بِإِذۡنِهِۦۚ يَعۡلَمُ مَا بَيۡنَ أَيۡدِيهِمۡ وَمَا خَلۡفَهُمۡۖ وَلَا يُحِيطُونَ بِشَيۡءٖ مِّنۡ عِلۡمِهِۦٓ إِلَّا بِمَا شَآءَۚ وَسِعَ كُرۡسِيُّهُ ٱلسَّمَٰوَٰتِ وَٱلۡأَرۡضَۖ وَلَا يَ‍ُٔودُهُۥ حِفۡظُهُمَاۚ وَهُوَ ٱلۡعَلِيُّ ٱلۡعَظِيمُ", "@azkarList5": { "description": "ٱللَّهُ لَآ إِلَٰهَ إِلَّا هُوَ ٱلۡحَيُّ ٱلۡقَيُّومُۚ لَا تَأۡخُذُهُۥ سِنَةٞ وَلَا نَوۡمٞۚ لَّهُۥ مَا فِي ٱلسَّمَٰوَٰتِ وَمَا فِي ٱلۡأَرۡضِۗ مَن ذَا ٱلَّذِي يَشۡفَعُ عِندَهُۥٓ إِلَّا بِإِذۡنِهِۦۚ يَعۡلَمُ مَا بَيۡنَ أَيۡدِيهِمۡ وَمَا خَلۡفَهُمۡۖ وَلَا يُحِيطُونَ بِشَيۡءٖ مِّنۡ عِلۡمِهِۦٓ إِلَّا بِمَا شَآءَۚ وَسِعَ كُرۡسِيُّهُ ٱلسَّمَٰوَٰتِ وَٱلۡأَرۡضَۖ وَلَا يَ‍ُٔودُهُۥ حِفۡظُهُمَاۚ وَهُوَ ٱلۡعَلِيُّ ٱلۡعَظِيمُ" }, @@ -87,12 +88,40 @@ "@azkarList6": { "description": "لا إِلَٰهَ إلاّ اللّهُ وحدَهُ لا شريكَ لهُ، لهُ المُـلْكُ ولهُ الحَمْد، وهوَ على كلّ شَيءٍ قَدير، اللّهُـمَّ لا مانِعَ لِما أَعْطَـيْت، وَلا مُعْطِـيَ لِما مَنَـعْت، وَلا يَنْفَـعُ ذا الجَـدِّ مِنْـكَ الجَـد" }, + "azkarList7": "اللهم أنت ربي، لا إله إلا أنت، خلقتني وأنا عبدُك, وأنا على عهدِك ووعدِك ما استطعتُ، أعوذ بك من شر ما صنعتُ، أبوءُ لَكَ بنعمتكَ عَلَيَّ، وأبوء بذنبي، فاغفر لي، فإنه لا يغفرُ الذنوب إلا أنت", + "@azkarList7": { + "description": "اللهم أنت ربي، لا إله إلا أنت، خلقتني وأنا عبدُك, وأنا على عهدِك ووعدِك ما استطعتُ، أعوذ بك من شر ما صنعتُ، أبوءُ لَكَ بنعمتكَ عَلَيَّ، وأبوء بذنبي، فاغفر لي، فإنه لا يغفرُ الذنوب إلا أنت" + }, + "azkarList8": "أصبحنا وأصبح الملك لله، والحمد لله ولا إله إلا الله وحده لا شريك له، له الملك وله الحمد، وهو على كل شيء قدير، أسألك خير ما في هذا اليوم، وخير ما بعده، وأعوذ بك من شر هذا اليوم، وشر ما بعده، وأعوذ بك من الكسل وسوء الكبر، وأعوذ بك من عذاب النار وعذاب القبر", + "@azkarList8": { + "description": "أصبحنا وأصبح الملك لله، والحمد لله ولا إله إلا الله وحده لا شريك له، له الملك وله الحمد، وهو على كل شيء قدير، أسألك خير ما في هذا اليوم، وخير ما بعده، وأعوذ بك من شر هذا اليوم، وشر ما بعده، وأعوذ بك من الكسل وسوء الكبر، وأعوذ بك من عذاب النار وعذاب القبر" + }, + "azkarList9": "اللَّهُمَّ إِنِّي أَصْبَحْتُ أُشْهِدُكَ، وَأُشْهِدُ حَمَلَةَ عَرْشِكَ، وَمَلاَئِكَتِكَ، وَجَمِيعَ خَلْقِكَ، أَنَّكَ أَنْتَ اللَّهُ لَا إِلَهَ إِلاَّ أَنْتَ وَحْدَكَ لاَ شَرِيكَ لَكَ، وَأَنَّ مُحَمَّداً عَبْدُكَ وَرَسُولُكَ |4 volte|. [ وإذا أمسى قال: اللَّهم إني أمسيت...]", + "@azkarList9": { + "description": "اللَّهُمَّ إِنِّي أَصْبَحْتُ أُشْهِدُكَ، وَأُشْهِدُ حَمَلَةَ عَرْشِكَ، وَمَلاَئِكَتِكَ، وَجَمِيعَ خَلْقِكَ، أَنَّكَ أَنْتَ اللَّهُ لَا إِلَهَ إِلاَّ أَنْتَ وَحْدَكَ لاَ شَرِيكَ لَكَ، وَأَنَّ مُحَمَّداً عَبْدُكَ وَرَسُولُكَ |أربعَ مَرَّات|. [ وإذا أمسى قال: اللَّهم إني أمسيت...]" + }, + "azkarList10": "|اللَّهُمَّ عَافِنِي فِي بَدَنِي، اللَّهُمَّ عَافِنِي فِي سَمْعِي، اللَّهُمَّ عَافِنِي فِي بَصَرِي، لاَ إِلَهَ إِلاَّ أَنْتَ. اللَّهُمَّ إِنِّي أَعُوذُ بِكَ مِنَ الْكُفْرِ، وَالفَقْرِ، وَأَعُوذُ بِكَ مِنْ عَذَابِ القَبْرِ، لاَ إِلَهَ إِلاَّ أَنْتَ |ثلاثَ مرَّاتٍ", + "@azkarList10": { + "description": "|اللَّهُمَّ عَافِنِي فِي بَدَنِي، اللَّهُمَّ عَافِنِي فِي سَمْعِي، اللَّهُمَّ عَافِنِي فِي بَصَرِي، لاَ إِلَهَ إِلاَّ أَنْتَ. اللَّهُمَّ إِنِّي أَعُوذُ بِكَ مِنَ الْكُفْرِ، وَالفَقْرِ، وَأَعُوذُ بِكَ مِنْ عَذَابِ القَبْرِ، لاَ إِلَهَ إِلاَّ أَنْتَ |ثلاثَ مرَّاتٍ" + }, + "azkarList11": "|حَسْبِيَ اللَّهُ لاَ إِلَهَ إِلاَّ هُوَ عَلَيهِ تَوَكَّلتُ وَهُوَ رَبُّ الْعَرْشِ الْعَظِيمِ |سَبْعَ مَرّاتٍ", + "@azkarList11": { + "description": "|حَسْبِيَ اللَّهُ لاَ إِلَهَ إِلاَّ هُوَ عَلَيهِ تَوَكَّلتُ وَهُوَ رَبُّ الْعَرْشِ الْعَظِيمِ |سَبْعَ مَرّاتٍ" + }, + "azkarList12": "|رَضِيتُ بِاللَّهِ رَبَّاً، وَبِالْإِسْلاَمِ دِيناً، وَبِمُحَمَّدٍ صلى الله عليه وسلم نَبِيّاً |ثلاثَ مرَّاتٍ", + "@azkarList12": { + "description": "|رَضِيتُ بِاللَّهِ رَبَّاً، وَبِالْإِسْلاَمِ دِيناً، وَبِمُحَمَّدٍ صلى الله عليه وسلم نَبِيّاً |ثلاثَ مرَّاتٍ" + }, + "azkarList13": "|لاَ إِلَهَ إِلاَّ اللَّهُ وَحْدَهُ لاَ شَرِيكَ لَهُ، لَهُ الْمُلْكُ وَلَهُ الْحَمْدُ، وَهُوَ عَلَى كُلِّ شَيْءٍ قَدِيرٌ |عشرَ مرَّاتٍ", + "@azkarList13": { + "description": "|لاَ إِلَهَ إِلاَّ اللَّهُ وَحْدَهُ لاَ شَرِيكَ لَهُ، لَهُ الْمُلْكُ وَلَهُ الْحَمْدُ، وَهُوَ عَلَى كُلِّ شَيْءٍ قَدِيرٌ |عشرَ مرَّات" + }, "jumuaaScreenTitle": "Jumuaa Time", - "jumuaaHadith": "Il Profeta ﷺ (pace e benedizioni di Allah siano su di lui) disse: \"A chi fa le abluzioni in modo perfetto e poi va alla jumua e poi ascolta e tace, viene perdonato ciò che c'è tra quel momento e il venerdì successivo e altri tre giorni, e chi tocca le pietre ha certamente commesso un'inutilità\"", + "jumuaaHadith": "Il Profeta Maometto ﷺ (pace e benedizioni di Allah siano su di lui) disse: \"A chi fa le abluzioni in modo perfetto e poi va alla jumua e poi ascolta e tace, viene perdonato ciò che c'è tra quel momento e il venerdì successivo e altri tre giorni, e chi tocca le pietre ha certamente commesso un'inutilità\"", "shuruk": "Alba", "reset": "Reset", "mosqueNotFoundMessage": "Siamo spiacenti, la sua moschea non è stata trovata, oppure potrebbe essere mancante o temporaneamente disattivata.", - "noInternetMessage": "Non c'è accesso a Internet. Verifichi la sua connettività internet e riprovi. Il suo Wi-Fi o Ethernet è collegato?", + "noInternetMessage": "Non connesso a Internet. Verifichi la sua connettività internet e riprovi. Il suo Wi-Fi o Ethernet è collegato?", "error": "Errore", "mosqueErrorMessage": "Errore della moschea se sei amministratore della moschea contatta il nostro supporto per risolvere questo problema", "muharram": "Muharram", @@ -109,22 +138,22 @@ "dhuAlhijjah": "Dhu al-Hijja", "duaaBetweenSalahAndAdhan": "Anas bin Malik disse: Il Messaggero di Allah ﷺ disse: La supplica non ritorna tra la chiamata alla preghiera e lo stare in piedi per la preghiera.", "salatKhayrMinaNawm": "Assalatu khayrun mina nawm", - "salatElEid": "Salat El Eid", + "salatElEid": "Salat Al Eid", "webView": "Abilita la modalità Legacy", "developersHomeScreen": "Schermata iniziale dello sviluppatore", "onlineHome": "Casa online", "prayerTimes": "Tempi di preghiera", "alerts": "Allarme", - "iqamaaCountDown": "Conto alla rovescia di Iqamaa", - "afterAdhanHadith": "Dopo l'Adhan Hadith", - "afterSalahAzkar": "Dopo Salah Azkar", + "iqamaaCountDown": "Conto alla rovescia dell' Iqama", + "afterAdhanHadith": "Hadith dopo l'azan", + "afterSalahAzkar": "Azkar dopo la preghiera", "iqama": "Iqama", "randomHadith": "Hadith a caso", "announcement": "Annunci", "jumuaaLive": "Jumuaa [Streaming in diretta]", "showSecondaryScreen": "Utilizzare come schermo secondario (per gli annunci)", "normalScreen": "Utilizzare come schermata principale", - "duaaRemainder": "Duaa Rimanente", + "duaaRemainder": "Du'aa rimanente", "fajrWakeUp": "Sveglia Fajr", "changeLanguage": "Cambiare la lingua", "forceScreen": "Schermo di forza", @@ -135,21 +164,220 @@ "mainScreenOrSecondaryScreenEXPLINATION": "Vuoi installare questa schermata nella sala di preghiera principale (stanza di preghiera degli uomini) ?", "mainScreen": "Schermo principale", "secondaryScreen": "Schermo secondario", - "duaaElEftar": "Duaa El Eftar", + "duaaElEftar": "Duaa Al Iftar", "announcementOnlyMode": "Annunci", "normalMode": "Modalità Normale", "announcementOnlyModeEXPLINATION": "Scelga se il suo schermo visualizzerà gli annunci per tutto il tempo, questo può essere utile se installa lo schermo all'ingresso, ad esempio.", - "duaaElEftarText": "", + "duaaElEftarText": "اللهم اني لگ صمت وعلى رزقك افطرت واليك انبت وعليگ توكلت ذهب الظما وابتلت العروق وثبت الاجر انشاء الله", "@duaaElEftarText": { "description": "اللهم اني لگ صمت وعلى رزقك افطرت واليك انبت وعليگ توكلت ذهب الظما وابتلت العروق وثبت الاجر انشاء الله" }, - "secondaryScreenExplanation": "Per una sala di preghiera secondaria (sala delle donne o un altro piano, per esempio), questa schermata mostrerà la jumua in live-streaming", - "mainScreenExplanation": "Per la sala della moschea principale, questa schermata non mostrerà il live-streaming della jumua.", + "secondaryScreenExplanation": "Per una sala di preghiera secondaria (sala delle donne o un altro piano, per esempio), questa schermata mostrerà la preghiera del jumua in diretta.", + "mainScreenExplanation": "Per la sala della moschea principale, questa schermata non mostrerà la diretta della preghiera del jumua.", "normalModeExplanation": "Mostrerà la schermata normale con gli orari di preghiera e gli annunci.", - "announcementOnlyModeExplanation": "Mostrerà gli annunci in ogni momento", + "announcementOnlyModeExplanation": "Mostrerà continuamente gli annunci", + "orientation": "Orientamento", + "selectYourMawaqitTvAppOrientation": "Seleziona l'orientamento dell'app mawaqit TV", + "deviceDefault": "Dispositivo Predefinito", + "deviceDefaultBTNDescription": "Mawaqit selezionerà automaticamente l'orientamento predefinito in base all'orientamento dello schermo", + "portrait": "Verticale", + "portraitBTNDescription": "Per moschee con spazi ridotti consigliamo l'orientamento verticale", + "landscape": "Orizzontale", + "landscapeBTNDescription": "L'orientamento orizzontale è il sistema principale dell'app Mawaqit Tv, raccomandato per la maggior parte delle moschee", "eidMubarak": "Eid Mubarak", "takbeerAleidText": "Allahu Akbar, Allahu Akbar, Allahu Akbar, la ilaha illa Allah, Allahu Akbar, Allahu Akbar, wa lillahi al-hamd", "settings": "Impostazioni", - "applicationModes": "Modalità di applicazione", - "ifYouAreFacingAnIssueWithTheAppActivateThis": "Se riscontra un problema con l'applicazione attivi questa opzione" + "applicationModes": "Modalità dell'applicazione", + "ifYouAreFacingAnIssueWithTheAppActivateThis": "Se riscontra un problema con l'applicazione attivi questa opzione", + "hijriAdjustments": "Regolazione Hijri locale", + "hijriAdjustmentsDescription": "Regola localmente la data dell'hijri nel tuo dispositivo. Questo non influenzerà le impostazioni della moschea online", + "backoffice_default": "Impostazioni predefinite del backoffice", + "recommended": "Consigliato", + "sabah": "Sabah", + "randomHadithLanguage": "Lingua casuale hadith", + "en": "Inglese", + "fr": "Francese", + "ar": "Arabo", + "tr": "Turca", + "de": "Tedesco", + "es": "Spagnolo", + "pt": "Portoghese", + "nl": "Olandese", + "fr_ar": "Francese e Arabo", + "en_ar": "Inglese e Arabo", + "de_ar": "Tedesco e Arabo", + "ta_ar": "Tamil e Arabo", + "tr_ar": "Turco e Arabo", + "es_ar": "Spagnolo e Arabo", + "pt_ar": "Portoghese e Arabo", + "nl_ar": "Olandese e Arabo", + "connectToChangeHadith": "Connettiti a internet per cambiare la lingua dei hadith.", + "retry": "Riprova", + "timeSetting": "Configurare l'orario", + "timeSettingDesc": "Imposta un nome personalizzato", + "selectedTime": "L'ora corrente selezionata", + "confirmation": "Conferma", + "confirmationMessage": "Sei sicuro di voler utilizzare l'ora del dispositivo?", + "useDeviceTime": "Usa l'ora del dispositivo", + "selectTime": "Seleziona l'ora", + "previous": "Precedente", + "appTimezone": "Fuso orario dell'applicazione", + "descTimezone": "Seleziona il tuo fuso orario per ottenere tempi di preghiera accurato.", + "appWifi": "Connettiti al WiFi", + "descWifi": "Connettiti al WiFi preferito", + "searchCountries": "Cerca paesi", + "scanAgain": "Scansiona di nuovo", + "noScannedResultsFound": "Non sono stati trovati punti di accesso nelle vicinanze", + "connect": "Connetti", + "wifiPassword": "Password", + "skip": "Salta", + "noSSID": "**SSID nascosto**", + "close": "Chiudi", + "search": "Cerca", + "wifiSuccess": "Connesso con successo a WiFi.", + "wifiFailure": "Impossibile connettersi al Wi-Fi", + "timezoneSuccess": "Fuso orario impostato con successo.", + "timezoneFailure": "Impossibile impostare il fuso orario.", + "screenLock": "Accensione/spegnimento dello schermo", + "screenLockConfig": "Configura schermo acceso/spento", + "screenLockMode": "Modalità accensione/spegnimento dello schermo", + "screenLockDesc": "Accendi/spegni la TV prima e dopo ogni preghiera per risparmiare energia.", + "screenLockDesc2": "Questa funzione accende/spegne il dispositivo prima e dopo ogni adhan della preghiera.", + "before": "Minuti prima di ogni preghiera", + "after": "Minuti dopo ogni preghiera", + "updateAvailable": "Aggiornamento disponibile", + "seeMore": "Vedi altro", + "whatIsNew": "Notizie", + "update": "Aggiornare", + "automaticUpdate": "Notifica aggiornamento", + "automaticUpdateDescription": "Abilita l'aggiornamento delle notifiche per ricevere le ultime funzionalità e miglioramenti", + "checkInternetLegacyMode": "Devi connetterti a internet per utilizzare la modalità legacy", + "powerOnScreen": "Accendi lo schermo", + "powerOffScreen": "Spegni lo schermo", + "deviceSettings": "Impostazioni Dispositivo", + "later": "Dopo", + "downloadQuran": "Scarica Corano", + "quran": "Corano", + "askDownloadQuran": "Vuoi scaricare il Corano?", + "download": "Scaricare", + "downloadingQuran": "Download del Corano", + "extractingQuran": "Estrazione del Corano", + "updatedQuran": "Corano aggiornato", + "quranLatestVersion": "Il Corano è aggiornato", + "quranUpdatedVersion": "La versione aggiornata del Corano è: {version}", + "quranIsUpdated": "Il Corano è aggiornato", + "quranDownloaded": "Corano scaricato", + "quranIsAlreadyDownloaded": "Il Corano è già scaricato", + "chooseReciter": "Scegli Recitatore", + "reciteType": "Tipo di recitazione", + "readingMode": "Voglio leggere", + "listeningMode": "Voglio ascoltare", + "quranReadingPage": "Pagina {leftPage} - {rightPage} / {totalPages}", + "@quranReadingPage": { + "description": "Placeholder text for displaying Quran reading page numbers", + "placeholders": { + "leftPage": { + "type": "int", + "example": "1" + }, + "rightPage": { + "type": "int", + "example": "2" + }, + "totalPages": { + "type": "int", + "example": "604" + } + } + }, + "quranReadingPagePortrait": "Pagina {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": "Scegli la pagina", + "checkingForUpdates": "Controllo aggiornamenti in corso...", + "chooseQuranType": "Scegli corano", + "hafs": "Hafs", + "warsh": "Warsh", + "favorites": "Preferiti", + "allReciters": "Tutti I Recitatori", + "reciterAddedToFavorites": "Recitatore {name} aggiunto ai preferiti", + "@reciterAddedToFavorites": { + "description": "Message shown when a reciter is added to favorites", + "placeholders": { + "name": { + "type": "String", + "example": "Abdul Basit" + } + } + }, + "reciterRemovedFromFavorites": "Recitatore {name} rimosso dai preferiti", + "@reciterRemovedFromFavorites": { + "description": "Message shown when a reciter is removed from favorites", + "placeholders": { + "name": { + "type": "String", + "example": "Abdul Basit" + } + } + }, + "noFavoriteReciters": "Nessun recitatore preferito. Prova ad aggiungerne uno alla lista", + "@noFavoriteReciters": { + "description": "Message shown when there are no favorite reciters" + }, + "noReciterSearchResult": "Nessun risultato trovato per la tua ricerca", + "searchForReciter": "Cerca un recitatore", + "downloadAllSuwarSuccessfully": "L'intero corano è stato scaricato", + "noSuwarDownload": "Nessuna nuova fonte da scaricare", + "connectDownloadQuran": "Connettiti a Internet per scaricare", + "playInOnlineModeQuran": "Connettiti a Internet per avviare.", + "downloaded": "Scaricato", + "switchQuranType": "Vai a {name}", + "@switchQuranType": { + "description": "Message shown when a reciter is added to favorites", + "placeholders": { + "name": { + "type": "String", + "example": "Warsh" + } + } + }, + "surahSelector": "Seleziona Surah", + "checkForUpdates": "Controlla gli aggiornamenti", + "checkForNewVersion": "Verifica se è disponibile una nuova versione", + "wouldYouLikeToUpdate": "Vuoi aggiornare l'app?", + "updateCompleted": "Aggiornamento completato con successo!", + "noUpdates": "Nessun Aggiornamento", + "usingLatestVersion": "Stai usando l'ultima versione.", + "updateCancelled": "Aggiornamento annullato", + "checkingUpdates": "Controllo degli aggiornamenti in corso...", + "downloadingUpdate": "Scaricamento aggiornamento...", + "installingUpdate": "Installazione dell'aggiornamento in corso...", + "updateCompletedSuccessfully": "Aggiornamento completato con successo", + "updateFailed": "Aggiornamento non riuscito", + "checkInternetUpdate": "Devi connetterti a internet per controllare nuovi aggiornamenti", + "appUpdateAvailable": "La tua app sta eseguendo la versione {currentVersion}. Un nuovo aggiornamento (versione {updatedVersion}) è disponibile con le ultime funzionalità e miglioramenti.", + "@appUpdateAvailable": { + "description": "Placeholder text for displaying update available message", + "placeholders": { + "currentVersion": { + "type": "String", + "example": "1" + }, + "updatedVersion": { + "type": "String", + "example": "604" + } + } + } } \ No newline at end of file diff --git a/lib/l10n/intl_pt.arb b/lib/l10n/intl_pt.arb index 92f2df2e5..a72509415 100644 --- a/lib/l10n/intl_pt.arb +++ b/lib/l10n/intl_pt.arb @@ -1,51 +1,51 @@ { - "home": "Ecrã inicial", - "share": "Partilhar", + "home": "Início", + "share": "Compartilhar", "about": "Sobre", - "rate": "Avalie-nos", + "rate": "Avaliar", "languages": "Idiomas", - "appLang": "Idioma da aplicação", - "descLang": "Selecione o seu idioma preferido", - "hadithLangDesc": "Pode escolher um idioma diferente para este ecrã", + "appLang": "Idioma do aplicativo", + "descLang": "Selecione seu idioma preferido", + "hadithLangDesc": "Isso substituirá sua escolha no console de administrador, você pode selecionar um idioma para esta tela", "whoops": "Ops!", "noInternet": "Sem conexão à internet", - "tryAgain": "Tente novamente", - "closeApp": "Fechar aplicação", + "tryAgain": "Tentar novamente", + "closeApp": "Fechar aplicativo", "quit": "Sair", - "forceStaging": "Trocar para ambiente de teste", - "disableStaging": "Mudar para modo operacional", - "sureCloseApp": "Tem a certeza que pretende sair da aplicação?", + "forceStaging": "Alterar para encenação", + "disableStaging": "Alterar para modo de produção", + "sureCloseApp": "Você tem certeza que quer sair do aplicativo?", "ok": "OK", "cancel": "CANCELAR", - "darkMode": "Modo escuro", - "lightMode": "Modo claro", - "changeMosque": "Alterar Mesquita", - "in1": "depois", + "darkMode": "Tema escuro", + "lightMode": "Tema claro", + "changeMosque": "Alterar mesquita", + "in1": "em", "sec": "Seg", - "online": "Disponível", - "missingMosqueId": "MAWAQIT #ID ou MOSQUE #ID em falta", - "mosqueIdIsNotValid": "{mosqueId} não é um ID de Mesquita válido", - "selectMosqueId": "Por favor, insira o ID da Mesquita", + "online": "Online", + "missingMosqueId": "MAWAQIT #ID ou MOSQUE #ID faltando", + "mosqueIdIsNotValid": "{mosqueId} não é um ID de mesquita válido", + "selectMosqueId": "Insira o ID da mesquita", "mawaqitWelcome": "Bem-vindo ao MAWAQIT", - "mawaqitDesc": "Assalamu Alaikom, e Baraka'Allah fikom por terem escolhido MAWAQIT, a primeira e #1 Rede de Mesquitas Inteligentes do Mundo, utilizada por milhões de muçulmanos em todo o mundo e em mais de 85 países desde 2016.\n\nFornecemos-lhe o mais avançado Ecrã de Mesquita Inteligente, disponível em múltiplos Dispositivos (Móvel, Smartwatch, ecrãs de TV), sem recolher ou partilhar os seus dados pessoais.\n\nPor favor, apoie este projecto abençoado aqui: https://donate.mawaqit.net\n\nSomos uma organização sem fins lucrativos, e este projecto é um \"Waqf fi sabili Allah\" (doação no caminho de Allah).\n\nAs suas doações mantêm este projecto disponível para qualquer pessoa, em qualquer lugar, totalmente GRATUITO, sem ANÚNCIOS, e não há SUBSCRIÇÃO MENSAL.\n\nEste projecto não seria possível sem a ajuda de Allah que reuniu uma comunidade de voluntários talentosos e apaixonados, que trabalham dia e noite para lhe prestar o melhor serviço possível, e um sistema de última geração disponível 24/7.\n\nPor favor, considere fazer uma doação para manter este projecto abençoado. Baraka'Allah fikom pela sua contínua confiança e apoio.", + "mawaqitDesc": "Assalamu Alaikom e Baraka'Allah fikom escolheu MAWAQIT como a primeira rede inteligente de mesquita, usado por milhões de muçulmanos em todo o mundo, em mais 86 países em 2016.\n\nNós lhe fornecemos com a melhor exibição avançada e inteligente de mesquita, disponível em vários dispositivos (Mobile, Smartwatch, TV Screens), sem coletar ou compartilhar seus dados.\n\nApoie esse abençoado projeto aqui:\nhttps://donate.mawaqit.net\n\nSomos uma organização sem fins lucrativos, e esse projeto é uma “Waqf fi'sabili Allah” (doação dedicada).\n\nSuas doações tornam este projeto disponível para qualquer pessoa, em qualquer lugar, totalmente GRÁTIS de qualquer cobrança, sendo SEM ANÚNCIOS, e SEM INSCRIÇÕES MENSAIS.\n\nEste projeto não seria possível sem a ajuda de Allah que trouxe uma comunidade apaixonada de voluntários apaixonados mágicos, que trabalham dia e noite para lhe fornecer o melhor serviço possível, e um estado de sistema final disponível 24/7.\n\nConsidere continuar doando para manter este projeto abençoado continuando. Baraka'Allah fikom pela sua confiança e apoio contínuo.", "privacyPolicy": "Política de privacidade", - "termsOfService": "Termos de Utilização", + "termsOfService": "Termos de serviço", "installationGuide": "Guia de instalação", "drawerTitle": "MAWAQIT", - "drawerDesc": "Connecting Muslims to Mosques", - "backendError": "Desculpe, não foi possível estabelecer ligação com o servidor.\nPor favor, verifique a sua ligação à Internet ou tente novamente mais tarde.", - "selectWithMosqueId": "Experimente: 256, É o ID da 'Grande Mosquée de Paris'", - "searchForMosque": "Que Mesquita procura? (ID, Nome, Cidade, Código Postal...)", - "searchMosque": "Procure uma Mesquita", - "mosqueNameError": "Digite o nome da Mesquita", + "drawerDesc": "Conectando os muçulmanos às mesquitas", + "backendError": "Desculpe, não podemos conectar ao servidor.\nVerifique a conexão com a rede e tente novamente mais tarde.", + "selectWithMosqueId": "Tente: 256 é o ID do 'Grande Mosquée de Paris'", + "searchForMosque": "Que mesquita você está procurando? (ID, Nome, Cidade, Código Postal...)", + "searchMosque": "Busque uma mesquita", + "mosqueNameError": "Inserir nome da mesquita", "slugError": "Não é um slug de mesquita válido", - "doYouKnowMosqueId": "Você sabe o ID de instalação ou o ID da sua Mesquita?", + "doYouKnowMosqueId": "Você sabe o ID de instalação ou ID da mesquita?", "yes": "Sim", "no": "Não", "networkStatus": "Estado da rede", "mosqueNoMore": "Sem mais resultados", "mosqueNoResults": "Sem resultados", - "offline": "Off-line", + "offline": "Offline", "imsak": "Sehri", "jumua": "Jumu'ah", "duhr": "Zuhr", @@ -54,76 +54,76 @@ "maghrib": "Magrib", "isha": "Ishá", "afterAdhanHadithTitle": "Duá depois de Azán", - "afterSalahHadith": "Ó Allah! Senhor deste chamamento perfeito e do Salah que lhe será oferecido, conceda a Muhammad (Que a paz e a misericórdia de Allah esteja sobre ele) a intercessão, graça e alta posição. Elevai-o ao lugar glorioso (Maqame-Mahmud), que lhe prometeste.", + "afterSalahHadith": "Ó Allah! salvador deste chamado perfeito (Da'wah) e do orador estabilizado (As-Salat), conceda a Muhammad o Wasilah e superioridade, e eleve-o a uma posição louvável que você o prometeu", "alIqama": "Jamaat", "alAdhan": "Azán", - "turnOfPhones": "Por favor, coloque o seu telemóvel em Silêncio", + "turnOfPhones": "Por favor, coloquem seus celulares no modo silencioso", "iqamaIn": "Jamaat em", "alAthkar": "Zikr", - "azkarList0": "Peço perdão a Allah, Peço perdão a Allah, Peço perdão a Allah, Ó Allah! Sois Aquele que concede a Paz e a Paz provém somente de Vós. Sois o Senhor da Bênção, Ó o Senhor da Majestade e Benevolência! Ó Allah! Ajuda-me na Tua recordação, no Teu agradecimento e na Tua boa adoração.", + "azkarList0": "Peço desculpas a Allah, peço desculpas a Allah, peço desculpas a Allah, Ó Allah! Você é aquele que concede a paz e a paz vem somente de você. Você é o senhor da benção, Ó o senhor da majestade e benevolência! Ó Allah! Me ajuda em sua recordação, em seu agradecimento e sua boa adoração", "@azkarList0": { "description": "أَسْـتَغْفِرُ الله، أَسْـتَغْفِرُ الله، أَسْـتَغْفِرُ الله اللّهُـمَّ أَنْـتَ السَّلامُ ، وَمِـنْكَ السَّلام ، تَبارَكْتَ يا ذا الجَـلالِ وَالإِكْـرام اللَّهُمَّ أَعِنِّي عَلَى ذِكْرِكَ وَشُكْرِكَ وَحُسْنِ عِبَادَتِكَ" }, - "azkarList1": "Puro de todos os defeitos é Allah, todos os louvores são para Allah e Allah é o maior (33 vezes) Ninguém é digno de adoração excepto Allah, o Único, sem parceiro nenhum, somente a Ele pertence o reinado e somente para Ele são todas as glórias; todo o bem está no Seu poder e Ele tem o poder sobre todas as coisas.", + "azkarList1": "Puro de qualquer defeito é Allah, todos os louvores são a Allah e Allah é o maior (33 vezes) Ninguém é digno de adoração exceto Allah, o único, sem parceiro algum, somente ele pertence ao reinado e somente para ele são todas as glórias; todo bem está no seu poder e ele tem o poder de tudo", "@azkarList1": { "description": "سُـبْحانَ اللهِ، والحَمْـدُ لله، واللهُ أكْـبَر 33 مرة لا إِلَٰهَ إلاّ اللّهُ وَحْـدَهُ لا شريكَ لهُ، لهُ الملكُ ولهُ الحَمْد، وهُوَ على كُلّ شَيءٍ قَـدير" }, - "azkarList2": "", + "azkarList2": "Em nome de Deus, o Clemente, o mais misericordioso diz: “Busco refúgio no Senhor dos humanos, O Rei dos humanos, O Deus dos humanos, contra o mal murmurante que recua, que murmura sobre os peitos dos humanos, dos jinns ou dos humanos.”", "@azkarList2": { "description": "بِسۡمِ ٱللَّهِ ٱلرَّحۡمَٰنِ ٱلرَّحِيمِ قُلۡ أَعُوذُ بِرَبِّ ٱلنَّاسِ ، مَلِكِ ٱلنَّاسِ ، إِلَٰهِ ٱلنَّاسِ ، مِن شَرِّ ٱلۡوَسۡوَاسِ ٱلۡخَنَّاسِ ، ٱلَّذِي يُوَسۡوِسُ فِي صُدُورِ ٱلنَّاسِ ، مِنَ ٱلۡجِنَّةِ وَٱلنَّاس" }, - "azkarList3": "", + "azkarList3": "Em nome de Deus, o mais gracioso, o mais misericordioso, diz: “Eu busco refúgio no Senhor da criação, Contra o mal que criou, o mal das trevas quando se aproxima, e do mal das trevas. Soprando o nó, e da maldade de tal invejoso quando se aproximar.”", "@azkarList3": { "description": "بِسۡمِ ٱللَّهِ ٱلرَّحۡمَٰنِ ٱلرَّحِيمِقُلۡ أَعُوذُ بِرَبِّ ٱلۡفَلَقِ ، مِن شَرِّ مَا خَلَقَ ، وَمِن شَرِّ غَاسِقٍ إِذَا وَقَبَ ، وَمِن شَرِ ٱلنَّفَّٰثَٰتِ فِي ٱلۡعُقَدِ ، وَمِن شَرِّ حَاسِدٍ إِذَا حَسَدَ" }, - "azkarList4": "", + "azkarList4": "Em nome de Deus, o mais gracioso, o mais misericordioso, diz “Ele é deus, único, Deus, o eterno, o Eterno. Já basta.”", "@azkarList4": { "description": "بِسۡمِ ٱللَّهِ ٱلرَّحۡمَٰنِ ٱلرَّحِيمِ قُلۡ هُوَ ٱللَّهُ أَحَدٌ ، ٱللَّهُ ٱلصَّمَدُ ، لَمۡ يَلِدۡ وَلَمۡ يُولَدۡ ، وَلَمۡ يَكُن لَّهُۥ كُفُوًا أَحَدُۢ" }, - "azkarList5": "", + "azkarList5": "Allah, não há Deus senão você, o Imortal, sempre subsistente. Nenhum ano ou sono pode apoderar de ti. Na terra, quem poderia interceder junto a você, exceto com sua permissão?", "@azkarList5": { "description": "ٱللَّهُ لَآ إِلَٰهَ إِلَّا هُوَ ٱلۡحَيُّ ٱلۡقَيُّومُۚ لَا تَأۡخُذُهُۥ سِنَةٞ وَلَا نَوۡمٞۚ لَّهُۥ مَا فِي ٱلسَّمَٰوَٰتِ وَمَا فِي ٱلۡأَرۡضِۗ مَن ذَا ٱلَّذِي يَشۡفَعُ عِندَهُۥٓ إِلَّا بِإِذۡنِهِۦۚ يَعۡلَمُ مَا بَيۡنَ أَيۡدِيهِمۡ وَمَا خَلۡفَهُمۡۖ وَلَا يُحِيطُونَ بِشَيۡءٖ مِّنۡ عِلۡمِهِۦٓ إِلَّا بِمَا شَآءَۚ وَسِعَ كُرۡسِيُّهُ ٱلسَّمَٰوَٰتِ وَٱلۡأَرۡضَۖ وَلَا يَ‍ُٔودُهُۥ حِفۡظُهُمَاۚ وَهُوَ ٱلۡعَلِيُّ ٱلۡعَظِيمُ" }, - "azkarList6": "Ninguém e digno de adoração exepto Allah, o Único, sem nenhum parceiro, somente a Ele pertence o reinado e somente para Ele são todos os louvores e Ele tem poder sobre todas as coisas. Ó Allah! Ninguém pode negar o que Você concede e ninguém pode conceder aquilo que Você retém; e a grandeza dos grandes será inútil para eles contra Si.", + "azkarList6": "Ninguém é digno de adoração exceto Allah, o único, sem parceiro algum, somente ele pertence ao reinado e somente para ele são todas as glórias e ele tem poder sobre tudo. Ó Allah! Ninguém nega que você concede e ninguém concede tudo que você retém; e a grandeza dos grandes é inútil para eles contra você", "@azkarList6": { "description": "لا إِلَٰهَ إلاّ اللّهُ وحدَهُ لا شريكَ لهُ، لهُ المُـلْكُ ولهُ الحَمْد، وهوَ على كلّ شَيءٍ قَدير، اللّهُـمَّ لا مانِعَ لِما أَعْطَـيْت، وَلا مُعْطِـيَ لِما مَنَـعْت، وَلا يَنْفَـعُ ذا الجَـدِّ مِنْـكَ الجَـد" }, - "azkarList7": "Ó Allah! Vós Sois meu Senhor. Não existe mais nenhuma divindade exceto Vossa. Vós criastes-me e sou Vosso servente tanto quanto possível, continuo com a minha solene promessa e convenção (que fiz Convosco). Peço Vossa proteção das consequências dos meus maus atos. Reconheço perfeitamente as graças por Vós a mim concedidas e confesso as minhas faltas. Assim, perdoai-me, pois ninguém exceto Vós poderá perdoar os meus pecados.", + "azkarList7": "Ó Allah! Você é meu senhor. Não existe nenhuma divinidade exceto a sua. Você me criou e sou seu servente tanto quanto possível, continuo com a minha promessa e convenção (que fiz contigo). Peço sua proteção das consequências de meus maus atos. Eu reconheço perfeitamente as graças concedidas por você e confesso as minhas faltas. Assim, me perdoe, pois ninguém exceto você poderá perdoar meus pecados", "@azkarList7": { "description": "اللهم أنت ربي، لا إله إلا أنت، خلقتني وأنا عبدُك, وأنا على عهدِك ووعدِك ما استطعتُ، أعوذ بك من شر ما صنعتُ، أبوءُ لَكَ بنعمتكَ عَلَيَّ، وأبوء بذنبي، فاغفر لي، فإنه لا يغفرُ الذنوب إلا أنت" }, - "azkarList8": "Nós, assim como todo o universo, amanhecemos para (a adoração de) Allah. Todos os louvores para Allah. Ninguém mais é digno de adoração exceto Allah. Ele é Único, não possui nenhum parceiro. A Ele pertence todo o universo. E para Ele são todos os louvores. Ó Senhor! Peço-Vos todas as coisas boas relacionadas com este dia e com os dias a seguir; peço a Vossa Proteção contra todos os males relacionados com este dia e os dias a seguir. Ó Senhor! Peço a Vossa ajuda no sentido de salvar-me da indolência e os males da velhice. Ó Allah! Peço a Vossa Proteção contra os castigos do Inferno e da sepultura.", + "azkarList8": "Nós, assim como todo o universo, amanhecemos para (a adoração de) Allah. Todos os louvores a Allah. Ninguém mais é digno de adoração exceto Allah. Ele é único, sem parceiro. A ele pertence o universo inteiro. E a ele são todos os louvores. Ó senhor! Lhe peço todas as seguintes coisas; peço nossa proteção contra qualquer mau deste dia e os próximos dias. Ó senhor! Peço nossa ajuda de me salvar da indolência e os maus da velhice. Ó Allah! Peço nossa proteção contra os castigos infernais e da sepultura", "@azkarList8": { "description": "أصبحنا وأصبح الملك لله، والحمد لله ولا إله إلا الله وحده لا شريك له، له الملك وله الحمد، وهو على كل شيء قدير، أسألك خير ما في هذا اليوم، وخير ما بعده، وأعوذ بك من شر هذا اليوم، وشر ما بعده، وأعوذ بك من الكسل وسوء الكبر، وأعوذ بك من عذاب النار وعذاب القبر" }, - "azkarList9": "Ó Allah! Amanheci, Invoco a Vós e aos anjos que carregam o Vosso Trono, e aos Vossos anjos e toda a Vossa criação para que sejam testemunhas que Vós Sois Allah; ninguém mais é digno da adoração exceto Vós; Sois Único; Não possuis parceiro; e que Muhammad (Que a paz e a misericórdia de Allah esteja sobre ele) é vosso Servo e Mensageiro. (quatro vezes) [E a noite dizer: \"Ó Allah! Anoiteceu, Invoco a Vós...\"]", + "azkarList9": "Oh Deus, eu presto seu testemunho, e presto testemunho aos seus portadores do trono, e aos seus anjos, e a toda sua criação, pois você é Deus, não há Deus senão somente você, sem parceiro para você, e Maomé", "@azkarList9": { "description": "اللَّهُمَّ إِنِّي أَصْبَحْتُ أُشْهِدُكَ، وَأُشْهِدُ حَمَلَةَ عَرْشِكَ، وَمَلاَئِكَتِكَ، وَجَمِيعَ خَلْقِكَ، أَنَّكَ أَنْتَ اللَّهُ لَا إِلَهَ إِلاَّ أَنْتَ وَحْدَكَ لاَ شَرِيكَ لَكَ، وَأَنَّ مُحَمَّداً عَبْدُكَ وَرَسُولُكَ |أربعَ مَرَّات|. [ وإذا أمسى قال: اللَّهم إني أمسيت...]" }, - "azkarList10": "Ó Allah! Conceda-me boa saúde no meu corpo, nos meus ouvidos e nos meus olhos. Ninguém exceto Vós é digno de adoração. Ó Allah! Peço refúgio da descrença e pobreza. Ó Allah! Peço Vossa proteção dos tormentos da sepultura; ninguém mais é digno de adoração exceto Vós. (três vezes)", + "azkarList10": "Ó Allah! Conceda-me boa saúde no meu corpo, nos meus ouvidos e olhos. Ninguém exceto você é digno de adoração. Ó Allah! Peço refúgio da descrença e pobreza. Ó Allah! Peço nossa proteção dos tormentos da sepultura; ninguém mais é digno de adoração exceto você (três vezes)", "@azkarList10": { "description": "|اللَّهُمَّ عَافِنِي فِي بَدَنِي، اللَّهُمَّ عَافِنِي فِي سَمْعِي، اللَّهُمَّ عَافِنِي فِي بَصَرِي، لاَ إِلَهَ إِلاَّ أَنْتَ. اللَّهُمَّ إِنِّي أَعُوذُ بِكَ مِنَ الْكُفْرِ، وَالفَقْرِ، وَأَعُوذُ بِكَ مِنْ عَذَابِ القَبْرِ، لاَ إِلَهَ إِلاَّ أَنْتَ |ثلاثَ مرَّاتٍ" }, - "azkarList11": "Allah é suficiente para mim, ninguém mais é digno da adoração exceto Ele, n’Ele coloquei a minha confiança e Ele é o Senhor do Trono Sublime. (sete vezes)", + "azkarList11": "Deus é suficiente para mim, não há Deus como ele, nele coloco toda minha confiança, ele é o Senhor do Grande Trono (sete vezes)", "@azkarList11": { "description": "|حَسْبِيَ اللَّهُ لاَ إِلَهَ إِلاَّ هُوَ عَلَيهِ تَوَكَّلتُ وَهُوَ رَبُّ الْعَرْشِ الْعَظِيمِ |سَبْعَ مَرّاتٍ" }, - "azkarList12": "Estou de todo contente em crer Allah como meu Senhor, Islam como a minha religião e Muhammad (Que a paz e a misericórdia de Allah esteja sobre ele) como meu Profeta. (três vezes)", + "azkarList12": "Estou tão contente em crer Allah como meu senhor, Islam é minha religião e Muhammad (que a paz e misericórdia de Allah esteja sobre ele) como meu profeta. (três vezes)", "@azkarList12": { "description": "|رَضِيتُ بِاللَّهِ رَبَّاً، وَبِالْإِسْلاَمِ دِيناً، وَبِمُحَمَّدٍ صلى الله عليه وسلم نَبِيّاً |ثلاثَ مرَّاتٍ" }, - "azkarList13": "Ninguém mais é digno da adoração exceto Allah; Ele é o Único; Não possui nenhum parceiro; o Reinado a Ele pertence e todos os Louvores são para Ele. Ele possui poder sobre todas as coisas. (dez vezes)", + "azkarList13": "Não há Deus senão Deus sozinho, sem parceiro, ele pertence ao domínio e a ele é o louvor, e ele tem capacidade de tudo", "@azkarList13": { "description": "|لاَ إِلَهَ إِلاَّ اللَّهُ وَحْدَهُ لاَ شَرِيكَ لَهُ، لَهُ الْمُلْكُ وَلَهُ الْحَمْدُ، وَهُوَ عَلَى كُلِّ شَيْءٍ قَدِيرٌ |عشرَ مرَّات" }, "jumuaaScreenTitle": "Hora de Jumu'ah", - "jumuaaHadith": "O Profeta ﷺ (Que a paz e a misericórdia de Allah esteja sobre ele) disse: \"Aquele que efectuar a ablução (Wudhu) correctamente e dirigir-se para a oração de Jumu’ah, ouvir o sermão (Khutbah) com atenção e em silêncio, todos os seus pequenos pecados entre aquela hora e o próximo Jumu’ah mais três dias (total 10 dias) serão perdoados. E aquele que durante o sermão brincar com uma pedra (ou seja, distrair-se), na realidade, interrompeu está recompensa.\"", - "shuruk": "Nascer do Sol", - "reset": "Reset", - "mosqueNotFoundMessage": "Desculpe, a sua mesquita não foi encontrada, pode estar desativada ou temporariamente indisponível.", - "noInternetMessage": "Por favor, verifique a sua ligação à Internet e tente novamente. O seu Wi-Fi ou Ethernet está ligado?", - "error": "ERRO", - "mosqueErrorMessage": "Erro na Mesquita se você é um administrador da Mesquita, entre em contacto com o nosso suporte para corrigir o problema", + "jumuaaHadith": "O Profeta ﷺ (Que a paz e a misericórdia de Allah esteja sobre ele) disse: \"Aquele que efetuar a ablução (Wudhu) correctamente e dirigir-se para a oração de Jumu’ah, ouvir o sermão (Khutbah) com atenção e em silêncio, todos os seus pequenos pecados entre aquela hora e o próximo Jumu’ah mais três dias (total 10 dias) serão perdoados. E aquele que durante o sermão brincar com uma pedra (ou seja, distrair-se), na realidade, interrompeu está recompensa.\"", + "shuruk": "Nascer do sol", + "reset": "Reiniciar", + "mosqueNotFoundMessage": "Desculpe, sua mesquita não foi encontrada, ou não exista, ou esteja temporariamente desativada.", + "noInternetMessage": "Verifique a sua conexão à rede e tente novamente. Seu Wi-Fi ou internet está ligado?", + "error": "Erro", + "mosqueErrorMessage": "Erro de mesquita. Se você for um administrador da mesquita, entre em contato conosco para corrigir este problema", "muharram": "Muharram", "safar": "Safar", "rabiAlawwal": "Rabi al-Awwal", @@ -139,10 +139,10 @@ "duaaBetweenSalahAndAdhan": "De acordo com Anas Ibn Mâlik, Rasulullah (Sallalahu Aleihi Wassalam) disse: O duá efetuado entre o Azán (chamamento para a oração) e o Iqamah (alerta para o início da oração), não é rejeitado.", "salatKhayrMinaNawm": "A oração é melhor que o sono", "salatElEid": "Salatul Eid", - "webView": "Ativar Modo Compatível", - "developersHomeScreen": "Ecrã de programador", - "onlineHome": "Home On-line", - "prayerTimes": "Horário de Orações", + "webView": "Ativar modo compatível", + "developersHomeScreen": "Início de desenvolvedor", + "onlineHome": "Início online", + "prayerTimes": "Horários de oração", "alerts": "Alerta", "iqamaaCountDown": "Jamaat contagem decrescente", "afterAdhanHadith": "Hadith depois de Azán", @@ -150,51 +150,51 @@ "iqama": "Jamaat", "randomHadith": "Hadith aleatório", "announcement": "Anúncios", - "jumuaaLive": "Jumu'ah [Live Streaming]", - "showSecondaryScreen": "Usar como ecrã secundário (Para Anúncios)", - "normalScreen": "Usar como ecrã principal", + "jumuaaLive": "Jumu'ah [Transmissão ao vivo]", + "showSecondaryScreen": "Usar como tela secundária (para anúncios)", + "normalScreen": "Usar como tela principal", "duaaRemainder": "Continuação Duá", "fajrWakeUp": "Acorde no Fajr", - "changeLanguage": "Mudar idioma", - "forceScreen": "Forçar Ecrã", + "changeLanguage": "Alterar idioma", + "forceScreen": "Forçar tela", "clear": "Limpar", "changeTheme": "Alterar tema", "next": "Próximo", - "mainScreenOrSecondaryScreen": "Localização do ecrã", - "mainScreenOrSecondaryScreenEXPLINATION": "Pretende instalar o ecrã na sala de oração principal (sala de oração para homens)?", - "mainScreen": "Ecrã principal", - "secondaryScreen": "Ecrã secundário", + "mainScreenOrSecondaryScreen": "Localização da tela", + "mainScreenOrSecondaryScreenEXPLINATION": "Deseja instalar esta tela na sala de oração principal (quarto de oração para homens)?", + "mainScreen": "Tela principal", + "secondaryScreen": "Tela secundária", "duaaElEftar": "Duá de Iftar", "announcementOnlyMode": "Modo de anúncios", "normalMode": "Modo normal ", - "announcementOnlyModeEXPLINATION": "Escolha se pretende que o ecrã mostre apenas anúncios, isso pode ser útil se você instalar o ecrã na entrada, por exemplo.", - "duaaElEftarText": "", + "announcementOnlyModeEXPLINATION": "Selecione se sua tela exibirá anunciados todo tempo, isso pode ser útil caso você instalar a tela na entrada, por exemplo.", + "duaaElEftarText": "Ó Allah, por você jejuei, com o sustento vindo por você, eu quebrei o jejum, voltei para você e em você, confiei. A sede já foi, as veias se hidrataram, a recompensa já está garantida, se o Allah quiser.", "@duaaElEftarText": { "description": "اللهم اني لگ صمت وعلى رزقك افطرت واليك انبت وعليگ توكلت ذهب الظما وابتلت العروق وثبت الاجر انشاء الله" }, - "secondaryScreenExplanation": "Para uma sala de oração secundária (sala de orações para senhoras ou para outro andar, por exemplo), este ecrã poderá mostrar a transmissão ao vivo do Jumu'ah", - "mainScreenExplanation": "Para a sala principal da Mesquita, este ecrã não irá mostrar a transmissão ao vivo do Jumu'ah", - "normalModeExplanation": "O modo normal irá mostrar os horários das orações e os anúncios.", - "announcementOnlyModeExplanation": "Irá mostrar apenas anúncios", + "secondaryScreenExplanation": "Para uma sala de oração secundária (sala de mulheres ou outro andar, por exemplo), esta tela exibirá a transmissão ao vivo de Jumu'ah", + "mainScreenExplanation": "Para a sala principal da mesquita, esta tela não exibirá a transmissão ao vivo de Jumu'ah", + "normalModeExplanation": "Exibirá a tela normal com os horários de oração e os anunciados.", + "announcementOnlyModeExplanation": "Exibirá anunciados toda hora", "orientation": "Orientação", - "selectYourMawaqitTvAppOrientation": "Selecione a orientação da aplicação mawaqit tv", - "deviceDefault": "Dispositivo predefinido", - "deviceDefaultBTNDescription": "O Mawaqit seleciona automaticamente a orientação predefinida com base na orientação do ecrã", + "selectYourMawaqitTvAppOrientation": "Selecione sua orientação do aplicativo mawaqit tv", + "deviceDefault": "Dispositivo padrão", + "deviceDefaultBTNDescription": "O Mawaqit selecionará automaticamente a orientação padrão baseada na orientação da tela", "portrait": "Vertical", - "portraitBTNDescription": "A orientação vertical é recomendada para mesquitas com espaço reduzido", + "portraitBTNDescription": "A orientação vertical é recomendada para mesquitas com menos espaço", "landscape": "Horizontal", - "landscapeBTNDescription": "A orientação horizontal é a mais recomendada para a aplicação mawaqit tv e para a maioria das mesquitas", + "landscapeBTNDescription": "Para a orientação horizontal. O layout principal para o aplicativo mawaqit tv e recomendado para a maioria das mesquitas", "eidMubarak": "Eid Mubarak", - "takbeerAleidText": "Allah é Grande, Allah é Grande, Allah é Grande, ninguém mais é digno da adoração exceto Allah, Allah é Grande, Allah é Grande, e louvores para Allah.", - "settings": "Definições", - "applicationModes": "Modo da aplicação", - "ifYouAreFacingAnIssueWithTheAppActivateThis": "Se estiver a ter problemas com a aplicação, tente ativar esta opção", + "takbeerAleidText": "Allah é grande, Allah é grande, Allah é grande, ninguém é mais digno de oração exceto Allah, Allah é grande, Allah é grande, todos os louvores à Allah.", + "settings": "Opções", + "applicationModes": "Modo do aplicativo", + "ifYouAreFacingAnIssueWithTheAppActivateThis": "Se você estiver tendo problemas com o aplicativo, tente ativar esta opção", "hijriAdjustments": "Ajuste local da data islâmica", - "hijriAdjustmentsDescription": "Ajuste a data islâmica do seu dispositivo local. Isso não afetará as configurações da mesquita on-line.", - "backoffice_default": "Configurações padrão do backoffice", + "hijriAdjustmentsDescription": "Ajuste a data islâmica do seu dispositivo local. Isto não afetará as opções da mesquita online", + "backoffice_default": "Padrões do backoffice", "recommended": "Recomendado", "sabah": "Amanhecer", - "randomHadithLanguage": "Idioma dos hadith aleatórios", + "randomHadithLanguage": "Idioma hadith aleatório", "en": "Inglês", "fr": "Francês", "ar": "Árabe", @@ -211,38 +211,38 @@ "es_ar": "Espanhol e Árabe", "pt_ar": "Português e Árabe", "nl_ar": "Holandês e Árabe", - "connectToChangeHadith": "Por favor, ligue-se à internet para mudar o idioma dos hadith.", + "connectToChangeHadith": "Conecte-se à internet para alterar o idioma hadith.", "retry": "Repetir", - "timeSetting": "A configurar a hora", - "timeSettingDesc": "Definir um nome personalizado", + "timeSetting": "Configurando hora", + "timeSettingDesc": "Definir nome personalizado", "selectedTime": "Hora atualmente selecionada", "confirmation": "Confirmar", - "confirmationMessage": "Tem a certeza que pretende usar a hora do dispositivo?", - "useDeviceTime": "Usar a hora do dispositivo", - "selectTime": "Selecionar Hora", + "confirmationMessage": "Deseja mesmo usar a hora do dispositivo?", + "useDeviceTime": "Usar hora do dispositivo", + "selectTime": "Selecionar hora", "previous": "Anterior", - "appTimezone": "Fuso horário da aplicação", - "descTimezone": "Selecione o seu fuso horário para obter os horários das orações mais precisos.", - "appWifi": "Ligar ao Wi-Fi", - "descWifi": "Por favor, escolha o Wi-Fi preferência", - "searchCountries": "Pesquisar países", + "appTimezone": "Fuso horário do aplicativo", + "descTimezone": "Selecione seu fuso horário para obter horários de oração mais precisos.", + "appWifi": "Conectar-se à rede", + "descWifi": "Conecte-se à rede preferida", + "searchCountries": "Buscar países", "scanAgain": "Procurar novamente", - "noScannedResultsFound": "Não foram encontrados pontos de acesso próximos", - "connect": "Ligar", - "wifiPassword": "Password", - "skip": "Saltar", - "noSSID": "**SSID Oculto**", + "noScannedResultsFound": "Sem pontos de acesso próximos encontrados", + "connect": "Conectar", + "wifiPassword": "Senha", + "skip": "Pular", + "noSSID": "**SSID oculto**", "close": "Fechar", - "search": "Pesquisa", - "wifiSuccess": "Ligação ao Wi-Fi estabelecida com sucesso.", - "wifiFailure": "Falha ao ligar ao Wi-Fi.", + "search": "Buscar", + "wifiSuccess": "Conectado à rede com sucesso.", + "wifiFailure": "Falhou ao conectar à rede.", "timezoneSuccess": "Fuso horário definido com sucesso.", - "timezoneFailure": "Falha ao definir o fuso horário.", - "screenLock": "Ligar/Desligar Ecrã", - "screenLockConfig": "Configurar ecrã para ligar/desligar", - "screenLockMode": "Modo ecrã ligado/desligado", - "screenLockDesc": "Ligar/Desligar a TV antes e depois de cada oração para poupar energia", - "screenLockDesc2": "Esta funcionalidade liga/desliga o dispositivo antes e depois de cada Azán", + "timezoneFailure": "Falhou ao definir fuso horário.", + "screenLock": "Ligar/desligar tela", + "screenLockConfig": "Ajustar tela para ligar/desligar", + "screenLockMode": "Modo de tela ligada/desligada", + "screenLockDesc": "Desligar/ligar televisão antes ou depois de cada oração para economizar bateria/energia.", + "screenLockDesc2": "Este recurso liga/desliga o dispositivo antes ou depois de cada Azán", "before": "minutos antes de cada oração", "after": "minutos depois de cada oração", "updateAvailable": "Atualização disponível", @@ -250,28 +250,28 @@ "whatIsNew": "O que há de novo", "update": "Atualizar", "automaticUpdate": "Notificar atualizações", - "automaticUpdateDescription": "Ative as notificações de atualização para receber os últimos recursos e melhorias", - "checkInternetLegacyMode": "Deve ligar-se à internet para usar o modo compatível", - "powerOnScreen": "Ligar o ecrã", - "powerOffScreen": "Desligar o ecrã", - "deviceSettings": "Configurações do Dispositivo", - "later": "Mais tarde", - "downloadQuran": "Descarregar o Alcorão", + "automaticUpdateDescription": "Ative as notificações de atualização para obter as melhorias e recursos mais recentes", + "checkInternetLegacyMode": "Você deve conectar à internet para usar o modo compatível", + "powerOnScreen": "Ligar a tela", + "powerOffScreen": "Desligar a tela", + "deviceSettings": "Opções do dispositivo", + "later": "Depois", + "downloadQuran": "Baixar alcorão", "quran": "Alcorão", - "askDownloadQuran": "Deseja descarregar o Alcorão?", - "download": "Descarregar", - "downloadingQuran": "A descarregar o Alcorão", - "extractingQuran": "A extrair o Alcorão", + "askDownloadQuran": "Deseja baixar o alcorão?", + "download": "Baixar", + "downloadingQuran": "Baixando o alcorão", + "extractingQuran": "Extraindo o alcorão", "updatedQuran": "Alcorão atualizado", - "quranLatestVersion": "O Alcorão está atualizado", - "quranUpdatedVersion": "A versão atualizada do Alcorão é: {version}", - "quranIsUpdated": "Alcorão está atualizado", - "quranDownloaded": "Alcorão descarregado", - "quranIsAlreadyDownloaded": "O Alcorão já está descarregado", - "chooseReciter": "Escolher Recitador", - "reciteType": "Tipo de Recitação", - "readingMode": "Eu quero ler", - "listeningMode": "Eu quero ouvir", + "quranLatestVersion": "O alcorão está atualizado", + "quranUpdatedVersion": "A nova versão do alcorão é: {version}", + "quranIsUpdated": "O alcorão está atualizado", + "quranDownloaded": "Alcorão baixado", + "quranIsAlreadyDownloaded": "O alcorão já foi baixado", + "chooseReciter": "Selecionar recitador", + "reciteType": "Tipo de recitação", + "readingMode": "Ler", + "listeningMode": "Ouvir", "quranReadingPage": "Página {leftPage} - {rightPage} / {totalPages}", "@quranReadingPage": { "description": "Placeholder text for displaying Quran reading page numbers", @@ -304,13 +304,13 @@ } } }, - "chooseQuranPage": "Escolha a página", - "checkingForUpdates": "A verificar atualizações...", - "chooseQuranType": "Escolher Alcorão", + "chooseQuranPage": "Escolher página", + "checkingForUpdates": "Verificando por atualizações...", + "chooseQuranType": "Escolher alcorão", "hafs": "Hafs", "warsh": "Warsh", "favorites": "Favoritos", - "allReciters": "Todos os Recitadores", + "allReciters": "Todos os recitadores", "reciterAddedToFavorites": "Recitador {name} adicionado aos favoritos", "@reciterAddedToFavorites": { "description": "Message shown when a reciter is added to favorites", @@ -331,18 +331,18 @@ } } }, - "noFavoriteReciters": "Sem recitadores favoritos. Tente adicionar um à lista.", + "noFavoriteReciters": "Nenhum recitador favorito. Primeiramente adicione um à lista.", "@noFavoriteReciters": { "description": "Message shown when there are no favorite reciters" }, - "noReciterSearchResult": "Nenhum resultado encontrado para a sua pesquisa", - "searchForReciter": "Pesquisar por um recitador", - "downloadAllSuwarSuccessfully": "O Alcorão completo foi descarregado", - "noSuwarDownload": "Não há novos surah para descarregar", - "connectDownloadQuran": "Por favor, conecte-se à Internet para descarregar", - "playInOnlineModeQuran": "Por favor, conecte-se à internet para reproduzir", - "downloaded": "Descarregado", - "switchQuranType": "Ir para {name}", + "noReciterSearchResult": "Nenhum resultado encontrado para sua busca", + "searchForReciter": "Buscar recitador", + "downloadAllSuwarSuccessfully": "O alcorão inteiro foi baixado", + "noSuwarDownload": "Sem surahs novos para baixar", + "connectDownloadQuran": "Conecte-se à internet para baixar", + "playInOnlineModeQuran": "Conecte-se à internet para reproduzir", + "downloaded": "Baixado", + "switchQuranType": "Ir a {name}", "@switchQuranType": { "description": "Message shown when a reciter is added to favorites", "placeholders": { @@ -352,17 +352,32 @@ } } }, - "surahSelector": "Selecionar Surah", - "checkForUpdates": "Verificar Atualizações", - "checkForNewVersion": "Verificar se uma nova versão está disponível", - "wouldYouLikeToUpdate": "Gostaria de atualizar a aplicação?", + "surahSelector": "Selecionar surah", + "checkForUpdates": "Verificar por atualizações", + "checkForNewVersion": "Verificar caso tenha uma nova versão", + "wouldYouLikeToUpdate": "Deseja atualizar o aplicativo?", "updateCompleted": "Atualização concluída com sucesso!", - "noUpdates": "Sem Atualizações", - "usingLatestVersion": "Está a utilizar a versão mais recente.", + "noUpdates": "Sem atualizações", + "usingLatestVersion": "Você está usando a versão mais recente.", "updateCancelled": "Atualização cancelada", - "checkingUpdates": "A verificar atualizações...", - "downloadingUpdate": "A descarregar a atualização...", - "installingUpdate": "A instalar a atualização...", + "checkingUpdates": "Verificando por atualizações...", + "downloadingUpdate": "Baixando atualização...", + "installingUpdate": "Instalando atualização...", "updateCompletedSuccessfully": "Atualização concluída com sucesso", - "updateFailed": "Falha na atualização" + "updateFailed": "Falhou ao atualizar", + "checkInternetUpdate": "Você deve conectar-se à internet para verificar por novas atualizações", + "appUpdateAvailable": "O aplicativo está executando a versão {currentVersion}. Uma nova atualização (versão {updatedVersion}) está disponível com novos recursos e melhorias.", + "@appUpdateAvailable": { + "description": "Placeholder text for displaying update available message", + "placeholders": { + "currentVersion": { + "type": "String", + "example": "1" + }, + "updatedVersion": { + "type": "String", + "example": "604" + } + } + } } \ No newline at end of file diff --git a/lib/src/pages/quran/widget/download_quran_popup.dart b/lib/src/pages/quran/widget/download_quran_popup.dart index 9900c2508..6dc28e5c8 100644 --- a/lib/src/pages/quran/widget/download_quran_popup.dart +++ b/lib/src/pages/quran/widget/download_quran_popup.dart @@ -178,6 +178,15 @@ 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/pubspec.yaml b/pubspec.yaml index c9d336e4f..11aa50ff3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,9 +18,11 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev + version: 1.18.0+1 + environment: sdk: ">=3.0.0 <4.0.0"