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 <kimura@texas0295.top>
This commit is contained in:
@@ -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",
|
||||
|
@@ -316,6 +316,7 @@
|
||||
"settingsRealmCompactView": "紧凑领域视图",
|
||||
"settingsMixedFeed": "混合动态",
|
||||
"settingsDataSavingMode": "流量节省模式",
|
||||
"dataSavingHint": "流量节省模式",
|
||||
"settingsAutoTranslate": "自动翻译",
|
||||
"settingsHideBottomNav": "隐藏底部导航",
|
||||
"settingsSoundEffects": "音效",
|
||||
|
@@ -315,7 +315,8 @@
|
||||
"settingsRealmCompactView": "緊湊領域視圖",
|
||||
"settingsMixedFeed": "混合動態",
|
||||
"settingsAutoTranslate": "自動翻譯",
|
||||
"settingsDataSavingMode": "低數據模式",
|
||||
"settingsDataSavingMode": "低數據模式",
|
||||
"dataSavingHint": "低數據模式",
|
||||
"settingsHideBottomNav": "隱藏底部導航",
|
||||
"settingsSoundEffects": "音效",
|
||||
"settingsAprilFoolFeatures": "愚人節功能",
|
||||
|
27
lib/utils/data_saving_gate.dart
Normal file
27
lib/utils/data_saving_gate.dart
Normal file
@@ -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;
|
||||
}
|
||||
}
|
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
Reference in New Issue
Block a user