diff --git a/lib/core/app/app_version.dart b/lib/core/app/app_version.dart index 08efebc..01f70b7 100644 --- a/lib/core/app/app_version.dart +++ b/lib/core/app/app_version.dart @@ -1,5 +1,5 @@ -const appVersionName = '1.0.11'; -const appVersionCode = 15; +const appVersionName = '1.0.13'; +const appVersionCode = 17; const appVersion = '$appVersionName+$appVersionCode'; const ctcpVersionReply = 'AndroidIRCX Flutter v$appVersion'; diff --git a/lib/core/models/app_settings.dart b/lib/core/models/app_settings.dart index 5f04321..89c448a 100644 --- a/lib/core/models/app_settings.dart +++ b/lib/core/models/app_settings.dart @@ -12,6 +12,8 @@ enum MessageDensity { compact, comfortable, relaxed } enum NickColorMode { none, soft, vivid } +enum MediaAutoDownloadMode { never, wifiOnly, always } + class AppSettings { const AppSettings({ this.showRawEvents = true, @@ -20,6 +22,7 @@ class AppSettings { this.showAttachmentPreviews = true, this.dccDownloadDirectoryPath = '', this.mediaDownloadDirectoryPath = '', + this.mediaAutoDownloadMode = MediaAutoDownloadMode.never, this.themePreset = AppThemePreset.light, this.customThemeJson = '', this.messageFontScale = 1.0, @@ -61,6 +64,7 @@ class AppSettings { final bool showAttachmentPreviews; final String dccDownloadDirectoryPath; final String mediaDownloadDirectoryPath; + final MediaAutoDownloadMode mediaAutoDownloadMode; final AppThemePreset themePreset; final String customThemeJson; final double messageFontScale; @@ -135,6 +139,7 @@ class AppSettings { bool? showAttachmentPreviews, String? dccDownloadDirectoryPath, String? mediaDownloadDirectoryPath, + MediaAutoDownloadMode? mediaAutoDownloadMode, AppThemePreset? themePreset, String? customThemeJson, double? messageFontScale, @@ -180,6 +185,8 @@ class AppSettings { dccDownloadDirectoryPath ?? this.dccDownloadDirectoryPath, mediaDownloadDirectoryPath: mediaDownloadDirectoryPath ?? this.mediaDownloadDirectoryPath, + mediaAutoDownloadMode: + mediaAutoDownloadMode ?? this.mediaAutoDownloadMode, themePreset: themePreset ?? this.themePreset, customThemeJson: customThemeJson ?? this.customThemeJson, messageFontScale: _clampFontScale( @@ -229,6 +236,7 @@ class AppSettings { 'showAttachmentPreviews': showAttachmentPreviews, 'dccDownloadDirectoryPath': dccDownloadDirectoryPath, 'mediaDownloadDirectoryPath': mediaDownloadDirectoryPath, + 'mediaAutoDownloadMode': mediaAutoDownloadMode.name, 'themePreset': themePreset.name, 'customThemeJson': customThemeJson, 'messageFontScale': messageFontScale, @@ -279,6 +287,11 @@ class AppSettings { (json['dccDownloadDirectoryPath'] as String?)?.trim() ?? '', mediaDownloadDirectoryPath: (json['mediaDownloadDirectoryPath'] as String?)?.trim() ?? '', + mediaAutoDownloadMode: _enumByName( + MediaAutoDownloadMode.values, + json['mediaAutoDownloadMode'], + MediaAutoDownloadMode.never, + ), themePreset: _enumByName( AppThemePreset.values, json['themePreset'], diff --git a/lib/features/chat/presentation/chat_screen.dart b/lib/features/chat/presentation/chat_screen.dart index 478fb7c..21efb4b 100644 --- a/lib/features/chat/presentation/chat_screen.dart +++ b/lib/features/chat/presentation/chat_screen.dart @@ -30,6 +30,7 @@ import 'package:androidircx/irc/parser/irc_formatter.dart'; import 'package:androidircx/irc/parser/message_content_parser.dart'; import 'package:androidircx/features/settings/presentation/settings_screen.dart'; import 'package:androidircx/media/services/link_preview_service.dart'; +import 'package:androidircx/media/services/media_auto_download_policy.dart'; import 'package:androidircx/media/services/media_download_service.dart'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; @@ -42,6 +43,7 @@ class ChatScreen extends StatefulWidget { required this.controller, this.filePicker, this.mediaDownloadService, + this.mediaAutoDownloadPolicy, this.sessionRegistry, this.networkController, this.onSwitchNetwork, @@ -53,6 +55,7 @@ class ChatScreen extends StatefulWidget { final ChatSessionController controller; final DccFilePicker? filePicker; final MediaDownloadService? mediaDownloadService; + final MediaAutoDownloadPolicy? mediaAutoDownloadPolicy; /// Local per-channel notes store; defaults to a shared-preferences backed /// instance. Injectable for tests. @@ -92,12 +95,19 @@ class _ChatScreenState extends State { String _nickSearchQuery = ''; IrcMessage? _pendingReplyMessage; bool _connectedBannerDismissed = false; + final Set _autoDownloadSeenAttachmentKeys = {}; + final Set _autoDownloadInFlightAttachmentKeys = {}; ChatSessionController get _controller => widget.controller; DccFilePicker get _filePicker => widget.filePicker ?? const MethodChannelDccFilePicker(); MediaDownloadService get _mediaDownloadService => widget.mediaDownloadService ?? createMediaDownloadService(); + MediaAutoDownloadPolicy? _defaultMediaAutoDownloadPolicy; + MediaAutoDownloadPolicy get _mediaAutoDownloadPolicy => + widget.mediaAutoDownloadPolicy ?? + (_defaultMediaAutoDownloadPolicy ??= + ConnectivityMediaAutoDownloadPolicy()); ChannelNotesRepository? _defaultChannelNotes; ChannelNotesRepository get _channelNotesRepository => widget.channelNotesRepository ?? @@ -110,7 +120,8 @@ class _ChatScreenState extends State { @override void initState() { super.initState(); - _controller.addListener(_syncConnectionBannerDismissal); + _rememberExistingAutoDownloadAttachments(); + _controller.addListener(_handleControllerChanged); _controller.start(); } @@ -118,22 +129,30 @@ class _ChatScreenState extends State { void didUpdateWidget(covariant ChatScreen oldWidget) { super.didUpdateWidget(oldWidget); if (!identical(oldWidget.controller, widget.controller)) { - oldWidget.controller.removeListener(_syncConnectionBannerDismissal); + oldWidget.controller.removeListener(_handleControllerChanged); _connectedBannerDismissed = false; - _controller.addListener(_syncConnectionBannerDismissal); + _autoDownloadSeenAttachmentKeys.clear(); + _autoDownloadInFlightAttachmentKeys.clear(); + _rememberExistingAutoDownloadAttachments(); + _controller.addListener(_handleControllerChanged); _controller.start(); } } @override void dispose() { - _controller.removeListener(_syncConnectionBannerDismissal); + _controller.removeListener(_handleControllerChanged); _composerController.dispose(); _messageSearchController.dispose(); _nickSearchController.dispose(); super.dispose(); } + void _handleControllerChanged() { + _syncConnectionBannerDismissal(); + _queueMediaAutoDownloads(); + } + void _syncConnectionBannerDismissal() { final snapshot = _controller.connection; final stableConnected = @@ -145,6 +164,74 @@ class _ChatScreenState extends State { setState(() => _connectedBannerDismissed = false); } + void _rememberExistingAutoDownloadAttachments() { + for (final tab in _controller.tabs) { + for (final message in _controller.messagesForTab(tab.id)) { + for (final attachment in message.attachments) { + if (_canDownloadAttachment(attachment)) { + _autoDownloadSeenAttachmentKeys.add( + _autoDownloadAttachmentKey(message, attachment), + ); + } + } + } + } + } + + void _queueMediaAutoDownloads() { + final mode = _controller.settings.mediaAutoDownloadMode; + if (mode == MediaAutoDownloadMode.never) { + _rememberExistingAutoDownloadAttachments(); + return; + } + + for (final tab in _controller.tabs) { + for (final message in _controller.messagesForTab(tab.id)) { + for (final attachment in message.attachments) { + if (!_canDownloadAttachment(attachment)) { + continue; + } + final key = _autoDownloadAttachmentKey(message, attachment); + if (!_autoDownloadSeenAttachmentKeys.add(key)) { + continue; + } + if (message.isOwn || message.isPlayback) { + continue; + } + if (!_autoDownloadInFlightAttachmentKeys.add(key)) { + continue; + } + unawaited(_autoDownloadAttachment(key, attachment, mode)); + } + } + } + } + + Future _autoDownloadAttachment( + String key, + IrcMessageAttachment attachment, + MediaAutoDownloadMode mode, + ) async { + try { + if (!await _mediaAutoDownloadPolicy.canAutoDownload(mode)) { + return; + } + final url = attachment.uri; + if (url == null || url.trim().isEmpty) { + return; + } + await _mediaDownloadService.download( + url, + directoryPath: _controller.settings.mediaDownloadDirectoryPath, + ); + } catch (_) { + // Auto-download must never interrupt chat; manual download remains + // available from the attachment card when a background attempt fails. + } finally { + _autoDownloadInFlightAttachmentKeys.remove(key); + } + } + @override Widget build(BuildContext context) { return CallbackShortcuts( @@ -4196,6 +4283,18 @@ bool _canDownloadAttachment(IrcMessageAttachment attachment) { }; } +String _autoDownloadAttachmentKey( + IrcMessage message, + IrcMessageAttachment attachment, +) { + return [ + message.networkId ?? '', + message.tabId, + message.id, + attachment.uri?.trim() ?? '', + ].join('\u001f'); +} + class _ConnectionBanner extends StatelessWidget { const _ConnectionBanner({ required this.controller, diff --git a/lib/features/monetization/presentation/monetization_banner.dart b/lib/features/monetization/presentation/monetization_banner.dart index 3b5402b..0ac2f0c 100644 --- a/lib/features/monetization/presentation/monetization_banner.dart +++ b/lib/features/monetization/presentation/monetization_banner.dart @@ -2,21 +2,34 @@ import 'dart:async'; import 'package:androidircx/monetization/monetization_config.dart'; import 'package:androidircx/monetization/monetization_controller.dart'; +import 'package:androidircx/monetization/mobile_ads_bootstrap.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:google_mobile_ads/google_mobile_ads.dart'; +typedef BannerAdLoader = Future Function(BannerAd ad); +typedef MobileAdsInitializer = Future Function(); + class MonetizationBanner extends StatefulWidget { const MonetizationBanner({ super.key, required this.controller, required this.onboardingCompleted, required this.child, + this.mobileAdsRuntimeSupported, + this.initializeMobileAds, + this.loadBannerAd, }); final MonetizationController controller; final bool onboardingCompleted; final Widget child; + @visibleForTesting + final bool? mobileAdsRuntimeSupported; + @visibleForTesting + final MobileAdsInitializer? initializeMobileAds; + @visibleForTesting + final BannerAdLoader? loadBannerAd; @override State createState() => _MonetizationBannerState(); @@ -31,6 +44,15 @@ class _MonetizationBannerState extends State { String? _lastFailure; Timer? _retryTimer; + bool get _mobileAdsRuntimeSupported => + widget.mobileAdsRuntimeSupported ?? + MonetizationConfig.mobileAdsRuntimeSupported; + + bool get _shouldShowBanner => widget.controller.shouldShowBanner( + onboardingCompleted: widget.onboardingCompleted, + mobileAdsRuntimeSupported: _mobileAdsRuntimeSupported, + ); + @override void initState() { super.initState(); @@ -57,13 +79,10 @@ class _MonetizationBannerState extends State { } void _syncBannerState() { - final shouldShow = widget.controller.shouldShowBanner( - onboardingCompleted: widget.onboardingCompleted, - ); + final shouldShow = _shouldShowBanner; if (!shouldShow) { _retryTimer?.cancel(); _retryTimer = null; - _loading = false; _lastFailure = null; _disposeBanner(); if (mounted) { @@ -72,12 +91,12 @@ class _MonetizationBannerState extends State { return; } if (_bannerAd == null && !_loading) { - _loadBanner(); + unawaited(_loadBanner()); } } - void _loadBanner() { - if (!MonetizationConfig.mobileAdsRuntimeSupported) { + Future _loadBanner() async { + if (!_mobileAdsRuntimeSupported || _bannerAd != null || _loading) { return; } _retryTimer?.cancel(); @@ -94,8 +113,8 @@ class _MonetizationBannerState extends State { size: AdSize.banner, listener: BannerAdListener( onAdLoaded: (ad) { - if (!mounted || !identical(_bannerAd, ad)) { - ad.dispose(); + if (!mounted || !identical(_bannerAd, ad) || !_shouldShowBanner) { + _disposeAd(ad); return; } setState(() { @@ -105,7 +124,7 @@ class _MonetizationBannerState extends State { }); }, onAdFailedToLoad: (ad, error) { - ad.dispose(); + _disposeAd(ad); if (!mounted) { return; } @@ -123,24 +142,47 @@ class _MonetizationBannerState extends State { ); _bannerAd = ad; try { - ad.load(); + await (widget.initializeMobileAds ?? MobileAdsBootstrap.ensureInitialized) + .call(); + if (!mounted || !identical(_bannerAd, ad) || !_shouldShowBanner) { + if (identical(_bannerAd, ad)) { + _bannerAd = null; + _loaded = false; + _loading = false; + } + _disposeAd(ad); + return; + } + await (widget.loadBannerAd ?? (ad) => ad.load()).call(ad); } catch (error) { - ad.dispose(); - _bannerAd = null; - _loaded = false; - _loading = false; - _lastFailure = error.toString(); - if (mounted) { - setState(() {}); - _scheduleRetry(); + _disposeAd(ad); + if (!mounted) { + return; } + setState(() { + if (identical(_bannerAd, ad)) { + _bannerAd = null; + _loaded = false; + _loading = false; + _lastFailure = error.toString(); + } + }); + _scheduleRetry(); } } void _disposeBanner() { - _bannerAd?.dispose(); + final ad = _bannerAd; _bannerAd = null; _loaded = false; + _loading = false; + if (ad != null) { + _disposeAd(ad); + } + } + + void _disposeAd(Ad ad) { + unawaited(ad.dispose().catchError((_) {})); } void _scheduleRetry() { @@ -152,40 +194,28 @@ class _MonetizationBannerState extends State { if (!mounted) { return; } - if (widget.controller.shouldShowBanner( - onboardingCompleted: widget.onboardingCompleted, - )) { - _loadBanner(); + if (_shouldShowBanner) { + unawaited(_loadBanner()); } }); } @override Widget build(BuildContext context) { - final shouldShow = widget.controller.shouldShowBanner( - onboardingCompleted: widget.onboardingCompleted, - ); - final banner = _loaded && _bannerAd != null - ? SafeArea( - bottom: false, - child: Material( - color: Theme.of(context).colorScheme.surface, - elevation: 1, - child: SizedBox( - width: double.infinity, - height: AdSize.banner.height.toDouble(), - child: Center( - child: SizedBox( - width: AdSize.banner.width.toDouble(), - height: AdSize.banner.height.toDouble(), - child: AdWidget(ad: _bannerAd!), - ), - ), - ), - ), + final banner = _shouldShowBanner + ? _BannerSlot( + child: _loaded && _bannerAd != null + ? Center( + child: SizedBox( + width: AdSize.banner.width.toDouble(), + height: AdSize.banner.height.toDouble(), + child: AdWidget(ad: _bannerAd!), + ), + ) + : !kReleaseMode + ? _BannerLoadStatus(loading: _loading, failure: _lastFailure) + : const SizedBox.shrink(), ) - : shouldShow && !kReleaseMode - ? _BannerLoadStatus(loading: _loading, failure: _lastFailure) : const SizedBox.shrink(); return Column( @@ -197,6 +227,29 @@ class _MonetizationBannerState extends State { } } +class _BannerSlot extends StatelessWidget { + const _BannerSlot({required this.child}); + + final Widget child; + + @override + Widget build(BuildContext context) { + return SafeArea( + bottom: false, + child: Material( + color: Theme.of(context).colorScheme.surface, + elevation: 1, + child: SizedBox( + key: const Key('monetization-banner-slot'), + height: AdSize.banner.height.toDouble(), + width: double.infinity, + child: child, + ), + ), + ); + } +} + class _BannerLoadStatus extends StatelessWidget { const _BannerLoadStatus({required this.loading, required this.failure}); @@ -211,23 +264,15 @@ class _BannerLoadStatus extends StatelessWidget { ? 'Banner ad loading' : 'Banner ad pending' : 'Banner ad failed: $failure'; - return SafeArea( - bottom: false, - child: Material( - color: colorScheme.surfaceContainerHighest, - elevation: 1, - child: SizedBox( - height: AdSize.banner.height.toDouble(), - width: double.infinity, - child: Center( - child: Text( - text, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.labelMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), + return ColoredBox( + color: colorScheme.surfaceContainerHighest, + child: Center( + child: Text( + text, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: colorScheme.onSurfaceVariant, ), ), ), diff --git a/lib/features/settings/presentation/settings_screen.dart b/lib/features/settings/presentation/settings_screen.dart index 5399e33..e6f04d1 100644 --- a/lib/features/settings/presentation/settings_screen.dart +++ b/lib/features/settings/presentation/settings_screen.dart @@ -492,6 +492,36 @@ class _SettingsScreenState extends State { ), ), ), + const Divider(height: 1), + ListTile( + leading: const Icon(Icons.cloud_download_outlined), + title: const Text('Auto-download media'), + subtitle: Text( + _subtitleForMediaAutoDownloadMode( + _settings.mediaAutoDownloadMode, + ), + ), + trailing: DropdownButton( + key: const Key('settings-media-auto-download'), + value: _settings.mediaAutoDownloadMode, + onChanged: (value) async { + if (value == null) { + return; + } + await _saveMediaAutoDownloadMode(value); + }, + items: MediaAutoDownloadMode.values + .map( + (mode) => DropdownMenuItem( + value: mode, + child: Text( + _labelForMediaAutoDownloadMode(mode), + ), + ), + ) + .toList(growable: false), + ), + ), ], ), const SizedBox(height: 12), @@ -1289,6 +1319,10 @@ class _SettingsScreenState extends State { ); } + Future _saveMediaAutoDownloadMode(MediaAutoDownloadMode mode) { + return _saveSettings(_settings.copyWith(mediaAutoDownloadMode: mode)); + } + Future _saveCustomThemeJson(String value) { return _saveSettings(_settings.copyWith(customThemeJson: value.trim())); } @@ -1497,6 +1531,24 @@ class _SettingsScreenState extends State { NoticeRoutingMode.private => 'Private query', }; } + + String _labelForMediaAutoDownloadMode(MediaAutoDownloadMode mode) { + return switch (mode) { + MediaAutoDownloadMode.never => 'Never', + MediaAutoDownloadMode.wifiOnly => 'Wi-Fi only', + MediaAutoDownloadMode.always => 'Any network', + }; + } + + String _subtitleForMediaAutoDownloadMode(MediaAutoDownloadMode mode) { + return switch (mode) { + MediaAutoDownloadMode.never => 'Keep media downloads manual.', + MediaAutoDownloadMode.wifiOnly => + 'Save new incoming media on Wi-Fi or Ethernet.', + MediaAutoDownloadMode.always => + 'Save new incoming media on any active connection.', + }; + } } const String _privacyText = ''' diff --git a/lib/main.dart b/lib/main.dart index b12c4f2..4390970 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,8 +4,8 @@ import 'package:androidircx/app/app.dart'; import 'package:androidircx/core/diagnostics/crash_reporter.dart'; import 'package:androidircx/core/firebase/firebase_service.dart'; import 'package:androidircx/monetization/monetization_config.dart'; +import 'package:androidircx/monetization/mobile_ads_bootstrap.dart'; import 'package:flutter/widgets.dart'; -import 'package:google_mobile_ads/google_mobile_ads.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -19,7 +19,7 @@ Future main() async { // Continue without Firebase if initialization fails. } if (MonetizationConfig.mobileAdsRuntimeSupported) { - unawaited(MobileAds.instance.initialize()); + unawaited(MobileAdsBootstrap.ensureInitialized().catchError((_) {})); } runApp(const AndroidIrcxApp()); } diff --git a/lib/media/services/media_auto_download_policy.dart b/lib/media/services/media_auto_download_policy.dart new file mode 100644 index 0000000..a5d2730 --- /dev/null +++ b/lib/media/services/media_auto_download_policy.dart @@ -0,0 +1,29 @@ +import 'package:androidircx/core/models/app_settings.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; + +abstract class MediaAutoDownloadPolicy { + Future canAutoDownload(MediaAutoDownloadMode mode); +} + +class ConnectivityMediaAutoDownloadPolicy implements MediaAutoDownloadPolicy { + ConnectivityMediaAutoDownloadPolicy({ + Future> Function()? checkConnectivity, + }) : _checkConnectivity = + checkConnectivity ?? Connectivity().checkConnectivity; + + final Future> Function() _checkConnectivity; + + @override + Future canAutoDownload(MediaAutoDownloadMode mode) async { + switch (mode) { + case MediaAutoDownloadMode.never: + return false; + case MediaAutoDownloadMode.always: + return true; + case MediaAutoDownloadMode.wifiOnly: + final results = await _checkConnectivity(); + return results.contains(ConnectivityResult.wifi) || + results.contains(ConnectivityResult.ethernet); + } + } +} diff --git a/lib/monetization/mobile_ads_bootstrap.dart b/lib/monetization/mobile_ads_bootstrap.dart new file mode 100644 index 0000000..b360961 --- /dev/null +++ b/lib/monetization/mobile_ads_bootstrap.dart @@ -0,0 +1,34 @@ +import 'package:androidircx/monetization/monetization_config.dart'; +import 'package:google_mobile_ads/google_mobile_ads.dart'; + +class MobileAdsBootstrap { + const MobileAdsBootstrap._(); + + static Future? _initialization; + + static Future ensureInitialized({Future Function()? initialize}) { + if (!MonetizationConfig.mobileAdsRuntimeSupported) { + return Future.value(); + } + + final current = _initialization; + if (current != null) { + return current; + } + + final starter = + initialize ?? + () async { + await MobileAds.instance.initialize(); + }; + final next = Future.sync(starter).catchError(( + Object error, + StackTrace stackTrace, + ) { + _initialization = null; + Error.throwWithStackTrace(error, stackTrace); + }); + _initialization = next; + return next; + } +} diff --git a/lib/monetization/monetization_controller.dart b/lib/monetization/monetization_controller.dart index 3cf5b3d..88b1731 100644 --- a/lib/monetization/monetization_controller.dart +++ b/lib/monetization/monetization_controller.dart @@ -57,10 +57,16 @@ class MonetizationController extends ChangeNotifier { notifyListeners(); } - bool shouldShowBanner({required bool onboardingCompleted}) { + bool shouldShowBanner({ + required bool onboardingCompleted, + bool? mobileAdsRuntimeSupported, + }) { + final adsSupported = + mobileAdsRuntimeSupported ?? + MonetizationConfig.mobileAdsRuntimeSupported; return _initialized && onboardingCompleted && - MonetizationConfig.mobileAdsRuntimeSupported && + adsSupported && !hasNoAds && !hasTemporaryAdFreeTime; } diff --git a/lib/monetization/rewarded_ad_service.dart b/lib/monetization/rewarded_ad_service.dart index bf30ebb..05b859f 100644 --- a/lib/monetization/rewarded_ad_service.dart +++ b/lib/monetization/rewarded_ad_service.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:androidircx/monetization/monetization_config.dart'; import 'package:androidircx/monetization/monetization_controller.dart'; +import 'package:androidircx/monetization/mobile_ads_bootstrap.dart'; import 'package:flutter/foundation.dart'; import 'package:google_mobile_ads/google_mobile_ads.dart'; @@ -84,6 +85,7 @@ class RewardedAdService extends ChangeNotifier { _lastError = null; notifyListeners(); try { + await MobileAdsBootstrap.ensureInitialized(); await RewardedAd.load( adUnitId: MonetizationConfig.rewardedAdUnitId, request: const AdRequest(nonPersonalizedAds: true), diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 0c18a5f..b2ce12e 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,6 +6,7 @@ import FlutterMacOS import Foundation import audioplayers_darwin +import connectivity_plus import file_selector_macos import firebase_analytics import firebase_app_check @@ -22,11 +23,12 @@ import webview_flutter_wkwebview func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) + ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FirebaseAnalyticsPlugin")) FirebaseAppCheckPlugin.register(with: registry.registrar(forPlugin: "FirebaseAppCheckPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) - FLTFirebaseCrashlyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCrashlyticsPlugin")) + FirebaseCrashlyticsPlugin.register(with: registry.registrar(forPlugin: "FirebaseCrashlyticsPlugin")) FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin")) InAppPurchasePlugin.register(with: registry.registrar(forPlugin: "InAppPurchasePlugin")) InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 1b153ff..a58e0a5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: "6727cf2ced9b104abca9daa278380be2eca2b98ce33d4b46f11708e387dc6b4d" + sha256: f6966125633a34f82d9be2ee895b755cff3554087b1d00a9eb884e5947f4bca9 url: "https://pub.dev" source: hosted - version: "1.3.76" + version: "1.3.77" analyzer: dependency: transitive description: @@ -181,26 +181,26 @@ packages: dependency: transitive description: name: cli_util - sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + sha256: "71e43f1976c3eae2d9468a5b4d6600bf8cae61508b87f27fa1799228935d48ab" url: "https://pub.dev" source: hosted - version: "0.4.2" + version: "0.5.2" clock: dependency: transitive description: name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + sha256: e51d50bca3217c9a9fa2b41a30e4a38971133f5f9ec7a3d57bae095007f1d28e url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.3" code_assets: dependency: transitive description: name: code_assets - sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8 + sha256: cfd4f5f575a49c5f10ca856e9846073f1e6c3ee94912377eea5f6cefc5272941 url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "2.0.0" collection: dependency: transitive description: @@ -209,6 +209,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + connectivity_plus: + dependency: "direct main" + description: + name: connectivity_plus + sha256: "762c99f890ca8bf87f7337236f99edd42793843bc6c3631da294a76653a54bd0" + url: "https://pub.dev" + source: hosted + version: "7.3.1" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + sha256: "3c09627c536d22fd24691a905cdd8b14520de69da52c7a97499c8be5284a32ed" + url: "https://pub.dev" + source: hosted + version: "2.1.0" convert: dependency: transitive description: @@ -221,10 +237,10 @@ packages: dependency: transitive description: name: cross_file - sha256: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51" + sha256: f141ea4f277af142a0356955707f6556f37b03947d39d55585981a06ca437bd6 url: "https://pub.dev" source: hosted - version: "0.3.5+4" + version: "0.3.5+5" crypto: dependency: "direct main" description: @@ -261,10 +277,18 @@ packages: dependency: transitive description: name: dart_style - sha256: "3f88fc9c96c568d631356507355a2d1e983ee000c9ac008bdcb39b5bf53ce777" + sha256: "82ade9fc4273f29ed673e33166944465225b4f7fc5d4aaef48605cc751c18fc1" + url: "https://pub.dev" + source: hosted + version: "3.1.13" + dbus: + dependency: transitive + description: + name: dbus + sha256: a48d5da28e89bd02196e80d81ed8d7954923d00a0f4a68cc20b575038f023383 url: "https://pub.dev" source: hosted - version: "3.1.12" + version: "0.7.15" drift: dependency: "direct main" description: @@ -325,18 +349,18 @@ packages: dependency: transitive description: name: file_selector_linux - sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" + sha256: da76400e7872ce7637ffdce12749ec24169c25f6195c28372208e65a24bcd2ab url: "https://pub.dev" source: hosted - version: "0.9.4" + version: "0.9.4+1" file_selector_macos: dependency: transitive description: name: file_selector_macos - sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" + sha256: d57c62362766b5e7ae739448650b66c6aab7a68ba7ecc65e04018652645ae0f4 url: "https://pub.dev" source: hosted - version: "0.9.5" + version: "0.9.5+1" file_selector_platform_interface: dependency: transitive description: @@ -349,98 +373,98 @@ packages: dependency: transitive description: name: file_selector_windows - sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" + sha256: fbefc5fb92c6d3cbe8d284a2cd971b593bb07d2cd6da8557b81a862250b4acec url: "https://pub.dev" source: hosted - version: "0.9.3+5" + version: "0.9.3+6" firebase_analytics: dependency: "direct main" description: name: firebase_analytics - sha256: a139dd0ada1c6e0ffd77bfdb877ac9061ffdec7c415e3e06113bdf8844db771f + sha256: "0bf812fe5fecf395c07b673f40d951929f631749bd7513a31b8bfafa182d9a06" url: "https://pub.dev" source: hosted - version: "12.4.6" + version: "12.5.0" firebase_analytics_platform_interface: dependency: transitive description: name: firebase_analytics_platform_interface - sha256: "448c319ea895da002e43892c7d3f15dccbc3c4c3b81d3e307e37da885ea52bdf" + sha256: "5c3ab4b681c837afd71f0abf77c7485f51c7b645d4ae77b96f9ed54779ff4126" url: "https://pub.dev" source: hosted - version: "6.0.6" + version: "6.0.7" firebase_analytics_web: dependency: transitive description: name: firebase_analytics_web - sha256: "67f287a0df75f27eafdd4a66a41e44f232c3aba713ea4794a1159893665a8bf5" + sha256: "09eb4d0ce4c9a16efbc277ac709a90eb197d5512592215a73c6a302ab1ab6c3a" url: "https://pub.dev" source: hosted - version: "0.6.1+12" + version: "0.6.1+13" firebase_app_check: dependency: "direct main" description: name: firebase_app_check - sha256: d422642d973b0c636e0582127a47319d8ac9140195e7330c71965d3b6b263095 + sha256: e8917c49b3336c8fbf72ce6db0d04d9f9df779583511dfaf5aac9adb31396557 url: "https://pub.dev" source: hosted - version: "0.4.6" + version: "0.4.7" firebase_app_check_platform_interface: dependency: transitive description: name: firebase_app_check_platform_interface - sha256: "645ff25c18160a2c6e6b487d3466b247619e0bf09e2b1f641d476b2a72f92106" + sha256: "87909de3a8952e69ab0d8b65b89311f3aebf3adb89acbfb0e65c00e57f78eb7e" url: "https://pub.dev" source: hosted - version: "0.4.2" + version: "0.4.2+1" firebase_app_check_web: dependency: transitive description: name: firebase_app_check_web - sha256: "66c938d522c8c325515d222aa55560921b063d06791cf95aa7c405183a3a454f" + sha256: e1072f46b66c1530820ec617909576924a685e2cf7b78e62acd6af82b50609c4 url: "https://pub.dev" source: hosted - version: "0.2.6" + version: "0.2.6+1" firebase_core: dependency: "direct main" description: name: firebase_core - sha256: "9478ca6700c02d315c6aba37e206e612317f98bb4f335bb1cbd6e0ce67dcf764" + sha256: "2343710d8a164e3157a5e2fbb00663503c669be4aa06185311df48626760fdf1" url: "https://pub.dev" source: hosted - version: "4.13.0" + version: "4.14.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - sha256: e28f9afdcb5b0f0a8ea74ea3b322f5a7592c81cb45dd9d189913bdae08a2089a + sha256: "9bfbc85faca09346471ac56fbcee00757ebcfd85887c6f602764eff0001902d8" url: "https://pub.dev" source: hosted - version: "8.1.0" + version: "8.1.1" firebase_core_web: dependency: transitive description: name: firebase_core_web - sha256: f471a288b0101a45567548322ac4a5ad31e3ecbf87a576dcc0e424e6a11e04ea + sha256: de1e678209a0c974d8aa4cff39f17d61d2dc263aa64993168360f9be4efb8e91 url: "https://pub.dev" source: hosted - version: "3.10.0" + version: "3.11.0" firebase_crashlytics: dependency: "direct main" description: name: firebase_crashlytics - sha256: "96c1d85de9eddc07061d3f7e2fb75596e75a45c9cec9c2e3a7cc9f97e6f3a748" + sha256: aca14d94150d50e3f9e0f5090008fa98232d924bc780689481056521d9dc25c9 url: "https://pub.dev" source: hosted - version: "5.2.7" + version: "5.3.0" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface - sha256: "47d83b71fd39c580297c696a507a7c2652680ea2045e40c6c4e3c371b0ee7bc6" + sha256: "8270dcdb1800db3e5f888ce30a2d5e8f92c379c461f022297c83b2c794aa1792" url: "https://pub.dev" source: hosted - version: "3.8.27" + version: "3.9.0" fixnum: dependency: transitive description: @@ -532,10 +556,10 @@ packages: dependency: transitive description: name: glob - sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + sha256: "218aeb56050c714f62a3182775320dfa04602b55074873e24e31bbd39bda96fb" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.2.0" google_mobile_ads: dependency: "direct main" description: @@ -556,18 +580,18 @@ packages: dependency: transitive description: name: hooks - sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba" + sha256: eaac480a35ec0814146c2c48d96aaa829e0e44a7662c88ae84c9edf4bc35651f url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.2.0" html: dependency: transitive description: name: html - sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" + sha256: "43b67b8f43321ab066817dfac5619596c98bb1b61624e77203bb4351785f9699" url: "https://pub.dev" source: hosted - version: "0.15.6" + version: "0.15.7" http: dependency: transitive description: @@ -604,10 +628,10 @@ packages: dependency: transitive description: name: image_picker_android - sha256: d5b3e1774af29c9ab00103afb0d4614070f924d2e0057ac867ec98800114793f + sha256: f71f4f3a9c5dbbe39800b31839cb3b305aac403a7cfbddf8331cf179d813b72a url: "https://pub.dev" source: hosted - version: "0.8.13+17" + version: "0.8.13+21" image_picker_for_web: dependency: transitive description: @@ -620,10 +644,10 @@ packages: dependency: transitive description: name: image_picker_ios - sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588 + sha256: ee3885b6fcd71958fbc79770dd194c63371439d536d69c47b279171a486482ae url: "https://pub.dev" source: hosted - version: "0.8.13+6" + version: "0.8.13+7" image_picker_linux: dependency: transitive description: @@ -668,10 +692,10 @@ packages: dependency: transitive description: name: in_app_purchase_android - sha256: c04e2cad0470fc868cb0ea06477648f003220b1b6612909db83528ca83ac0905 + sha256: f7327b5fd70d8dc1b419fb4cd5c17520175c4059441afd985e8a9c3fa2cefed9 url: "https://pub.dev" source: hosted - version: "0.5.2" + version: "0.5.3" in_app_purchase_platform_interface: dependency: transitive description: @@ -684,10 +708,10 @@ packages: dependency: transitive description: name: in_app_purchase_storekit - sha256: "9602e249a0e30351f047d5715957f27709ed7b42f631fba8941dcad51489932a" + sha256: "66aa6db2236aed23d1b0dd508fb33641ed9878d5e5fb6db5fdbd690b7de6fe98" url: "https://pub.dev" source: hosted - version: "0.4.11+1" + version: "0.4.11+2" in_app_review: dependency: "direct main" description: @@ -716,10 +740,10 @@ packages: dependency: transitive description: name: io - sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + sha256: "2635216ca6a737e60de577ffa1a48a0bec76ca8a62917cfc1bb88c14c570646f" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.1.0" jni: dependency: transitive description: @@ -796,18 +820,18 @@ packages: dependency: transitive description: name: local_auth_android - sha256: b201c006fa769c23386f89aa6837ec0eb8179fcfb212eadcf87b422b3f9a6a78 + sha256: "2de619ed7196c136ec4579539c0446668e794afeec1e1815786a3158a478d04b" url: "https://pub.dev" source: hosted - version: "2.0.8" + version: "2.0.10" local_auth_darwin: dependency: transitive description: name: local_auth_darwin - sha256: a8c3d4e17454111f7fd31ff72a31222359f6059f7fe956c2dcfe0f88f49826d4 + sha256: b1b2d938cc3511512e552d92e782e055b8dc29c83e21c59e6ecfc6bd79ab0744 url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.0.4" local_auth_platform_interface: dependency: transitive description: @@ -820,10 +844,10 @@ packages: dependency: transitive description: name: local_auth_windows - sha256: be12c5b8ba5e64896983123655c5f67d2484ecfcc95e367952ad6e3bff94cb16 + sha256: d72b8a441e5e6a679902f87c51529fcce65f442d77598484cf35454c3d733eda url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" logging: dependency: transitive description: @@ -860,26 +884,34 @@ packages: dependency: transitive description: name: mime - sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + sha256: bd47de35f07e27267e69c8c8b22edf9473bfee170a60d60fcc93730c5144b7f6 url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" native_toolchain_c: dependency: transitive description: name: native_toolchain_c - sha256: f9c168717100ae6d9fee9ffb0be379bf1f8b26b0f6bcbd4fdddcd931993a6a72 + sha256: "9d233b6f2d9c52e1a2b5fbe70451d2c10ac674d3bb419d0ec8de14989d437c26" + url: "https://pub.dev" + source: hosted + version: "0.19.4" + nm: + dependency: transitive + description: + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" url: "https://pub.dev" source: hosted - version: "0.19.2" + version: "0.5.0" objective_c: dependency: transitive description: name: objective_c - sha256: b7fb95a6d9a4f009edd63dc5ac69f07420b23a16161c6dd8660290b59c602e8e + sha256: ad56fd53a78ff6b1472fa59ff2a4e8b8ccabafc586fc263a1dfad0b99b5553e3 url: "https://pub.dev" source: hosted - version: "9.5.0" + version: "9.6.0" package_config: dependency: transitive description: @@ -924,18 +956,18 @@ packages: dependency: transitive description: name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" path_provider_windows: dependency: transitive description: @@ -992,6 +1024,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.2" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" platform: dependency: transitive description: @@ -1012,26 +1052,34 @@ packages: dependency: transitive description: name: pool - sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + sha256: "4177f68c237ea2128d1bee66ac17b2ce05ba3dbaafcbdd54c5d40a39d0b6b11c" + url: "https://pub.dev" + source: hosted + version: "1.5.3" + process: + dependency: transitive + description: + name: process + sha256: "4242ba3508d37e01808bdf71ad1d5bb93a8d671bf2e7450e6b1b353fb0808891" url: "https://pub.dev" source: hosted - version: "1.5.2" + version: "5.0.6" pub_semver: dependency: transitive description: name: pub_semver - sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + sha256: "261236774e8b1d69cfc6b9eabbc96c40f25e7a2d6b171f3385d4f65d5734fb24" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + sha256: c38b81cbf34450b67e0265d73433569d12e34782e30ed769c9cc99c9d5f2e796 url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.6.0" recase: dependency: transitive description: @@ -1044,10 +1092,10 @@ packages: dependency: transitive description: name: record_use - sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + sha256: "1cb8564af8d43b464294411db9217f5ec04891c6f22ee2c32d73ae05e88a6bd2" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "1.1.1" shared_preferences: dependency: "direct main" description: @@ -1060,18 +1108,18 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "8374d6200ab33ac99031a852eba4c8eb2170c4bf20778b3e2c9eccb45384fb41" + sha256: "1e12aafe408aa50da80edfd679a2a6bf63ba7ab37c7fa98286da459a757b3399" url: "https://pub.dev" source: hosted - version: "2.4.21" + version: "2.4.28" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + sha256: "2ec3934efa51e46117f23031cc141b8fc878e8525b94ec1ea4f7f586cf1b47ea" url: "https://pub.dev" source: hosted - version: "2.5.6" + version: "2.5.7" shared_preferences_linux: dependency: transitive description: @@ -1084,10 +1132,10 @@ packages: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" shared_preferences_web: dependency: transitive description: @@ -1169,10 +1217,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + sha256: "277654b3034d17ac6f9f1cb5595db011b1d5d41e8806866db28e0abaa101c490" url: "https://pub.dev" source: hosted - version: "1.12.1" + version: "1.12.2" stream_channel: dependency: transitive description: @@ -1185,10 +1233,10 @@ packages: dependency: transitive description: name: stream_transform - sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + sha256: a00e5f18bffc764f923e7dec1038527f7fe7a1791361a7117f0358193f13d53a url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" string_scanner: dependency: transitive description: @@ -1241,34 +1289,34 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" + sha256: "611e87fb320b70d1dd721dc46af89c98aceccea9b31fde49e084591414e0c610" url: "https://pub.dev" source: hosted - version: "6.3.28" + version: "6.3.33" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" + sha256: "8faa1aab294f1ab4040b43660c887b0418d5fa4f0cffef76a484e6aa1092eb4a" url: "https://pub.dev" source: hosted - version: "6.4.1" + version: "6.4.2" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + sha256: "10f86fef4c2c43563fa6c211ff9cf757adf4d3ab762c56bd430664a947d70cd0" url: "https://pub.dev" source: hosted - version: "3.2.2" + version: "3.2.3" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + sha256: "5e835a3b869c2d70325349c81c5a45c28e20791265b67b2669da6b08c5cd5201" url: "https://pub.dev" source: hosted - version: "3.2.5" + version: "3.2.6" url_launcher_platform_interface: dependency: transitive description: @@ -1281,18 +1329,18 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f + sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.3" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + sha256: "6c5ad3f22cd4c38e089b81963b3cd7bb83b111b2df5dce008bb066162f42e429" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.6" uuid: dependency: transitive description: @@ -1321,18 +1369,18 @@ packages: dependency: transitive description: name: video_player_android - sha256: e229676f8fade3e0124482495aaa2b548872cc4018b6268adfca4868be16aa74 + sha256: "39cf6b79ead3e5b339a8a27651359bde5ed47f753b4c3d0c0942db033591f5ba" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.12.1" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - sha256: c238f5f0a26845cd0bcc2956049b63065a4d3c40ddfc22c3414cd170bef7fff9 + sha256: "436fd029bd1c1e303b2d95ebd76948893f3c28dab286e7235ba9dd7b22533bf0" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.11.1" video_player_platform_interface: dependency: transitive description: @@ -1353,10 +1401,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + sha256: "5f37239c4851efcef929cea7824e76df7f2f0970aef85d66bbc430afa40e72f0" url: "https://pub.dev" source: hosted - version: "15.0.2" + version: "15.3.0" watcher: dependency: transitive description: @@ -1401,10 +1449,10 @@ packages: dependency: transitive description: name: webview_flutter_android - sha256: b98656fa4461f8cc05c48a778b4d4883e60ec63e1778348f363f9bb9a477745d + sha256: "4de8b3d1ff4ebe1bdb42e68a5e4f809194a3cb0117a8f495f590004f00da3964" url: "https://pub.dev" source: hosted - version: "4.14.0" + version: "4.14.1" webview_flutter_platform_interface: dependency: transitive description: @@ -1417,10 +1465,10 @@ packages: dependency: transitive description: name: webview_flutter_wkwebview - sha256: c879dd64b87c452aa84381b244d5469da57ba7e8cca6884c7b1e0d406372c12d + sha256: fe359c7fac1002124b5b9e2ba3a41906bbb9b2d029ccb4a0067404d8f3704730 url: "https://pub.dev" source: hosted - version: "3.26.0" + version: "3.26.1" win32: dependency: transitive description: @@ -1437,14 +1485,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4" + url: "https://pub.dev" + source: hosted + version: "7.0.1" yaml: dependency: transitive description: name: yaml - sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + sha256: f67cdd8e07d3c6329146aaef1ba043542b3134c12489f553ca9a7435d1068aea url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.4" sdks: dart: ">=3.12.0 <4.0.0" flutter: ">=3.44.0" diff --git a/pubspec.yaml b/pubspec.yaml index 8cca9d4..8079e5b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,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 # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.11+15 +version: 1.0.13+17 environment: sdk: ^3.11.1 @@ -50,14 +50,15 @@ dependencies: in_app_review: ^2.0.12 video_player: ^2.9.2 permission_handler: ^13.0.1 - firebase_core: ^4.13.0 - firebase_analytics: ^12.4.6 - firebase_crashlytics: ^5.2.7 - firebase_app_check: ^0.4.6 + firebase_core: ^4.14.0 + firebase_analytics: ^12.5.0 + firebase_crashlytics: ^5.3.0 + firebase_app_check: ^0.4.7 google_mobile_ads: ^9.1.0 in_app_purchase: ^3.3.0 enough_convert: ^1.6.0 audioplayers: ^6.8.1 + connectivity_plus: ^7.3.1 dev_dependencies: flutter_test: diff --git a/test/monetization_banner_test.dart b/test/monetization_banner_test.dart new file mode 100644 index 0000000..eea962e --- /dev/null +++ b/test/monetization_banner_test.dart @@ -0,0 +1,121 @@ +import 'package:androidircx/features/monetization/presentation/monetization_banner.dart'; +import 'package:androidircx/monetization/monetization_config.dart'; +import 'package:androidircx/monetization/monetization_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +const _slotKey = Key('monetization-banner-slot'); +const _bodyKey = Key('app-body'); + +void main() { + setUp(() { + SharedPreferences.setMockInitialValues({}); + }); + + Future initializedController() async { + final controller = MonetizationController(); + await controller.initialize(); + return controller; + } + + Future pumpBanner( + WidgetTester tester, + MonetizationController controller, { + required bool onboardingCompleted, + BannerAdLoader? loadBannerAd, + }) async { + await tester.pumpWidget( + MaterialApp( + home: MonetizationBanner( + controller: controller, + onboardingCompleted: onboardingCompleted, + mobileAdsRuntimeSupported: true, + initializeMobileAds: () async {}, + loadBannerAd: loadBannerAd ?? (_) async {}, + child: const SizedBox.expand(key: _bodyKey), + ), + ), + ); + await tester.pump(); + } + + testWidgets('reserves top banner slot for free users while ad loads', ( + tester, + ) async { + final controller = await initializedController(); + + await pumpBanner(tester, controller, onboardingCompleted: true); + + expect(find.byKey(_slotKey), findsOneWidget); + expect(find.text('Banner ad loading'), findsOneWidget); + expect( + tester.getTopLeft(find.byKey(_bodyKey)).dy, + tester.getBottomLeft(find.byKey(_slotKey)).dy, + ); + + await tester.pumpWidget(const SizedBox.shrink()); + controller.dispose(); + }); + + testWidgets('does not reserve banner slot after permanent no-ads purchase', ( + tester, + ) async { + final controller = await initializedController(); + await controller.processPurchase( + MonetizationConfig.productRemoveAds, + 'token-1', + ); + + await pumpBanner(tester, controller, onboardingCompleted: true); + + expect(find.byKey(_slotKey), findsNothing); + expect(tester.getTopLeft(find.byKey(_bodyKey)).dy, 0); + + await tester.pumpWidget(const SizedBox.shrink()); + controller.dispose(); + }); + + testWidgets('does not reserve banner slot during rewarded ad-free time', ( + tester, + ) async { + final controller = await initializedController(); + await controller.grantTemporaryAdFreeTime(const Duration(minutes: 1)); + + await pumpBanner(tester, controller, onboardingCompleted: true); + + expect(find.byKey(_slotKey), findsNothing); + expect(tester.getTopLeft(find.byKey(_bodyKey)).dy, 0); + + await tester.pumpWidget(const SizedBox.shrink()); + controller.dispose(); + }); + + testWidgets('reports async banner load failures instead of staying loading', ( + tester, + ) async { + final controller = await initializedController(); + var loadCalls = 0; + + await pumpBanner( + tester, + controller, + onboardingCompleted: true, + loadBannerAd: (_) { + loadCalls += 1; + return Future.error(StateError('load failed')); + }, + ); + await tester.pump(); + + expect(loadCalls, 1); + expect(find.byKey(_slotKey), findsOneWidget); + expect( + find.text('Banner ad failed: Bad state: load failed'), + findsOneWidget, + ); + + await tester.pumpWidget(const SizedBox.shrink()); + controller.dispose(); + }); +} diff --git a/test/monetization_controller_test.dart b/test/monetization_controller_test.dart index fa6a937..3a03cb1 100644 --- a/test/monetization_controller_test.dart +++ b/test/monetization_controller_test.dart @@ -77,4 +77,53 @@ void main() { controller.dispose(); }); + + test( + 'shows banner only for initialized free users after onboarding', + () async { + final controller = MonetizationController(); + await controller.initialize(); + + expect( + controller.shouldShowBanner( + onboardingCompleted: true, + mobileAdsRuntimeSupported: true, + ), + isTrue, + ); + expect( + controller.shouldShowBanner( + onboardingCompleted: false, + mobileAdsRuntimeSupported: true, + ), + isFalse, + ); + + await controller.grantTemporaryAdFreeTime(const Duration(minutes: 1)); + + expect( + controller.shouldShowBanner( + onboardingCompleted: true, + mobileAdsRuntimeSupported: true, + ), + isFalse, + ); + + await controller.resetTemporaryAdFreeTime(); + await controller.processPurchase( + MonetizationConfig.productRemoveAds, + 'token-1', + ); + + expect( + controller.shouldShowBanner( + onboardingCompleted: true, + mobileAdsRuntimeSupported: true, + ), + isFalse, + ); + + controller.dispose(); + }, + ); } diff --git a/test/storage_repositories_test.dart b/test/storage_repositories_test.dart index 77a1dc8..780b015 100644 --- a/test/storage_repositories_test.dart +++ b/test/storage_repositories_test.dart @@ -609,6 +609,7 @@ void main() { showAttachmentPreviews: false, dccDownloadDirectoryPath: r'C:\Downloads\IRC', mediaDownloadDirectoryPath: r'C:\Downloads\Media', + mediaAutoDownloadMode: MediaAutoDownloadMode.wifiOnly, themePreset: AppThemePreset.dark, customThemeJson: '{"primary":"#123456"}', messageFontScale: 1.2, @@ -625,6 +626,7 @@ void main() { expect(settings.showAttachmentPreviews, isFalse); expect(settings.dccDownloadDirectoryPath, r'C:\Downloads\IRC'); expect(settings.mediaDownloadDirectoryPath, r'C:\Downloads\Media'); + expect(settings.mediaAutoDownloadMode, MediaAutoDownloadMode.wifiOnly); expect(settings.themePreset, AppThemePreset.dark); expect(settings.customThemeJson, '{"primary":"#123456"}'); expect(settings.messageFontScale, 1.2); diff --git a/test/widget_test.dart b/test/widget_test.dart index 26b8c82..9d44e4e 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -30,6 +30,7 @@ import 'package:androidircx/media/services/link_preview_service.dart'; import 'package:androidircx/features/settings/presentation/settings_screen.dart'; import 'package:androidircx/irc/services/irc_service.dart'; import 'package:androidircx/irc/services/irc_transport.dart'; +import 'package:androidircx/media/services/media_auto_download_policy.dart'; import 'package:androidircx/media/services/media_download_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -166,6 +167,19 @@ class _FakeMediaDownloadService implements MediaDownloadService { } } +class _FakeMediaAutoDownloadPolicy implements MediaAutoDownloadPolicy { + _FakeMediaAutoDownloadPolicy({required this.allowed}); + + bool allowed; + final modes = []; + + @override + Future canAutoDownload(MediaAutoDownloadMode mode) async { + modes.add(mode); + return allowed; + } +} + class _FakeSettingsRepository implements SettingsRepository { _FakeSettingsRepository(this._settings); @@ -693,6 +707,28 @@ void main() { expect(settings.mediaDownloadDirectoryPath, r'C:\Downloads\Media'); }); + testWidgets('settings saves media auto-download mode', (tester) async { + SharedPreferences.setMockInitialValues({}); + + await tester.pumpWidget(const MaterialApp(home: SettingsScreen())); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + await tester.scrollUntilVisible( + find.byKey(const Key('settings-media-auto-download')), + 500, + ); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('settings-media-auto-download'))); + await tester.pumpAndSettle(); + await tester.tap(find.text('Any network').last); + await tester.pumpAndSettle(); + + final settings = await SharedPrefsSettingsRepository().loadSettings(); + + expect(settings.mediaAutoDownloadMode, MediaAutoDownloadMode.always); + }); + testWidgets('settings saves appearance and theme options', (tester) async { SharedPreferences.setMockInitialValues({}); @@ -1353,6 +1389,69 @@ void main() { controller.dispose(); }); + testWidgets('auto-downloads new media attachments when enabled', ( + tester, + ) async { + SharedPreferences.setMockInitialValues({}); + const network = NetworkConfig( + id: 'dbase', + name: 'DBase', + host: 'irc.dbase.in.rs', + port: 6697, + nickname: 'AndroidIRCX', + altNickname: 'AndroidIRCX_', + ); + final transport = _FakeTransport(); + final mediaDownloadService = _FakeMediaDownloadService(); + final autoDownloadPolicy = _FakeMediaAutoDownloadPolicy(allowed: true); + final controller = ChatSessionController( + network: network, + ircService: IrcService(transportConnector: (_) async => transport), + settingsRepository: _FakeSettingsRepository( + const AppSettings( + mediaDownloadDirectoryPath: r'C:\Downloads\Media', + mediaAutoDownloadMode: MediaAutoDownloadMode.always, + ), + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: ChatScreen( + controller: controller, + mediaDownloadService: mediaDownloadService, + mediaAutoDownloadPolicy: autoDownloadPolicy, + ), + ), + ); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + await controller.joinChannel(const JoinChannelRequest(channel: '#room')); + await tester.pump(); + transport.emit( + ':alice!user@example PRIVMSG #room :auto https://example.com/auto.pdf', + ); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 10)); + + expect(autoDownloadPolicy.modes, [MediaAutoDownloadMode.always]); + expect(mediaDownloadService.calls, hasLength(1)); + expect( + mediaDownloadService.calls.single.url, + 'https://example.com/auto.pdf', + ); + expect( + mediaDownloadService.calls.single.directoryPath, + r'C:\Downloads\Media', + ); + + controller.selectTab(controller.activeTabId); + await tester.pump(); + expect(mediaDownloadService.calls, hasLength(1)); + + controller.dispose(); + }); + testWidgets('fills composer from message quote and reply actions', ( tester, ) async { diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index f54e12f..ce137d2 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -18,6 +19,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { AudioplayersWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin")); + ConnectivityPlusWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); FirebaseAppCheckPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index a3e1dfc..b6ef2ed 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST audioplayers_windows + connectivity_plus file_selector_windows firebase_app_check firebase_core