From 56ccbd4973b2dba22a336a6ad3263aa1513d91d0 Mon Sep 17 00:00:00 2001 From: primozratej Date: Sun, 22 Sep 2024 17:39:33 +0200 Subject: [PATCH 1/5] Fix animation blink --- lib/models/manifest.dart | 8 +- lib/pages/opener.dart | 123 ++++++++------------- lib/pages/web_view.dart | 37 +++++-- lib/util/file_handler.dart | 1 - lib/util/notifications/init_from_push.dart | 2 +- lib/util/openers/opener_controller.dart | 80 +++++++++++++- lib/util/show_dialog.dart | 8 +- 7 files changed, 164 insertions(+), 95 deletions(-) diff --git a/lib/models/manifest.dart b/lib/models/manifest.dart index 2ce792f..46d2dd0 100644 --- a/lib/models/manifest.dart +++ b/lib/models/manifest.dart @@ -8,7 +8,13 @@ class Manifest { final String backgroundColor; final String themeColor; - Manifest({required this.display, required this.startUrl, required this.shortName, required this.name, required this.backgroundColor, required this.themeColor}); + Manifest( + {required this.display, + required this.startUrl, + required this.shortName, + required this.name, + required this.backgroundColor, + required this.themeColor}); String get baseUrl { Uri url = Uri.parse(startUrl); diff --git a/lib/pages/opener.dart b/lib/pages/opener.dart index a7c7fa7..879aa57 100644 --- a/lib/pages/opener.dart +++ b/lib/pages/opener.dart @@ -1,18 +1,18 @@ import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:humhub/components/language_switcher.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:humhub/pages/help/help_android.dart'; +import 'package:humhub/pages/help/help_ios.dart'; import 'package:humhub/pages/web_view.dart'; import 'package:humhub/util/const.dart'; -import 'package:humhub/util/form_helper.dart'; import 'package:humhub/util/intent/intent_plugin.dart'; import 'package:humhub/util/notifications/channel.dart'; import 'package:humhub/util/openers/opener_controller.dart'; import 'package:humhub/util/providers.dart'; import 'package:rive/rive.dart'; -import 'help/help_android.dart'; -import 'help/help_ios.dart'; class Opener extends ConsumerStatefulWidget { const Opener({Key? key}) : super(key: key); @@ -23,39 +23,25 @@ class Opener extends ConsumerStatefulWidget { } class OpenerState extends ConsumerState with SingleTickerProviderStateMixin { - late OpenerController controlLer; - - late RiveAnimationController _controller; - late SimpleAnimation _animation; - late RiveAnimationController _controllerReverse; - late SimpleAnimation _animationReverse; - - final FormHelper helper = FormHelper(); - // Fade out Logo and opener when redirecting - bool _visible = true; - bool _textFieldVisibility = false; - bool _languageSwitcherVisibility = false; + late OpenerController openerControlLer; @override void initState() { super.initState(); - _animation = SimpleAnimation('animation', autoplay: false); - _controller = _animation; - - _animationReverse = SimpleAnimation('animation', autoplay: true); - _controllerReverse = _animationReverse; + openerControlLer = OpenerController(ref: ref); + openerControlLer.setForwardAnimation(SimpleAnimation('animation', autoplay: false)); + openerControlLer.setReverseAnimation(SimpleAnimation('animation', autoplay: true)); WidgetsBinding.instance.addPostFrameCallback((_) async { + // Delay before showing text field + ref.read(visibilityProvider.notifier).toggleVisibility(true); Future.delayed(const Duration(milliseconds: 900), () { - setState(() { - _textFieldVisibility = true; - }); + ref.read(textFieldVisibilityProvider.notifier).toggleVisibility(true); }); + // Delay before showing language switcher Future.delayed(const Duration(milliseconds: 700), () { - setState(() { - _languageSwitcherVisibility = true; - }); + ref.read(languageSwitcherVisibilityProvider.notifier).toggleVisibility(true); }); String? urlIntent = InitFromIntent.usePayloadForInit(); @@ -67,7 +53,6 @@ class OpenerState extends ConsumerState with SingleTickerProviderStateMi @override Widget build(BuildContext context) { - controlLer = OpenerController(ref: ref, helper: helper); return Container( color: Colors.white, child: Stack( @@ -75,12 +60,12 @@ class OpenerState extends ConsumerState with SingleTickerProviderStateMi RiveAnimation.asset( Assets.openerAnimationForward, fit: BoxFit.fill, - controllers: [_controller], + controllers: [openerControlLer.animationForwardController], ), RiveAnimation.asset( Assets.openerAnimationReverse, fit: BoxFit.fill, - controllers: [_controllerReverse], + controllers: [openerControlLer.animationReverseController], ), Scaffold( resizeToAvoidBottomInset: true, @@ -89,14 +74,15 @@ class OpenerState extends ConsumerState with SingleTickerProviderStateMi bottom: false, top: false, child: Form( - key: helper.key, + key: openerControlLer.helper.key, child: Padding( padding: const EdgeInsets.only(top: 50), child: Column( mainAxisSize: MainAxisSize.min, children: [ + // Language Switcher visibility AnimatedOpacity( - opacity: _languageSwitcherVisibility ? 1.0 : 0.0, + opacity: ref.watch(languageSwitcherVisibilityProvider) ? 1.0 : 0.0, duration: const Duration(milliseconds: 300), child: const Padding( padding: EdgeInsets.only(top: 10, right: 16), @@ -120,7 +106,7 @@ class OpenerState extends ConsumerState with SingleTickerProviderStateMi Expanded( flex: 12, child: AnimatedOpacity( - opacity: _textFieldVisibility ? 1.0 : 0.0, + opacity: ref.watch(textFieldVisibilityProvider) ? 1.0 : 0.0, duration: const Duration(milliseconds: 250), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 35), @@ -130,31 +116,33 @@ class OpenerState extends ConsumerState with SingleTickerProviderStateMi future: ref.read(humHubProvider).getLastUrl(), builder: (context, snapshot) { if (snapshot.hasData) { - controlLer.urlTextController.text = snapshot.data!; + openerControlLer.urlTextController.text = snapshot.data!; return TextFormField( keyboardType: TextInputType.url, - controller: controlLer.urlTextController, + controller: openerControlLer.urlTextController, cursorColor: Theme.of(context).textTheme.bodySmall?.color, - onSaved: controlLer.helper.onSaved(controlLer.formUrlKey), + onSaved: openerControlLer.helper.onSaved(openerControlLer.formUrlKey), onEditingComplete: () { - controlLer.helper.onSaved(controlLer.formUrlKey); + openerControlLer.helper.onSaved(openerControlLer.formUrlKey); _connectInstance(); }, onChanged: (value) { - // Calculate the new cursor position - final cursorPosition = controlLer.urlTextController.selection.baseOffset; + final cursorPosition = + openerControlLer.urlTextController.selection.baseOffset; final trimmedValue = value.trim(); - // Update the text controller and set the new cursor position - controlLer.urlTextController.value = TextEditingValue( + openerControlLer.urlTextController.value = TextEditingValue( text: trimmedValue, - selection: TextSelection.collapsed(offset: cursorPosition > trimmedValue.length ? trimmedValue.length : cursorPosition), + selection: TextSelection.collapsed( + offset: cursorPosition > trimmedValue.length + ? trimmedValue.length + : cursorPosition), ); }, style: const TextStyle( decoration: TextDecoration.none, ), decoration: openerDecoration(context), - validator: controlLer.validateUrl, + validator: openerControlLer.validateUrl, autocorrect: false, ); } @@ -177,7 +165,7 @@ class OpenerState extends ConsumerState with SingleTickerProviderStateMi Expanded( flex: 4, child: AnimatedOpacity( - opacity: _visible ? 1.0 : 0.0, + opacity: ref.watch(visibilityProvider) ? 1.0 : 0.0, duration: const Duration(milliseconds: 300), child: Center( child: TextButton( @@ -209,15 +197,8 @@ class OpenerState extends ConsumerState with SingleTickerProviderStateMi flex: 4, child: GestureDetector( onTap: () { - FocusManager.instance.primaryFocus?.unfocus(); - _controller.isActive = true; - setState(() { - _visible = false; - _textFieldVisibility = false; - _languageSwitcherVisibility = false; - }); - Future.delayed(const Duration(milliseconds: 700)).then((value) { - Navigator.push( + openerControlLer.animationNavigationWrapper( + navigate: () => Navigator.push( context, PageRouteBuilder( transitionDuration: const Duration(milliseconds: 500), @@ -230,26 +211,11 @@ class OpenerState extends ConsumerState with SingleTickerProviderStateMi ); }, ), - ).then((value) { - setState(() { - _controller.isActive = true; - _animation.reset(); - _visible = true; - Future.delayed(const Duration(milliseconds: 700), () { - setState(() { - _textFieldVisibility = true; - _languageSwitcherVisibility = true; - }); - }); - _controllerReverse.isActive = true; - }); - }); - _controllerReverse.isActive = true; - _animationReverse.reset(); - }); + ), + ); }, child: AnimatedOpacity( - opacity: _visible ? 1.0 : 0.0, + opacity: ref.watch(visibilityProvider) ? 1.0 : 0.0, duration: const Duration(milliseconds: 300), child: Text( AppLocalizations.of(context)!.opener_need_help, @@ -275,10 +241,13 @@ class OpenerState extends ConsumerState with SingleTickerProviderStateMi _connectInstance() async { FocusManager.instance.primaryFocus?.unfocus(); - await controlLer.initHumHub(); - if (controlLer.allOk) { - ref.read(humHubProvider).getInstance().then((value) { - Navigator.pushNamed(ref.context, WebView.path, arguments: value.manifest); + await openerControlLer.initHumHub(); + if (openerControlLer.allOk) { + ref.read(humHubProvider).getInstance().then((instance) { + FocusManager.instance.primaryFocus?.unfocus(); + openerControlLer.animationNavigationWrapper( + navigate: () => Navigator.pushNamed(ref.context, WebView.path, arguments: instance.manifest), + ); }); } } @@ -302,11 +271,7 @@ class OpenerState extends ConsumerState with SingleTickerProviderStateMi @override void dispose() { - controlLer.urlTextController.dispose(); - _controller.dispose(); - _controllerReverse.dispose(); - _animation.dispose(); - _animationReverse.dispose(); + openerControlLer.dispose(); super.dispose(); } } diff --git a/lib/pages/web_view.dart b/lib/pages/web_view.dart index ab79586..049c461 100644 --- a/lib/pages/web_view.dart +++ b/lib/pages/web_view.dart @@ -40,12 +40,12 @@ class WebView extends ConsumerStatefulWidget { } class WebViewAppState extends ConsumerState { - late AuthInAppBrowser authBrowser; late Manifest manifest; late URLRequest _initialRequest; late PullToRefreshController _pullToRefreshController; HeadlessInAppWebView? headlessWebView; + bool isInit = false; final _settings = InAppWebViewSettings( useShouldOverrideUrlLoading: true, @@ -58,9 +58,33 @@ class WebViewAppState extends ConsumerState { ); @override - initState() { - super.initState(); - LoadingProvider.of(ref).dismissAll(); + void didChangeDependencies() { + super.didChangeDependencies(); + if (!isInit) { + _initialRequest = _initRequest; + _pullToRefreshController = PullToRefreshController( + settings: PullToRefreshSettings( + color: HexColor(manifest.themeColor), + ), + onRefresh: () async { + if (Platform.isAndroid) { + WebViewGlobalController.value?.reload(); + } else if (Platform.isIOS) { + WebViewGlobalController.value?.loadUrl( + urlRequest: URLRequest( + url: await WebViewGlobalController.value?.getUrl(), + headers: ref.read(humHubProvider).customHeaders)); + } + }, + ); + authBrowser = AuthInAppBrowser( + manifest: manifest, + concludeAuth: (URLRequest request) { + _concludeAuth(request); + }, + ); + isInit = true; + } } @override @@ -341,9 +365,8 @@ class WebViewAppState extends ConsumerState { @override void dispose() { + if (headlessWebView != null) headlessWebView!.dispose(); + //_pullToRefreshController.dispose(); super.dispose(); - if (headlessWebView != null) { - headlessWebView!.dispose(); - } } } diff --git a/lib/util/file_handler.dart b/lib/util/file_handler.dart index 150da21..6e40265 100644 --- a/lib/util/file_handler.dart +++ b/lib/util/file_handler.dart @@ -39,7 +39,6 @@ class FileHandler { required this.onSuccess, this.filename, this.onError, - }); download() async { diff --git a/lib/util/notifications/init_from_push.dart b/lib/util/notifications/init_from_push.dart index 7f42313..a0e4102 100644 --- a/lib/util/notifications/init_from_push.dart +++ b/lib/util/notifications/init_from_push.dart @@ -10,4 +10,4 @@ class InitFromPush { _redirectUrlFromInit = null; return payload; } -} \ No newline at end of file +} diff --git a/lib/util/openers/opener_controller.dart b/lib/util/openers/opener_controller.dart index 0ac8eca..4e3072e 100644 --- a/lib/util/openers/opener_controller.dart +++ b/lib/util/openers/opener_controller.dart @@ -6,22 +6,54 @@ import 'package:humhub/models/manifest.dart'; import 'package:humhub/util/providers.dart'; import 'package:http/http.dart' as http; import 'package:loggy/loggy.dart'; +import 'package:rive/rive.dart'; import '../api_provider.dart'; import '../connectivity_plugin.dart'; import '../form_helper.dart'; +// Create a notifier for visibility state +class VisibilityNotifier extends StateNotifier { + VisibilityNotifier() : super(false); // Default to false + + void toggleVisibility(bool isVisible) { + state = isVisible; + } +} + +final textFieldVisibilityProvider = StateNotifierProvider( + (ref) => VisibilityNotifier(), +); + +final languageSwitcherVisibilityProvider = StateNotifierProvider( + (ref) => VisibilityNotifier(), +); + +final visibilityProvider = StateNotifierProvider( + (ref) => VisibilityNotifier(), +); + class OpenerController { late AsyncValue? asyncData; bool doesViewExist = false; - final FormHelper helper; TextEditingController urlTextController = TextEditingController(); late String? postcodeErrorMessage; final String formUrlKey = "redirect_url"; final String error404 = "404"; final String noConnection = "no_connection"; final WidgetRef ref; + late RiveAnimationController _animationForwardController; + late SimpleAnimation _animationForward; + late RiveAnimationController _animationReverseController; + late SimpleAnimation _animationReverse; + + RiveAnimationController get animationForwardController => _animationForwardController; + SimpleAnimation get animationForward => _animationForward; + RiveAnimationController get animationReverseController => _animationReverseController; + SimpleAnimation get animationReverse => _animationReverse; + + final FormHelper helper = FormHelper(); - OpenerController({required this.ref, required this.helper}); + OpenerController({required this.ref}); /// Finds the `manifest.json` file associated with the given URL. If the URL does not /// directly point to the `manifest.json` file, it traverses up the directory structure @@ -129,4 +161,48 @@ class OpenerController { if (url.startsWith("https://") || url.startsWith("http://")) return Uri.parse(url); return Uri.parse("https://$url"); } + + setForwardAnimation(SimpleAnimation animation) { + _animationForward = animation; + _animationForwardController = _animationForward; + } + + setReverseAnimation(SimpleAnimation animation) { + _animationReverse = animation; + _animationReverseController = _animationReverse; + } + + void animationNavigationWrapper({required Future Function() navigate}) { + FocusManager.instance.primaryFocus?.unfocus(); + _animationForwardController.isActive = true; + ref.read(visibilityProvider.notifier).toggleVisibility(false); + ref.read(textFieldVisibilityProvider.notifier).toggleVisibility(false); + ref.read(languageSwitcherVisibilityProvider.notifier).toggleVisibility(false); + + Future.delayed(const Duration(milliseconds: 700)).then((_) { + navigate().then((value) { + _animationForwardController.isActive = true; + _animationForward.reset(); + ref.read(visibilityProvider.notifier).toggleVisibility(true); + + Future.delayed(const Duration(milliseconds: 700), () { + ref.read(textFieldVisibilityProvider.notifier).toggleVisibility(true); + ref.read(languageSwitcherVisibilityProvider.notifier).toggleVisibility(true); + }); + + _animationReverseController.isActive = true; + }); + + _animationReverseController.isActive = true; + _animationReverse.reset(); + }); + } + + dispose() { + urlTextController.dispose(); + _animationForwardController.dispose(); + _animationReverseController.dispose(); + _animationForward.dispose(); + _animationReverse.dispose(); + } } diff --git a/lib/util/show_dialog.dart b/lib/util/show_dialog.dart index 780f758..19a20cd 100644 --- a/lib/util/show_dialog.dart +++ b/lib/util/show_dialog.dart @@ -36,16 +36,16 @@ class ShowDialog { ); } - noInternetPopup(){ + noInternetPopup() { showDialog( context: context, builder: (BuildContext context) { return AlertDialog( - title: Text(AppLocalizations.of(context)!.connectivity_popup_title), - content: Text(AppLocalizations.of(context)!.connectivity_popup_content), + title: Text(AppLocalizations.of(context)!.connectivity_popup_title), + content: Text(AppLocalizations.of(context)!.connectivity_popup_content), actions: [ TextButton( - child: Text(AppLocalizations.of(context)!.ok.toUpperCase()), + child: Text(AppLocalizations.of(context)!.ok.toUpperCase()), onPressed: () { Navigator.of(context).pop(); // Close the dialog }, From af8d6538fd6d19d97b5348e31e26f38de2d21c3b Mon Sep 17 00:00:00 2001 From: primozratej Date: Sun, 22 Sep 2024 17:43:08 +0200 Subject: [PATCH 2/5] Only set state of _pullToRefreshController in didChangeDependencies --- lib/pages/web_view.dart | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/lib/pages/web_view.dart b/lib/pages/web_view.dart index 049c461..4a4c1e0 100644 --- a/lib/pages/web_view.dart +++ b/lib/pages/web_view.dart @@ -89,28 +89,6 @@ class WebViewAppState extends ConsumerState { @override Widget build(BuildContext context) { - _initialRequest = _initRequest; - _pullToRefreshController = PullToRefreshController( - settings: PullToRefreshSettings( - color: HexColor(manifest.themeColor), - ), - onRefresh: () async { - if (Platform.isAndroid) { - WebViewGlobalController.value?.reload(); - } else if (Platform.isIOS) { - WebViewGlobalController.value?.loadUrl( - urlRequest: URLRequest( - url: await WebViewGlobalController.value?.getUrl(), headers: ref.read(humHubProvider).customHeaders)); - } - }, - ); - authBrowser = AuthInAppBrowser( - manifest: manifest, - concludeAuth: (URLRequest request) { - _concludeAuth(request); - }, - ); - return Scaffold( backgroundColor: HexColor(manifest.themeColor), body: SafeArea( From ed39926481644e599293871d35738ad1cbe51317 Mon Sep 17 00:00:00 2001 From: primozratej Date: Sun, 22 Sep 2024 20:53:18 +0200 Subject: [PATCH 3/5] Ask for permission handler for ios. --- android/app/src/main/AndroidManifest.xml | 1 + ios/Runner.xcodeproj/project.pbxproj | 21 +++------- ios/Runner/Info.plist | 6 +++ ios/build/.last_build_id | 1 + lib/app_opener.dart | 8 +++- lib/pages/web_view.dart | 49 ++++++++++++------------ lib/util/file_handler.dart | 4 +- 7 files changed, 46 insertions(+), 44 deletions(-) create mode 100644 ios/build/.last_build_id diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 8b6079a..9864f10 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -18,6 +18,7 @@ + 14.4 FLTEnableImpeller + NSPhotoLibraryUsageDescription + We need access to your photo library to save photos and videos. + UIFileSharingEnabled + + LSSupportsOpeningDocumentsInPlace + diff --git a/ios/build/.last_build_id b/ios/build/.last_build_id new file mode 100644 index 0000000..2c7279a --- /dev/null +++ b/ios/build/.last_build_id @@ -0,0 +1 @@ +e728d5ba4e52068eb059d8c80cc8e750 \ No newline at end of file diff --git a/lib/app_opener.dart b/lib/app_opener.dart index bd42757..bbf786f 100644 --- a/lib/app_opener.dart +++ b/lib/app_opener.dart @@ -23,10 +23,14 @@ class OpenerAppState extends ConsumerState { void initState() { super.initState(); SchedulerBinding.instance.addPostFrameCallback((_) async { - final status = await Permission.notification.status; - if (!status.isGranted) { + final notifications = await Permission.notification.status; + if (!notifications.isGranted) { await Permission.notification.request(); } + final storage = await Permission.manageExternalStorage.status; + if (!storage.isGranted) { + await Permission.storage.request(); + } }); } diff --git a/lib/pages/web_view.dart b/lib/pages/web_view.dart index 4a4c1e0..2215ef3 100644 --- a/lib/pages/web_view.dart +++ b/lib/pages/web_view.dart @@ -21,6 +21,8 @@ import 'package:humhub/util/providers.dart'; import 'package:humhub/util/openers/universal_opener_controller.dart'; import 'package:humhub/util/push/provider.dart'; import 'package:humhub/util/router.dart'; +import 'package:humhub/util/show_dialog.dart'; +import 'package:humhub/util/web_view_global_controller.dart'; import 'package:loggy/loggy.dart'; import 'package:open_file_plus/open_file_plus.dart'; import 'package:permission_handler/permission_handler.dart'; @@ -28,9 +30,6 @@ import 'package:humhub/util/router.dart' as m; import 'package:url_launcher/url_launcher.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import '../util/show_dialog.dart'; -import '../util/web_view_global_controller.dart'; - class WebView extends ConsumerStatefulWidget { const WebView({super.key}); static const String path = '/web_view'; @@ -40,12 +39,12 @@ class WebView extends ConsumerStatefulWidget { } class WebViewAppState extends ConsumerState { - late AuthInAppBrowser authBrowser; - late Manifest manifest; + late AuthInAppBrowser _authBrowser; + late Manifest _manifest; late URLRequest _initialRequest; late PullToRefreshController _pullToRefreshController; - HeadlessInAppWebView? headlessWebView; - bool isInit = false; + HeadlessInAppWebView? _headlessWebView; + bool _isInit = false; final _settings = InAppWebViewSettings( useShouldOverrideUrlLoading: true, @@ -60,11 +59,11 @@ class WebViewAppState extends ConsumerState { @override void didChangeDependencies() { super.didChangeDependencies(); - if (!isInit) { + if (!_isInit) { _initialRequest = _initRequest; _pullToRefreshController = PullToRefreshController( settings: PullToRefreshSettings( - color: HexColor(manifest.themeColor), + color: HexColor(_manifest.themeColor), ), onRefresh: () async { if (Platform.isAndroid) { @@ -77,20 +76,20 @@ class WebViewAppState extends ConsumerState { } }, ); - authBrowser = AuthInAppBrowser( - manifest: manifest, + _authBrowser = AuthInAppBrowser( + manifest: _manifest, concludeAuth: (URLRequest request) { _concludeAuth(request); }, ); - isInit = true; + _isInit = true; } } @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: HexColor(manifest.themeColor), + backgroundColor: HexColor(_manifest.themeColor), body: SafeArea( bottom: false, // ignore: deprecated_member_use @@ -118,25 +117,25 @@ class WebViewAppState extends ConsumerState { final args = ModalRoute.of(context)!.settings.arguments; String? url; if (args is Manifest) { - manifest = args; + _manifest = args; } if (args is UniversalOpenerController) { UniversalOpenerController controller = args; ref.read(humHubProvider).setInstance(controller.humhub); - manifest = controller.humhub.manifest!; + _manifest = controller.humhub.manifest!; url = controller.url; } if (args == null) { - manifest = m.MyRouter.initParams; + _manifest = m.MyRouter.initParams; } if (args is ManifestWithRemoteMsg) { ManifestWithRemoteMsg manifestPush = args; - manifest = manifestPush.manifest; + _manifest = manifestPush.manifest; url = manifestPush.remoteMessage.data['url']; } String? payloadFromPush = InitFromPush.usePayload(); if (payloadFromPush != null) url = payloadFromPush; - return URLRequest(url: WebUri(url ?? manifest.startUrl), headers: ref.read(humHubProvider).customHeaders); + return URLRequest(url: WebUri(url ?? _manifest.startUrl), headers: ref.read(humHubProvider).customHeaders); } Future _shouldOverrideUrlLoading( @@ -145,8 +144,8 @@ class WebViewAppState extends ConsumerState { //Open in external browser final url = action.request.url!.origin; - if (!url.startsWith(manifest.baseUrl) && action.isForMainFrame) { - authBrowser.launchUrl(action.request); + if (!url.startsWith(_manifest.baseUrl) && action.isForMainFrame) { + _authBrowser.launchUrl(action.request); return NavigationActionPolicy.CANCEL; } // 2nd Append customHeader if url is in app redirect and CANCEL the requests without custom headers @@ -163,15 +162,15 @@ class WebViewAppState extends ConsumerState { _onWebViewCreated(InAppWebViewController controller) async { LoadingProvider.of(ref).showLoading(); - headlessWebView = HeadlessInAppWebView(); - headlessWebView!.run(); + _headlessWebView = HeadlessInAppWebView(); + _headlessWebView!.run(); await controller.addWebMessageListener( WebMessageListener( jsObjectName: "flutterChannel", onPostMessage: (inMessage, sourceOrigin, isMainFrame, replyProxy) async { logInfo(inMessage); ChannelMessage message = ChannelMessage.fromJson(inMessage!.data); - await _handleJSMessage(message, headlessWebView!); + await _handleJSMessage(message, _headlessWebView!); logDebug('flutterChannel triggered: ${message.type}'); }, ), @@ -231,7 +230,7 @@ class WebViewAppState extends ConsumerState { } _concludeAuth(URLRequest request) { - authBrowser.close(); + _authBrowser.close(); WebViewGlobalController.value!.loadUrl(urlRequest: request); } @@ -343,7 +342,7 @@ class WebViewAppState extends ConsumerState { @override void dispose() { - if (headlessWebView != null) headlessWebView!.dispose(); + if (_headlessWebView != null) _headlessWebView!.dispose(); //_pullToRefreshController.dispose(); super.dispose(); } diff --git a/lib/util/file_handler.dart b/lib/util/file_handler.dart index 6e40265..7618e3d 100644 --- a/lib/util/file_handler.dart +++ b/lib/util/file_handler.dart @@ -109,9 +109,9 @@ class FileHandler { Directory? directory; if (Platform.isIOS) { - directory = await getApplicationDocumentsDirectory(); + directory = await getDownloadsDirectory(); } else { - String check = "/storage/emulated/0/Download/"; + String check = "/storage/emulated/0/Download"; bool dirDownloadExists = await Directory(check).exists(); if (dirDownloadExists) { From 9c05938e83ca89a075fbe3b6248426ac825c782a Mon Sep 17 00:00:00 2001 From: primozratej Date: Sun, 22 Sep 2024 20:56:05 +0200 Subject: [PATCH 4/5] manageExternalStorage --- lib/app_opener.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/app_opener.dart b/lib/app_opener.dart index bbf786f..08fa093 100644 --- a/lib/app_opener.dart +++ b/lib/app_opener.dart @@ -29,7 +29,7 @@ class OpenerAppState extends ConsumerState { } final storage = await Permission.manageExternalStorage.status; if (!storage.isGranted) { - await Permission.storage.request(); + await Permission.manageExternalStorage.request(); } }); } From 05654be51d60b7d1bbf7fa374e434ebe84dfa34a Mon Sep 17 00:00:00 2001 From: primozratej Date: Sun, 22 Sep 2024 22:21:50 +0200 Subject: [PATCH 5/5] Use one navigatorKey and save files to ApplicationDocumentsDirectory on ios --- android/app/src/main/AndroidManifest.xml | 4 +- lib/app_flavored.dart | 14 +++-- lib/app_opener.dart | 14 ++--- lib/flavored/util/intent_plugin.f.dart | 12 ++-- lib/flavored/util/notifications/channel.dart | 10 ++-- lib/flavored/util/router.f.dart | 6 +- lib/flavored/web_view.f.dart | 6 +- lib/l10n/app_de.arb | 3 + lib/l10n/app_en.arb | 3 + lib/l10n/app_fr.arb | 3 + lib/pages/web_view.dart | 1 + lib/util/const.dart | 3 + lib/util/file_handler.dart | 15 ++++- lib/util/intent/intent_plugin.dart | 2 +- lib/util/notifications/channel.dart | 2 +- lib/util/permission_handler.dart | 62 ++++++++++++++++++++ lib/util/router.dart | 4 +- 17 files changed, 123 insertions(+), 41 deletions(-) create mode 100644 lib/util/permission_handler.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 9864f10..263793f 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -15,10 +15,12 @@ + + - + { void initState() { super.initState(); SchedulerBinding.instance.addPostFrameCallback((_) async { - final status = await Permission.notification.status; - if (!status.isGranted) { - await Permission.notification.request(); - } + await PermissionHandler.requestPermissions([ + Permission.notification, + Permission.manageExternalStorage, + ]); }); } @@ -41,13 +43,13 @@ class FlavoredAppState extends ConsumerState { child: OverrideLocale( builder: (overrideLocale) => Builder( builder: (context) => MaterialApp( - scaffoldMessengerKey: scaffoldMessengerStateKeyF, + scaffoldMessengerKey: scaffoldMessengerStateKey, debugShowCheckedModeBanner: false, initialRoute: RouterF.initRoute, routes: RouterF.routes, localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, - navigatorKey: navigatorKeyF, + navigatorKey: navigatorKey, builder: (context, child) => child!, theme: ThemeData( fontFamily: 'OpenSans', diff --git a/lib/app_opener.dart b/lib/app_opener.dart index 08fa093..73d51c6 100644 --- a/lib/app_opener.dart +++ b/lib/app_opener.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:humhub/util/const.dart'; import 'package:humhub/util/intent/intent_plugin.dart'; import 'package:humhub/util/loading_provider.dart'; import 'package:humhub/util/notifications/plugin.dart'; import 'package:humhub/util/override_locale.dart'; +import 'package:humhub/util/permission_handler.dart'; import 'package:humhub/util/push/push_plugin.dart'; import 'package:humhub/util/router.dart'; import 'package:humhub/util/storage_service.dart'; @@ -23,14 +25,10 @@ class OpenerAppState extends ConsumerState { void initState() { super.initState(); SchedulerBinding.instance.addPostFrameCallback((_) async { - final notifications = await Permission.notification.status; - if (!notifications.isGranted) { - await Permission.notification.request(); - } - final storage = await Permission.manageExternalStorage.status; - if (!storage.isGranted) { - await Permission.manageExternalStorage.request(); - } + await PermissionHandler.requestPermissions([ + Permission.notification, + Permission.manageExternalStorage, + ]); }); } diff --git a/lib/flavored/util/intent_plugin.f.dart b/lib/flavored/util/intent_plugin.f.dart index 240b2a9..9fad039 100644 --- a/lib/flavored/util/intent_plugin.f.dart +++ b/lib/flavored/util/intent_plugin.f.dart @@ -4,8 +4,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:humhub/flavored/util/router.f.dart'; import 'package:humhub/flavored/web_view.f.dart'; +import 'package:humhub/util/const.dart'; import 'package:humhub/util/loading_provider.dart'; import 'package:loggy/loggy.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; @@ -68,7 +68,7 @@ class IntentPluginFState extends ConsumerState { if (!mounted) return; _latestUri = uri; String? redirectUrl = uri?.toString(); - if (redirectUrl != null && navigatorKeyF.currentState != null) { + if (redirectUrl != null && navigatorKey.currentState != null) { tryNavigateWithOpener(redirectUrl); } _err = null; @@ -103,11 +103,11 @@ class IntentPluginFState extends ConsumerState { } _latestUri = uri; String? redirectUrl = uri.queryParameters['url']; - if (redirectUrl != null && navigatorKeyF.currentState != null) { + if (redirectUrl != null && navigatorKey.currentState != null) { tryNavigateWithOpener(redirectUrl); } else { if (redirectUrl != null) { - navigatorKeyF.currentState!.pushNamed(WebViewF.path, arguments: redirectUrl); + navigatorKey.currentState!.pushNamed(WebViewF.path, arguments: redirectUrl); return; } } @@ -125,13 +125,13 @@ class IntentPluginFState extends ConsumerState { Future tryNavigateWithOpener(String redirectUrl) async { LoadingProvider.of(ref).showLoading(); bool isNewRouteSameAsCurrent = false; - navigatorKeyF.currentState!.popUntil((route) { + navigatorKey.currentState!.popUntil((route) { if (route.settings.name == WebViewF.path) { isNewRouteSameAsCurrent = true; } return true; }); - navigatorKeyF.currentState!.pushNamed(WebViewF.path, arguments: redirectUrl); + navigatorKey.currentState!.pushNamed(WebViewF.path, arguments: redirectUrl); return isNewRouteSameAsCurrent; } } diff --git a/lib/flavored/util/notifications/channel.dart b/lib/flavored/util/notifications/channel.dart index 196d769..69b264d 100644 --- a/lib/flavored/util/notifications/channel.dart +++ b/lib/flavored/util/notifications/channel.dart @@ -1,5 +1,5 @@ -import 'package:humhub/flavored/util/router.f.dart'; import 'package:humhub/flavored/web_view.f.dart'; +import 'package:humhub/util/const.dart'; import 'package:humhub/util/notifications/channel.dart'; import 'package:humhub/util/notifications/init_from_push.dart'; @@ -14,19 +14,19 @@ class NotificationChannelF extends NotificationChannel { /// @override Future onTap(String? payload) async { - if (payload != null && navigatorKeyF.currentState != null) { + if (payload != null && navigatorKey.currentState != null) { bool isNewRouteSameAsCurrent = false; - navigatorKeyF.currentState!.popUntil((route) { + navigatorKey.currentState!.popUntil((route) { if (route.settings.name == WebViewF.path) { isNewRouteSameAsCurrent = true; } return true; }); if (isNewRouteSameAsCurrent) { - navigatorKeyF.currentState!.pushNamed(WebViewF.path, arguments: payload); + navigatorKey.currentState!.pushNamed(WebViewF.path, arguments: payload); return; } - navigatorKeyF.currentState!.pushNamed(WebViewF.path, arguments: payload); + navigatorKey.currentState!.pushNamed(WebViewF.path, arguments: payload); } else { if (payload != null) { InitFromPush.setPayload(payload); diff --git a/lib/flavored/util/router.f.dart b/lib/flavored/util/router.f.dart index 44dfd57..24c01e1 100644 --- a/lib/flavored/util/router.f.dart +++ b/lib/flavored/util/router.f.dart @@ -1,11 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:humhub/flavored/web_view.f.dart'; +import 'package:humhub/util/const.dart'; -final GlobalKey navigatorKeyF = GlobalKey(); -final GlobalKey scaffoldMessengerStateKeyF = GlobalKey(); - -NavigatorState? get navigator => navigatorKeyF.currentState; +NavigatorState? get navigator => navigatorKey.currentState; class RouterF { static String? initRoute = WebViewF.path; diff --git a/lib/flavored/web_view.f.dart b/lib/flavored/web_view.f.dart index 0d5f9ce..e8e7de1 100644 --- a/lib/flavored/web_view.f.dart +++ b/lib/flavored/web_view.f.dart @@ -8,9 +8,9 @@ import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:humhub/app_flavored.dart'; import 'package:humhub/flavored/models/humhub.f.dart'; -import 'package:humhub/flavored/util/router.f.dart'; import 'package:humhub/util/auth_in_app_browser.dart'; import 'package:humhub/models/channel_message.dart'; +import 'package:humhub/util/const.dart'; import 'package:humhub/util/extensions.dart'; import 'package:humhub/util/loading_provider.dart'; import 'package:humhub/util/notifications/init_from_push.dart'; @@ -279,7 +279,7 @@ class FlavoredWebViewState extends ConsumerState { downloadStartRequest: downloadStartRequest, controller: controller, onSuccess: (File file, String filename) { - scaffoldMessengerStateKeyF.currentState?.showSnackBar( + scaffoldMessengerStateKey.currentState?.showSnackBar( SnackBar( content: Text('${AppLocalizations.of(context)!.file_download}: $filename'), action: SnackBarAction( @@ -293,7 +293,7 @@ class FlavoredWebViewState extends ConsumerState { ); }, onError: (er) { - scaffoldMessengerStateKeyF.currentState?.showSnackBar( + scaffoldMessengerStateKey.currentState?.showSnackBar( SnackBar( content: Text(AppLocalizations.of(context)!.generic_error), ), diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 2ae77b7..670f860 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -35,6 +35,9 @@ "file_download": "Datei heruntergeladen", "generic_error": "Uups! Etwas ist schiefgelaufen.", + "enable_permissions": "Berechtigungen aktivieren.", + + "settings": "Einstellungen", "connect": "Verbinden", "open": "Öffnen", "cancel": "Abbrechen", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 8521a2f..45d2fcb 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -36,6 +36,9 @@ "file_download": "File downloaded", "generic_error": "Oops! Something went wrong.", + "enable_permissions": "Enable permissions.", + + "settings": "Settings", "connect": "Connect", "open": "Open", "cancel" : "Cancel", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 435a5f7..7b13c99 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -35,6 +35,9 @@ "file_download": "Fichier téléchargé", "generic_error": "Oups ! Quelque chose s'est mal passé.", + "enable_permissions": "Activer les autorisations.", + + "settings": "Paramètres", "connect": "Connecter", "open": "Ouvrir", "cancel": "Annuler", diff --git a/lib/pages/web_view.dart b/lib/pages/web_view.dart index 2215ef3..7f32aa3 100644 --- a/lib/pages/web_view.dart +++ b/lib/pages/web_view.dart @@ -12,6 +12,7 @@ import 'package:humhub/models/hum_hub.dart'; import 'package:humhub/models/manifest.dart'; import 'package:humhub/pages/opener.dart'; import 'package:humhub/util/connectivity_plugin.dart'; +import 'package:humhub/util/const.dart'; import 'package:humhub/util/extensions.dart'; import 'package:humhub/util/file_handler.dart'; import 'package:humhub/util/loading_provider.dart'; diff --git a/lib/util/const.dart b/lib/util/const.dart index 8689c0e..bd40922 100644 --- a/lib/util/const.dart +++ b/lib/util/const.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; +final GlobalKey scaffoldMessengerStateKey = GlobalKey(); +final GlobalKey navigatorKey = GlobalKey(); + class StorageKeys { static String humhubInstance = "humHubInstance"; static String lastInstanceUrl = "humHubLastUrl"; diff --git a/lib/util/file_handler.dart b/lib/util/file_handler.dart index 7618e3d..0e1d5a1 100644 --- a/lib/util/file_handler.dart +++ b/lib/util/file_handler.dart @@ -2,8 +2,10 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:humhub/util/permission_handler.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; class FileHandler { final InAppWebViewController controller; @@ -41,7 +43,14 @@ class FileHandler { this.onError, }); - download() async { + download() { + PermissionHandler.runWithPermissionCheck( + permissions: [Permission.storage], + action: () => _download(), + ); + } + + _download() async { try { await controller.evaluateJavascript(source: _jsCode); await controller.evaluateJavascript(source: "downloadFile('${downloadStartRequest.url.toString()}');"); @@ -109,7 +118,7 @@ class FileHandler { Directory? directory; if (Platform.isIOS) { - directory = await getDownloadsDirectory(); + directory = await getApplicationDocumentsDirectory(); } else { String check = "/storage/emulated/0/Download"; @@ -117,7 +126,7 @@ class FileHandler { if (dirDownloadExists) { directory = Directory(check); } else { - directory = await getExternalStorageDirectory(); + directory = await getApplicationDocumentsDirectory(); } } diff --git a/lib/util/intent/intent_plugin.dart b/lib/util/intent/intent_plugin.dart index 84668ca..ee9f779 100644 --- a/lib/util/intent/intent_plugin.dart +++ b/lib/util/intent/intent_plugin.dart @@ -5,8 +5,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:humhub/pages/web_view.dart'; +import 'package:humhub/util/const.dart'; import 'package:humhub/util/loading_provider.dart'; -import 'package:humhub/util/router.dart'; import 'package:humhub/util/openers/universal_opener_controller.dart'; import 'package:loggy/loggy.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; diff --git a/lib/util/notifications/channel.dart b/lib/util/notifications/channel.dart index a9d3626..e1c7ade 100644 --- a/lib/util/notifications/channel.dart +++ b/lib/util/notifications/channel.dart @@ -1,9 +1,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:humhub/flavored/util/notifications/channel.dart'; import 'package:humhub/pages/web_view.dart'; +import 'package:humhub/util/const.dart'; import 'package:humhub/util/notifications/init_from_push.dart'; import 'package:humhub/util/openers/universal_opener_controller.dart'; -import 'package:humhub/util/router.dart'; import 'package:package_info_plus/package_info_plus.dart'; class NotificationChannel { diff --git a/lib/util/permission_handler.dart b/lib/util/permission_handler.dart new file mode 100644 index 0000000..9d4d317 --- /dev/null +++ b/lib/util/permission_handler.dart @@ -0,0 +1,62 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:humhub/util/const.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class PermissionHandler { + // Static method that takes a list of permissions and handles requests + static Future requestPermissions(List permissions) async { + for (Permission permission in permissions) { + // Check the current status of the permission + PermissionStatus status = await permission.status; + + // Only request the permission if it has never been asked or is not granted + if (status.isDenied || status.isRestricted || status.isPermanentlyDenied) { + continue; // Don't request again if it's denied or restricted + } + + // Request the permission if not granted + if (!status.isGranted) { + await permission.request(); + } + } + } + + // Static method to check permissions before executing a function + static Future runWithPermissionCheck({ + required List permissions, + required Function action, + }) async { + bool allPermissionsGranted = true; + + for (Permission permission in permissions) { + PermissionStatus status = await permission.status; + if (!status.isGranted) { + allPermissionsGranted = false; + break; + } + } + + if (allPermissionsGranted || Platform.isIOS) { + // All permissions are granted, run the provided action + action(); + } else { + // Show a SnackBar indicating that permissions are missing + if(navigatorKey.currentState != null && navigatorKey.currentState!.mounted){ + scaffoldMessengerStateKey.currentState?.showSnackBar( + SnackBar( + content: Text(AppLocalizations.of(navigatorKey.currentState!.context)!.enable_permissions), + action: SnackBarAction( + label: AppLocalizations.of(navigatorKey.currentState!.context)!.settings, + onPressed: () { + openAppSettings(); + }, + ), + ), + ); + } + } + } +} diff --git a/lib/util/router.dart b/lib/util/router.dart index 0143c2a..cf6d05c 100644 --- a/lib/util/router.dart +++ b/lib/util/router.dart @@ -9,11 +9,9 @@ import 'package:humhub/pages/help/help_android.dart'; import 'package:humhub/pages/help/help_ios.dart'; import 'package:humhub/pages/opener.dart'; import 'package:humhub/pages/web_view.dart'; +import 'package:humhub/util/const.dart'; import 'package:humhub/util/providers.dart'; -final GlobalKey navigatorKey = GlobalKey(); -final GlobalKey scaffoldMessengerStateKey = GlobalKey(); - NavigatorState? get navigator => navigatorKey.currentState; List _pendingRoutes = [];