✨ Sensitive marks hidden image
This commit is contained in:
@@ -2,6 +2,7 @@ import 'dart:math' as math;
|
|||||||
import 'dart:ui';
|
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:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_blurhash/flutter_blurhash.dart';
|
import 'package:flutter_blurhash/flutter_blurhash.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
@@ -13,6 +14,7 @@ import 'package:island/pods/config.dart';
|
|||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/content/cloud_files.dart';
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
|
import 'package:island/widgets/content/sensitive.dart';
|
||||||
import 'package:island/widgets/content/sheet.dart';
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:path/path.dart' show extension;
|
import 'package:path/path.dart' show extension;
|
||||||
@@ -558,7 +560,7 @@ class CloudFileZoomIn extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CloudFileListEntry extends StatelessWidget {
|
class _CloudFileListEntry extends HookConsumerWidget {
|
||||||
final SnCloudFile file;
|
final SnCloudFile file;
|
||||||
final String heroTag;
|
final String heroTag;
|
||||||
final bool isImage;
|
final bool isImage;
|
||||||
@@ -574,8 +576,10 @@ class _CloudFileListEntry extends StatelessWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final content = Stack(
|
final showMature = useState(false);
|
||||||
|
|
||||||
|
var content = Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
children: [
|
children: [
|
||||||
if (isImage)
|
if (isImage)
|
||||||
@@ -600,10 +604,133 @@ class _CloudFileListEntry extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
'Sensitive Content',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Show categories for sensitive content
|
||||||
|
Text(
|
||||||
|
file.sensitiveMarks
|
||||||
|
.map(
|
||||||
|
(e) =>
|
||||||
|
SensitiveCategory
|
||||||
|
.values[e]
|
||||||
|
.i18nKey
|
||||||
|
.tr(),
|
||||||
|
)
|
||||||
|
.join(' · '),
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const Gap(4),
|
||||||
|
Text(
|
||||||
|
'Tap to Reveal',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 11,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 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) {
|
if (onTap != null) {
|
||||||
return InkWell(
|
return InkWell(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||||
onTap: onTap,
|
onTap: () {
|
||||||
|
if (!showMature.value) {
|
||||||
|
showMature.value = true;
|
||||||
|
} else {
|
||||||
|
onTap?.call();
|
||||||
|
}
|
||||||
|
},
|
||||||
child: content,
|
child: content,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -14,7 +15,7 @@ import 'package:styled_widget/styled_widget.dart';
|
|||||||
import 'image.dart';
|
import 'image.dart';
|
||||||
import 'video.dart';
|
import 'video.dart';
|
||||||
|
|
||||||
class CloudFileWidget extends ConsumerWidget {
|
class CloudFileWidget extends HookConsumerWidget {
|
||||||
final SnCloudFile item;
|
final SnCloudFile item;
|
||||||
final BoxFit fit;
|
final BoxFit fit;
|
||||||
final String? heroTag;
|
final String? heroTag;
|
||||||
@@ -37,7 +38,7 @@ class CloudFileWidget extends ConsumerWidget {
|
|||||||
? item.fileMeta!['ratio'].toDouble()
|
? item.fileMeta!['ratio'].toDouble()
|
||||||
: 1.0;
|
: 1.0;
|
||||||
if (ratio == 0) ratio = 1.0;
|
if (ratio == 0) ratio = 1.0;
|
||||||
final content = switch (item.mimeType?.split('/').firstOrNull) {
|
var content = switch (item.mimeType?.split('/').firstOrNull) {
|
||||||
"image" => AspectRatio(
|
"image" => AspectRatio(
|
||||||
aspectRatio: ratio,
|
aspectRatio: ratio,
|
||||||
child: UniversalImage(
|
child: UniversalImage(
|
||||||
@@ -64,7 +65,7 @@ class CloudFileWidget extends ConsumerWidget {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (heroTag != null) {
|
if (heroTag != null) {
|
||||||
return Hero(tag: heroTag!, child: content);
|
content = Hero(tag: heroTag!, child: content);
|
||||||
}
|
}
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
|
Reference in New Issue
Block a user