From a8c3830d677d54f74c1cf45be044572226c0de08 Mon Sep 17 00:00:00 2001 From: Texas0295 Date: Sat, 6 Sep 2025 13:53:19 +0800 Subject: [PATCH] data-saving: implement gate with bypass - Implement DataSavingGate util (previous commit was only the shell) - Update ProfilePictureWidget to always load avatars via UniversalImage using fileId, bypassing CloudFileWidget and its data-saving check - Keep larger media under data-saving control - Add i18n strings for data-saving mode Signed-off-by: Texas0295 --- assets/i18n/en-US.json | 1 + assets/i18n/zh-CN.json | 1 + assets/i18n/zh-TW.json | 3 +- lib/utils/data_saving_gate.dart | 27 +++++++++ lib/widgets/content/cloud_files.dart | 85 +++++++++++++++++++++------- 5 files changed, 96 insertions(+), 21 deletions(-) create mode 100644 lib/utils/data_saving_gate.dart diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index 2e18499b..4027864f 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -350,6 +350,7 @@ "settingsRealmCompactView": "Compact Realm View", "settingsMixedFeed": "Mixed Feed", "settingsDataSavingMode": "Data Saving Mode", + "dataSavingHint": "Data Saving Mode", "settingsAutoTranslate": "Auto Translate", "settingsHideBottomNav": "Hide Bottom Navigation", "settingsSoundEffects": "Sound Effects", diff --git a/assets/i18n/zh-CN.json b/assets/i18n/zh-CN.json index c87ea76e..5fe9a354 100644 --- a/assets/i18n/zh-CN.json +++ b/assets/i18n/zh-CN.json @@ -316,6 +316,7 @@ "settingsRealmCompactView": "紧凑领域视图", "settingsMixedFeed": "混合动态", "settingsDataSavingMode": "流量节省模式", + "dataSavingHint": "流量节省模式", "settingsAutoTranslate": "自动翻译", "settingsHideBottomNav": "隐藏底部导航", "settingsSoundEffects": "音效", diff --git a/assets/i18n/zh-TW.json b/assets/i18n/zh-TW.json index cc5a1668..d8d56cfe 100644 --- a/assets/i18n/zh-TW.json +++ b/assets/i18n/zh-TW.json @@ -315,7 +315,8 @@ "settingsRealmCompactView": "緊湊領域視圖", "settingsMixedFeed": "混合動態", "settingsAutoTranslate": "自動翻譯", - "settingsDataSavingMode": "低數據模式", + "settingsDataSavingMode": "低數據模式", + "dataSavingHint": "低數據模式", "settingsHideBottomNav": "隱藏底部導航", "settingsSoundEffects": "音效", "settingsAprilFoolFeatures": "愚人節功能", diff --git a/lib/utils/data_saving_gate.dart b/lib/utils/data_saving_gate.dart new file mode 100644 index 00000000..dc4c23b3 --- /dev/null +++ b/lib/utils/data_saving_gate.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/pods/config.dart'; + + +typedef WidgetBuilder0 = Widget Function(); + +class DataSavingGate extends ConsumerWidget { + final bool bypass; + final WidgetBuilder0 content; + final Widget placeholder; + + const DataSavingGate({ + super.key, + required this.bypass, + required this.content, + required this.placeholder, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final dataSaving = + ref.watch(appSettingsNotifierProvider.select((s) => s.dataSavingMode)); + if (bypass || !dataSaving) return content(); + return placeholder; + } +} diff --git a/lib/widgets/content/cloud_files.dart b/lib/widgets/content/cloud_files.dart index c9d1bfe4..7729fba2 100644 --- a/lib/widgets/content/cloud_files.dart +++ b/lib/widgets/content/cloud_files.dart @@ -14,6 +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 'image.dart'; import 'video.dart'; @@ -33,6 +34,7 @@ class CloudFileWidget extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final dataSaving = ref.watch(appSettingsNotifierProvider.select((s) => s.dataSavingMode)); final serverUrl = ref.watch(serverUrlProvider); final uri = '$serverUrl/drive/files/${item.id}'; @@ -44,7 +46,12 @@ class CloudFileWidget extends HookConsumerWidget { var content = switch (item.mimeType?.split('/').firstOrNull) { "image" => AspectRatio( aspectRatio: ratio, - child: UniversalImage( + child: dataSaving ? _DataSavingPlaceholder( + icon: Symbols.image, + onTap: () { + // TODO: single picture unlock logic + }) + : UniversalImage( uri: uri, blurHash: noBlurhash @@ -54,7 +61,13 @@ class CloudFileWidget extends HookConsumerWidget { ), "video" => AspectRatio( aspectRatio: ratio, - child: CloudVideoWidget(item: item), + child: dataSaving ? _DataSavingPlaceholder( + icon: Symbols.play_arrow, + onTap: () { + // TODO: single vedio unlock logic + } + ) + : CloudVideoWidget(item: item), ), "audio" => Center( child: ConstrainedBox( @@ -113,6 +126,35 @@ class CloudFileWidget extends HookConsumerWidget { } } +class _DataSavingPlaceholder extends StatelessWidget { + final IconData icon; + final VoidCallback onTap; + const _DataSavingPlaceholder({required this.icon, required this.onTap}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + color: Colors.black26, + alignment: Alignment.center, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 36, + color: Theme.of(context).colorScheme.onSurfaceVariant), + const Gap(8), + Text( + 'dataSavingHint'.tr(), + style: Theme.of(context).textTheme.bodySmall, + textAlign: TextAlign.center, + ), + ], + ), + ), + ); + } +} class CloudVideoWidget extends HookConsumerWidget { final SnCloudFile item; const CloudVideoWidget({super.key, required this.item}); @@ -311,32 +353,35 @@ class ProfilePictureWidget extends ConsumerWidget { this.fallbackColor, }); - @override +@override Widget build(BuildContext context, WidgetRef ref) { final serverUrl = ref.watch(serverUrlProvider); - final uri = '$serverUrl/drive/files/${file?.id ?? fileId}'; + final String? id = file?.id ?? fileId; + + final fallback = Icon( + fallbackIcon ?? Symbols.account_circle, + size: radius, + color: fallbackColor ?? Theme.of(context).colorScheme.onPrimaryContainer, + ).center(); return ClipRRect( - borderRadius: - borderRadius == null - ? BorderRadius.all(Radius.circular(radius)) - : BorderRadius.all(Radius.circular(borderRadius!)), + borderRadius: borderRadius == null + ? BorderRadius.all(Radius.circular(radius)) + : BorderRadius.all(Radius.circular(borderRadius!)), child: Container( width: radius * 2, height: radius * 2, color: Theme.of(context).colorScheme.primaryContainer, - child: - file != null - ? CloudFileWidget(item: file!, fit: BoxFit.cover) - : fileId == null - ? Icon( - fallbackIcon ?? Symbols.account_circle, - size: radius, - color: - fallbackColor ?? - Theme.of(context).colorScheme.onPrimaryContainer, - ).center() - : UniversalImage(uri: uri, fit: BoxFit.cover), + child: id == null + ? fallback + : DataSavingGate( + bypass: true, // 小頭像永遠繞過低數據 + placeholder: fallback, + content: () => UniversalImage( + uri: '$serverUrl/drive/files/$id', + fit: BoxFit.cover, + ), + ), ), ); }