From 99b7bf81990965a33809961faa3758d12a53a322 Mon Sep 17 00:00:00 2001 From: Texas0295 Date: Sun, 7 Sep 2025 00:21:20 +0800 Subject: [PATCH] fixup! data-saving: implement gate with bypass --- .../content/cloud_file_collection.dart | 311 +++++++++--------- lib/widgets/content/cloud_files.dart | 8 +- lib/{utils => widgets}/data_saving_gate.dart | 0 3 files changed, 163 insertions(+), 156 deletions(-) rename lib/{utils => widgets}/data_saving_gate.dart (100%) diff --git a/lib/widgets/content/cloud_file_collection.dart b/lib/widgets/content/cloud_file_collection.dart index ca308c8b..ffed312b 100644 --- a/lib/widgets/content/cloud_file_collection.dart +++ b/lib/widgets/content/cloud_file_collection.dart @@ -5,11 +5,11 @@ import 'dart:ui'; import 'package:dismissible_page/dismissible_page.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter_blurhash/flutter_blurhash.dart'; import 'package:file_saver/file_saver.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_blurhash/flutter_blurhash.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:gal/gal.dart'; @@ -802,166 +802,171 @@ class _CloudFileListEntry extends HookConsumerWidget { this.onTap, }); - @override - Widget build(BuildContext context, WidgetRef ref) { +@override +Widget build(BuildContext context, WidgetRef ref) { + final dataSaving = ref.watch( + appSettingsNotifierProvider.select((s) => s.dataSavingMode), + ); 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( - fit: StackFit.expand, - children: [ - if (isImage) - Positioned.fill( - child: - file.fileMeta?['blur'] is String - ? BlurHash(hash: file.fileMeta?['blur']) - : ImageFiltered( - imageFilter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), - child: CloudFileWidget(item: file, noBlurhash: true), - ), - ), - if (isImage) - CloudFileWidget( - item: file, - heroTag: heroTag, - noBlurhash: true, - fit: BoxFit.contain, - ) - else - CloudFileWidget(item: file, heroTag: heroTag, fit: BoxFit.contain), - ], + Widget bg = const SizedBox.shrink(); + if (isImage) { + if (meta['blur'] is String) { + bg = BlurHash(hash: meta['blur'] as String); + } else if (!lockedByDS && !lockedByMature) { + bg = ImageFiltered( + imageFilter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: CloudFileWidget( + item: file, + noBlurhash: true, + useInternalGate: false, + ), + ); + } else { + bg = const ColoredBox(color: Colors.black26); + } + } + + final bool fullyUnlocked = !lockedByDS && !lockedByMature; + Widget fg = fullyUnlocked + ? (isImage + ? CloudFileWidget( + item: file, + heroTag: heroTag, + noBlurhash: true, + fit: BoxFit.contain, + useInternalGate: false, + ) + : CloudFileWidget( + item: file, + heroTag: heroTag, + fit: BoxFit.contain, + useInternalGate: false, + ) + ) + : AspectRatio(aspectRatio: ratio, child: const SizedBox.shrink()); + + Widget overlays; + if (lockedByDS) { + overlays = _DataSavingOverlay(); + } else if (lockedByMature) { + overlays = _SensitiveOverlay(file: file); + } else { + overlays = const SizedBox.shrink(); + } + + final content = Stack( + fit: StackFit.expand, + children: [ + if (isImage) Positioned.fill(child: bg), + fg, + overlays, + ], ); - if (file.sensitiveMarks.isNotEmpty) { - // Show a blurred overlay only when not revealed yet, with a smooth transition - content = Stack( - children: [ - content, - // Toggle blur overlay with animation - Positioned.fill( - 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) { - return InkWell( + return InkWell( borderRadius: const BorderRadius.all(Radius.circular(16)), onTap: () { - if (!showMature.value) { - showMature.value = true; - } else { - onTap?.call(); - } + if (lockedByDS) { + showDataSaving.value = true; + } else if (lockedByMature) { + showMature.value = true; + } else { + onTap?.call(); + } }, 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)), + ], + ), + ); } } diff --git a/lib/widgets/content/cloud_files.dart b/lib/widgets/content/cloud_files.dart index 22f93634..f0c15ccf 100644 --- a/lib/widgets/content/cloud_files.dart +++ b/lib/widgets/content/cloud_files.dart @@ -14,7 +14,7 @@ import 'package:island/widgets/content/audio.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:styled_widget/styled_widget.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 'video.dart'; @@ -24,12 +24,14 @@ class CloudFileWidget extends HookConsumerWidget { final BoxFit fit; final String? heroTag; final bool noBlurhash; + final bool useInternalGate; const CloudFileWidget({ super.key, required this.item, this.fit = BoxFit.cover, this.heroTag, this.noBlurhash = false, + this.useInternalGate = true, }); @override @@ -60,11 +62,11 @@ class CloudFileWidget extends HookConsumerWidget { var content = switch (item.mimeType?.split('/').firstOrNull) { 'image' => AspectRatio( aspectRatio: ratio, - child: (dataSaving && !unlocked.value) ? dataPlaceHolder(Symbols.image) : cloudImage(), + child: (useInternalGate && dataSaving && !unlocked.value) ? dataPlaceHolder(Symbols.image) : cloudImage(), ), 'video' => AspectRatio( aspectRatio: ratio, - child: (dataSaving && !unlocked.value) ? dataPlaceHolder(Symbols.play_arrow) : cloudVideo(), + child: (useInternalGate && dataSaving && !unlocked.value) ? dataPlaceHolder(Symbols.play_arrow) : cloudVideo(), ), 'audio' => Center( child: ConstrainedBox( diff --git a/lib/utils/data_saving_gate.dart b/lib/widgets/data_saving_gate.dart similarity index 100% rename from lib/utils/data_saving_gate.dart rename to lib/widgets/data_saving_gate.dart