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,166 +802,171 @@ 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
|
imageFilter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||||
? BlurHash(hash: file.fileMeta?['blur'])
|
child: CloudFileWidget(
|
||||||
: ImageFiltered(
|
item: file,
|
||||||
imageFilter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
noBlurhash: true,
|
||||||
child: CloudFileWidget(item: file, noBlurhash: true),
|
useInternalGate: false,
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
if (isImage)
|
} else {
|
||||||
CloudFileWidget(
|
bg = const ColoredBox(color: Colors.black26);
|
||||||
item: file,
|
}
|
||||||
heroTag: heroTag,
|
}
|
||||||
noBlurhash: true,
|
|
||||||
fit: BoxFit.contain,
|
final bool fullyUnlocked = !lockedByDS && !lockedByMature;
|
||||||
)
|
Widget fg = fullyUnlocked
|
||||||
else
|
? (isImage
|
||||||
CloudFileWidget(item: file, heroTag: heroTag, fit: BoxFit.contain),
|
? 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) {
|
return InkWell(
|
||||||
// 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(
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (!showMature.value) {
|
if (lockedByDS) {
|
||||||
showMature.value = true;
|
showDataSaving.value = true;
|
||||||
} else {
|
} else if (lockedByMature) {
|
||||||
onTap?.call();
|
showMature.value = true;
|
||||||
}
|
} else {
|
||||||
|
onTap?.call();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
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