fixup! data-saving: implement gate with bypass
This commit is contained in:
		| @@ -5,11 +5,11 @@ import 'dart:ui'; | |||||||
|  |  | ||||||
| import 'package:dismissible_page/dismissible_page.dart'; | import 'package:dismissible_page/dismissible_page.dart'; | ||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package:easy_localization/easy_localization.dart'; | ||||||
|  | import 'package:flutter_blurhash/flutter_blurhash.dart'; | ||||||
| import 'package:file_saver/file_saver.dart'; | import 'package:file_saver/file_saver.dart'; | ||||||
| import 'package:flutter/foundation.dart'; | import 'package:flutter/foundation.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:flutter/services.dart'; | import 'package:flutter/services.dart'; | ||||||
| import 'package:flutter_blurhash/flutter_blurhash.dart'; |  | ||||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | import 'package:flutter_hooks/flutter_hooks.dart'; | ||||||
| import 'package:gap/gap.dart'; | import 'package:gap/gap.dart'; | ||||||
| import 'package:gal/gal.dart'; | import 'package:gal/gal.dart'; | ||||||
| @@ -802,157 +802,81 @@ class _CloudFileListEntry extends HookConsumerWidget { | |||||||
|     this.onTap, |     this.onTap, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   @override | @override | ||||||
|   Widget build(BuildContext context, WidgetRef ref) { | Widget build(BuildContext context, WidgetRef ref) { | ||||||
|  |     final dataSaving = ref.watch( | ||||||
|  |         appSettingsNotifierProvider.select((s) => s.dataSavingMode), | ||||||
|  |     ); | ||||||
|     final showMature = useState(false); |     final showMature = useState(false); | ||||||
|  |     final showDataSaving = useState(!dataSaving); | ||||||
|  |     final lockedByDS     = dataSaving && !showDataSaving.value; | ||||||
|  |     final lockedByMature = file.sensitiveMarks.isNotEmpty && !showMature.value; | ||||||
|  |     final meta = file.fileMeta is Map ? file.fileMeta as Map : const {}; | ||||||
|  |     final ratio = (meta['ratio'] is num && (meta['ratio'] as num) != 0) | ||||||
|  |       ? (meta['ratio'] as num).toDouble() | ||||||
|  |       : 1.0; | ||||||
|  |  | ||||||
|     var content = Stack( |     Widget bg = const SizedBox.shrink(); | ||||||
|       fit: StackFit.expand, |     if (isImage) { | ||||||
|       children: [ |         if (meta['blur'] is String) { | ||||||
|         if (isImage) |             bg = BlurHash(hash: meta['blur'] as String); | ||||||
|           Positioned.fill( |         } else if (!lockedByDS && !lockedByMature) { | ||||||
|             child: |             bg = ImageFiltered( | ||||||
|                 file.fileMeta?['blur'] is String |  | ||||||
|                     ? BlurHash(hash: file.fileMeta?['blur']) |  | ||||||
|                     : ImageFiltered( |  | ||||||
|                 imageFilter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), |                 imageFilter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), | ||||||
|                       child: CloudFileWidget(item: file, noBlurhash: true), |                 child: CloudFileWidget( | ||||||
|  |                     item: file, | ||||||
|  |                     noBlurhash: true, | ||||||
|  |                     useInternalGate: false, | ||||||
|                 ), |                 ), | ||||||
|           ), |             ); | ||||||
|         if (isImage) |         } else { | ||||||
|           CloudFileWidget( |             bg = const ColoredBox(color: Colors.black26); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     final bool fullyUnlocked = !lockedByDS && !lockedByMature; | ||||||
|  |     Widget fg = fullyUnlocked | ||||||
|  |         ? (isImage | ||||||
|  |             ? CloudFileWidget( | ||||||
|                 item: file, |                 item: file, | ||||||
|                 heroTag: heroTag, |                 heroTag: heroTag, | ||||||
|                 noBlurhash: true, |                 noBlurhash: true, | ||||||
|                 fit: BoxFit.contain, |                 fit: BoxFit.contain, | ||||||
|  |                 useInternalGate: false, | ||||||
|             ) |             ) | ||||||
|         else |             : CloudFileWidget( | ||||||
|           CloudFileWidget(item: file, heroTag: heroTag, fit: BoxFit.contain), |                 item: file, | ||||||
|       ], |                 heroTag: heroTag, | ||||||
|     ); |                 fit: BoxFit.contain, | ||||||
|  |                 useInternalGate: false, | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |         : AspectRatio(aspectRatio: ratio, child: const SizedBox.shrink()); | ||||||
|  |  | ||||||
|     if (file.sensitiveMarks.isNotEmpty) { |     Widget overlays; | ||||||
|       // Show a blurred overlay only when not revealed yet, with a smooth transition |     if (lockedByDS) { | ||||||
|       content = Stack( |         overlays = _DataSavingOverlay(); | ||||||
|         children: [ |     } else if (lockedByMature) { | ||||||
|           content, |         overlays = _SensitiveOverlay(file: file); | ||||||
|           // Toggle blur overlay with animation |     } else { | ||||||
|           Positioned.fill( |         overlays = const SizedBox.shrink(); | ||||||
|             child: AnimatedSwitcher( |  | ||||||
|               duration: const Duration(milliseconds: 250), |  | ||||||
|               switchInCurve: Curves.easeOut, |  | ||||||
|               switchOutCurve: Curves.easeIn, |  | ||||||
|               layoutBuilder: |  | ||||||
|                   (currentChild, previousChildren) => Stack( |  | ||||||
|                     fit: StackFit.expand, |  | ||||||
|                     children: [ |  | ||||||
|                       ...previousChildren, |  | ||||||
|                       if (currentChild != null) currentChild, |  | ||||||
|                     ], |  | ||||||
|                   ), |  | ||||||
|               child: |  | ||||||
|                   showMature.value |  | ||||||
|                       ? const SizedBox.shrink(key: ValueKey('revealed')) |  | ||||||
|                       : ColoredBox( |  | ||||||
|                         key: const ValueKey('blurred'), |  | ||||||
|                         color: Colors.transparent, |  | ||||||
|                         child: BackdropFilter( |  | ||||||
|                           filter: ImageFilter.blur(sigmaX: 64, sigmaY: 64), |  | ||||||
|                           child: Stack( |  | ||||||
|                             fit: StackFit.expand, |  | ||||||
|                             children: [ |  | ||||||
|                               const ColoredBox(color: Colors.transparent), |  | ||||||
|                               Center( |  | ||||||
|                                 child: Container( |  | ||||||
|                                   margin: const EdgeInsets.all(12), |  | ||||||
|                                   padding: const EdgeInsets.symmetric( |  | ||||||
|                                     horizontal: 12, |  | ||||||
|                                     vertical: 8, |  | ||||||
|                                   ), |  | ||||||
|                                   decoration: BoxDecoration( |  | ||||||
|                                     color: Colors.black54, |  | ||||||
|                                     borderRadius: BorderRadius.circular(12), |  | ||||||
|                                   ), |  | ||||||
|                                   child: ConstrainedBox( |  | ||||||
|                                     constraints: const BoxConstraints( |  | ||||||
|                                       maxWidth: 280, |  | ||||||
|                                     ), |  | ||||||
|                                     child: Column( |  | ||||||
|                                       mainAxisSize: MainAxisSize.min, |  | ||||||
|                                       children: [ |  | ||||||
|                                         const Icon( |  | ||||||
|                                           Icons.warning, |  | ||||||
|                                           color: Colors.white, |  | ||||||
|                                           fill: 1, |  | ||||||
|                                           size: 24, |  | ||||||
|                                         ), |  | ||||||
|                                         const Gap(4), |  | ||||||
|                                         Text( |  | ||||||
|                                           file.sensitiveMarks |  | ||||||
|                                               .map( |  | ||||||
|                                                 (e) => |  | ||||||
|                                                     SensitiveCategory |  | ||||||
|                                                         .values[e] |  | ||||||
|                                                         .i18nKey |  | ||||||
|                                                         .tr(), |  | ||||||
|                                               ) |  | ||||||
|                                               .join(' · '), |  | ||||||
|                                           style: const TextStyle( |  | ||||||
|                                             color: Colors.white, |  | ||||||
|                                             fontWeight: FontWeight.w600, |  | ||||||
|                                           ), |  | ||||||
|                                           textAlign: TextAlign.center, |  | ||||||
|                                         ), |  | ||||||
|                                         Text( |  | ||||||
|                                           'Sensitive Content', |  | ||||||
|                                           style: TextStyle( |  | ||||||
|                                             color: Colors.white, |  | ||||||
|                                             fontSize: 13, |  | ||||||
|                                           ), |  | ||||||
|                                         ), |  | ||||||
|                                         const Gap(4), |  | ||||||
|                                         Text( |  | ||||||
|                                           'Tap to Reveal', |  | ||||||
|                                           style: TextStyle( |  | ||||||
|                                             color: Colors.white, |  | ||||||
|                                             fontSize: 11, |  | ||||||
|                                           ), |  | ||||||
|                                         ), |  | ||||||
|                                       ], |  | ||||||
|                                     ), |  | ||||||
|                                   ).padding(horizontal: 24, vertical: 16), |  | ||||||
|                                 ), |  | ||||||
|                               ), |  | ||||||
|                             ], |  | ||||||
|                           ), |  | ||||||
|                         ), |  | ||||||
|                       ), |  | ||||||
|             ), |  | ||||||
|           ), |  | ||||||
|           // When revealed (no blur), show a small control at top-left to re-enable blur |  | ||||||
|           if (showMature.value) |  | ||||||
|             Positioned( |  | ||||||
|               top: 3, |  | ||||||
|               left: 4, |  | ||||||
|               child: IconButton( |  | ||||||
|                 iconSize: 16, |  | ||||||
|                 constraints: const BoxConstraints(), |  | ||||||
|                 icon: const Icon(Icons.visibility_off, color: Colors.white), |  | ||||||
|                 tooltip: 'Blur content', |  | ||||||
|                 onPressed: () { |  | ||||||
|                   showMature.value = false; |  | ||||||
|                 }, |  | ||||||
|               ), |  | ||||||
|             ), |  | ||||||
|         ], |  | ||||||
|       ); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (onTap != null) { |     final content = Stack( | ||||||
|  |         fit: StackFit.expand, | ||||||
|  |         children: [ | ||||||
|  |             if (isImage) Positioned.fill(child: bg), | ||||||
|  |             fg, | ||||||
|  |             overlays, | ||||||
|  |         ], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     return InkWell( |     return InkWell( | ||||||
|         borderRadius: const BorderRadius.all(Radius.circular(16)), |         borderRadius: const BorderRadius.all(Radius.circular(16)), | ||||||
|         onTap: () { |         onTap: () { | ||||||
|           if (!showMature.value) { |             if (lockedByDS) { | ||||||
|  |                 showDataSaving.value = true; | ||||||
|  |             } else if (lockedByMature) { | ||||||
|                 showMature.value = true; |                 showMature.value = true; | ||||||
|             } else { |             } else { | ||||||
|                 onTap?.call(); |                 onTap?.call(); | ||||||
| @@ -960,8 +884,89 @@ class _CloudFileListEntry extends HookConsumerWidget { | |||||||
|         }, |         }, | ||||||
|         child: content, |         child: content, | ||||||
|     ); |     ); | ||||||
|     } | } | ||||||
|  | } | ||||||
|  |  | ||||||
|     return content; | class _SensitiveOverlay extends StatelessWidget { | ||||||
|  |   final SnCloudFile file; | ||||||
|  |   const _SensitiveOverlay({required this.file}); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return BackdropFilter( | ||||||
|  |       filter: ImageFilter.blur(sigmaX: 64, sigmaY: 64), | ||||||
|  |       child: Container( | ||||||
|  |         color: Colors.transparent, | ||||||
|  |         child: Center( | ||||||
|  |           child: _OverlayCard( | ||||||
|  |             icon: Icons.warning, | ||||||
|  |             title: file.sensitiveMarks | ||||||
|  |                 .map((e) => SensitiveCategory.values[e].i18nKey.tr()) | ||||||
|  |                 .join(' · '), | ||||||
|  |             subtitle: 'Sensitive Content', | ||||||
|  |             hint: 'Tap to Reveal', | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _DataSavingOverlay extends StatelessWidget { | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return ColoredBox( | ||||||
|  |       color: Colors.black38, | ||||||
|  |       child: Center( | ||||||
|  |         child: _OverlayCard( | ||||||
|  |           icon: Symbols.image, | ||||||
|  |           title: 'Data Saving Mode', | ||||||
|  |           subtitle: '', | ||||||
|  |           hint: 'Tap to Load', | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | class _OverlayCard extends StatelessWidget { | ||||||
|  |   final IconData icon; | ||||||
|  |   final String title; | ||||||
|  |   final String subtitle; | ||||||
|  |   final String hint; | ||||||
|  |  | ||||||
|  |   const _OverlayCard({ | ||||||
|  |     required this.icon, | ||||||
|  |     required this.title, | ||||||
|  |     required this.subtitle, | ||||||
|  |     required this.hint, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return Container( | ||||||
|  |       margin: const EdgeInsets.all(12), | ||||||
|  |       padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), | ||||||
|  |       decoration: BoxDecoration( | ||||||
|  |         color: Colors.black54, | ||||||
|  |         borderRadius: BorderRadius.circular(12), | ||||||
|  |       ), | ||||||
|  |       constraints: const BoxConstraints(maxWidth: 280), | ||||||
|  |       child: Column( | ||||||
|  |         mainAxisSize: MainAxisSize.min, | ||||||
|  |         children: [ | ||||||
|  |           Icon(icon, color: Colors.white, size: 24), | ||||||
|  |           const Gap(4), | ||||||
|  |           Text(title, | ||||||
|  |               style: const TextStyle( | ||||||
|  |                   color: Colors.white, fontWeight: FontWeight.w600), | ||||||
|  |               textAlign: TextAlign.center), | ||||||
|  |           Text(subtitle, | ||||||
|  |               style: const TextStyle(color: Colors.white, fontSize: 13)), | ||||||
|  |           const Gap(4), | ||||||
|  |           Text(hint, | ||||||
|  |               style: const TextStyle(color: Colors.white, fontSize: 11)), | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -14,7 +14,7 @@ import 'package:island/widgets/content/audio.dart'; | |||||||
| import 'package:material_symbols_icons/symbols.dart'; | import 'package:material_symbols_icons/symbols.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
| import 'package:url_launcher/url_launcher_string.dart'; | import 'package:url_launcher/url_launcher_string.dart'; | ||||||
| import 'package:island/utils/data_saving_gate.dart'; | import 'package:island/widgets/data_saving_gate.dart'; | ||||||
|  |  | ||||||
| import 'image.dart'; | import 'image.dart'; | ||||||
| import 'video.dart'; | import 'video.dart'; | ||||||
| @@ -24,12 +24,14 @@ class CloudFileWidget extends HookConsumerWidget { | |||||||
|   final BoxFit fit; |   final BoxFit fit; | ||||||
|   final String? heroTag; |   final String? heroTag; | ||||||
|   final bool noBlurhash; |   final bool noBlurhash; | ||||||
|  |   final bool useInternalGate; | ||||||
|   const CloudFileWidget({ |   const CloudFileWidget({ | ||||||
|     super.key, |     super.key, | ||||||
|     required this.item, |     required this.item, | ||||||
|     this.fit = BoxFit.cover, |     this.fit = BoxFit.cover, | ||||||
|     this.heroTag, |     this.heroTag, | ||||||
|     this.noBlurhash = false, |     this.noBlurhash = false, | ||||||
|  |     this.useInternalGate = true, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
| @@ -60,11 +62,11 @@ class CloudFileWidget extends HookConsumerWidget { | |||||||
|     var content = switch (item.mimeType?.split('/').firstOrNull) { |     var content = switch (item.mimeType?.split('/').firstOrNull) { | ||||||
|       'image' => AspectRatio( |       'image' => AspectRatio( | ||||||
|           aspectRatio: ratio, |           aspectRatio: ratio, | ||||||
|           child: (dataSaving && !unlocked.value) ? dataPlaceHolder(Symbols.image) : cloudImage(), |           child: (useInternalGate && dataSaving && !unlocked.value) ? dataPlaceHolder(Symbols.image) : cloudImage(), | ||||||
|         ), |         ), | ||||||
|       'video' => AspectRatio( |       'video' => AspectRatio( | ||||||
|           aspectRatio: ratio, |           aspectRatio: ratio, | ||||||
|           child: (dataSaving && !unlocked.value) ? dataPlaceHolder(Symbols.play_arrow) : cloudVideo(), |           child: (useInternalGate && dataSaving && !unlocked.value) ? dataPlaceHolder(Symbols.play_arrow) : cloudVideo(), | ||||||
|         ), |         ), | ||||||
|       'audio' => Center( |       'audio' => Center( | ||||||
|           child: ConstrainedBox( |           child: ConstrainedBox( | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user