#316 wallpaper: scroll effect option
This commit is contained in:
parent
38b9f84af0
commit
3d0e079df2
8 changed files with 160 additions and 44 deletions
|
@ -14,6 +14,7 @@ All notable changes to this project will be documented in this file.
|
|||
- Stats: open full top listings
|
||||
- Slideshow: option for no transition
|
||||
- Widget: tap action setting
|
||||
- Wallpaper: scroll effect option
|
||||
|
||||
### Fixed
|
||||
|
||||
|
|
|
@ -861,6 +861,8 @@
|
|||
"viewerInfoSearchSuggestionResolution": "Resolution",
|
||||
"viewerInfoSearchSuggestionRights": "Rights",
|
||||
|
||||
"wallpaperUseScrollEffect": "Use scroll effect on home screen",
|
||||
|
||||
"tagEditorPageTitle": "Edit Tags",
|
||||
"tagEditorPageNewTagFieldLabel": "New tag",
|
||||
"tagEditorPageAddTagTooltip": "Add tag",
|
||||
|
|
|
@ -676,6 +676,8 @@
|
|||
"viewerInfoSearchSuggestionResolution": "Résolution",
|
||||
"viewerInfoSearchSuggestionRights": "Droits",
|
||||
|
||||
"wallpaperUseScrollEffect": "Utiliser l’effet de défilement sur l’écran d’accueil",
|
||||
|
||||
"tagEditorPageTitle": "Modifier les libellés",
|
||||
"tagEditorPageNewTagFieldLabel": "Nouveau libellé",
|
||||
"tagEditorPageAddTagTooltip": "Ajouter le libellé",
|
||||
|
|
|
@ -676,6 +676,8 @@
|
|||
"viewerInfoSearchSuggestionResolution": "해상도",
|
||||
"viewerInfoSearchSuggestionRights": "권리",
|
||||
|
||||
"wallpaperUseScrollEffect": "홈 화면에 스크롤 효과 사용",
|
||||
|
||||
"tagEditorPageTitle": "태그 수정",
|
||||
"tagEditorPageNewTagFieldLabel": "새 태그",
|
||||
"tagEditorPageAddTagTooltip": "태그 추가",
|
||||
|
|
|
@ -67,7 +67,21 @@ class _AvesSelectionDialogState<T> extends State<AvesSelectionDialog<T>> {
|
|||
padding: const EdgeInsets.all(16),
|
||||
child: Text(message),
|
||||
),
|
||||
...widget.options.entries.map((kv) => _buildRadioListTile(kv.key, kv.value, needConfirmation)),
|
||||
...widget.options.entries.map((kv) {
|
||||
final value = kv.key;
|
||||
final title = kv.value;
|
||||
return SelectionRadioListTile(
|
||||
// key is expected by test driver
|
||||
key: Key(value.toString()),
|
||||
value: value,
|
||||
title: title,
|
||||
optionSubtitleBuilder: widget.optionSubtitleBuilder,
|
||||
needConfirmation: needConfirmation,
|
||||
dense: widget.dense,
|
||||
getGroupValue: () => _selectedValue,
|
||||
setGroupValue: (v) => setState(() => _selectedValue = v),
|
||||
);
|
||||
}),
|
||||
],
|
||||
actions: [
|
||||
TextButton(
|
||||
|
@ -82,17 +96,39 @@ class _AvesSelectionDialogState<T> extends State<AvesSelectionDialog<T>> {
|
|||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildRadioListTile(T value, String title, bool needConfirmation) {
|
||||
final subtitle = widget.optionSubtitleBuilder?.call(value);
|
||||
class SelectionRadioListTile<T> extends StatelessWidget {
|
||||
final T value;
|
||||
final String title;
|
||||
final TextBuilder<T>? optionSubtitleBuilder;
|
||||
final bool needConfirmation;
|
||||
final bool? dense;
|
||||
final T Function() getGroupValue;
|
||||
final void Function(T value) setGroupValue;
|
||||
|
||||
const SelectionRadioListTile({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.title,
|
||||
this.optionSubtitleBuilder,
|
||||
required this.needConfirmation,
|
||||
this.dense,
|
||||
required this.getGroupValue,
|
||||
required this.setGroupValue,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final subtitle = optionSubtitleBuilder?.call(value);
|
||||
return ReselectableRadioListTile<T>(
|
||||
// key is expected by test driver
|
||||
key: Key(value.toString()),
|
||||
value: value,
|
||||
groupValue: _selectedValue,
|
||||
groupValue: getGroupValue(),
|
||||
onChanged: (v) {
|
||||
if (needConfirmation) {
|
||||
setState(() => _selectedValue = v as T);
|
||||
setGroupValue(v as T);
|
||||
} else {
|
||||
Navigator.pop(context, v);
|
||||
}
|
||||
|
@ -112,7 +148,7 @@ class _AvesSelectionDialogState<T> extends State<AvesSelectionDialog<T>> {
|
|||
maxLines: 1,
|
||||
)
|
||||
: null,
|
||||
dense: widget.dense,
|
||||
dense: dense,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
53
lib/widgets/dialogs/wallpaper_settings_dialog.dart
Normal file
53
lib/widgets/dialogs/wallpaper_settings_dialog.dart
Normal file
|
@ -0,0 +1,53 @@
|
|||
import 'package:aves/model/device.dart';
|
||||
import 'package:aves/model/wallpaper_target.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
import 'aves_dialog.dart';
|
||||
|
||||
class WallpaperSettingsDialog extends StatefulWidget {
|
||||
const WallpaperSettingsDialog({super.key});
|
||||
|
||||
@override
|
||||
State<WallpaperSettingsDialog> createState() => _WallpaperSettingsDialogState();
|
||||
}
|
||||
|
||||
class _WallpaperSettingsDialogState extends State<WallpaperSettingsDialog> {
|
||||
WallpaperTarget _selectedTarget = WallpaperTarget.home;
|
||||
bool _useScrollEffect = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AvesDialog(
|
||||
scrollableContent: [
|
||||
if (device.canSetLockScreenWallpaper)
|
||||
...WallpaperTarget.values.map((value) {
|
||||
return SelectionRadioListTile(
|
||||
value: value,
|
||||
title: value.getName(context),
|
||||
needConfirmation: true,
|
||||
getGroupValue: () => _selectedTarget,
|
||||
setGroupValue: (v) => setState(() => _selectedTarget = v),
|
||||
);
|
||||
}),
|
||||
SwitchListTile(
|
||||
value: _useScrollEffect,
|
||||
onChanged: (v) => setState(() => _useScrollEffect = v),
|
||||
title: Text(context.l10n.wallpaperUseScrollEffect),
|
||||
)
|
||||
],
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, Tuple2<WallpaperTarget, bool>(_selectedTarget, _useScrollEffect)),
|
||||
child: Text(context.l10n.applyButtonLabel),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -2,14 +2,13 @@ import 'dart:async';
|
|||
import 'dart:math';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:aves/model/device.dart';
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/entry_images.dart';
|
||||
import 'package:aves/model/wallpaper_target.dart';
|
||||
import 'package:aves/services/wallpaper_service.dart';
|
||||
import 'package:aves/widgets/common/action_mixins/feedback.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/wallpaper_settings_dialog.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/common.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/viewer_buttons.dart';
|
||||
import 'package:aves/widgets/viewer/video/conductor.dart';
|
||||
|
@ -18,6 +17,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter/services.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
class WallpaperButtons extends StatelessWidget with FeedbackMixin {
|
||||
final AvesEntry entry;
|
||||
|
@ -56,19 +56,14 @@ class WallpaperButtons extends StatelessWidget with FeedbackMixin {
|
|||
|
||||
Future<void> _setWallpaper(BuildContext context) async {
|
||||
final l10n = context.l10n;
|
||||
var target = WallpaperTarget.home;
|
||||
if (device.canSetLockScreenWallpaper) {
|
||||
final value = await showDialog<WallpaperTarget>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<WallpaperTarget>(
|
||||
initialValue: WallpaperTarget.home,
|
||||
options: Map.fromEntries(WallpaperTarget.values.map((v) => MapEntry(v, v.getName(context)))),
|
||||
confirmationButtonLabel: l10n.continueButtonLabel,
|
||||
),
|
||||
);
|
||||
if (value == null) return;
|
||||
target = value;
|
||||
}
|
||||
final value = await showDialog<Tuple2<WallpaperTarget, bool>>(
|
||||
context: context,
|
||||
builder: (context) => const WallpaperSettingsDialog(),
|
||||
);
|
||||
if (value == null) return;
|
||||
|
||||
final target = value.item1;
|
||||
final useScrollEffect = value.item2;
|
||||
|
||||
final reportController = StreamController.broadcast();
|
||||
unawaited(showOpReport(
|
||||
|
@ -76,17 +71,15 @@ class WallpaperButtons extends StatelessWidget with FeedbackMixin {
|
|||
opStream: reportController.stream,
|
||||
));
|
||||
|
||||
final viewState = context.read<ViewStateConductor>().getOrCreateController(entry).value;
|
||||
final viewportSize = viewState.viewportSize;
|
||||
final contentSize = viewState.contentSize;
|
||||
final scale = viewState.scale;
|
||||
if (viewportSize == null || contentSize == null || contentSize.isEmpty || scale == null) return;
|
||||
var region = _getVisibleRegion(context);
|
||||
if (region == null) return;
|
||||
|
||||
final center = (contentSize / 2 - viewState.position / scale) as Size;
|
||||
final regionSize = viewportSize / scale;
|
||||
final regionTopLeft = (center - regionSize / 2) as Offset;
|
||||
final region = Rect.fromLTWH(regionTopLeft.dx, regionTopLeft.dy, regionSize.width, regionSize.height);
|
||||
final bytes = await _getBytes(context, scale, region);
|
||||
if (useScrollEffect) {
|
||||
final deltaX = min(region.left, entry.displaySize.width - region.right);
|
||||
region = Rect.fromLTRB(region.left - deltaX, region.top, region.right + deltaX, region.bottom);
|
||||
}
|
||||
|
||||
final bytes = await _getBytes(context, region);
|
||||
|
||||
final success = bytes != null && await WallpaperService.set(bytes, target);
|
||||
unawaited(reportController.close());
|
||||
|
@ -98,9 +91,25 @@ class WallpaperButtons extends StatelessWidget with FeedbackMixin {
|
|||
}
|
||||
}
|
||||
|
||||
Future<Uint8List?> _getBytes(BuildContext context, double scale, Rect displayRegion) async {
|
||||
Rect? _getVisibleRegion(BuildContext context) {
|
||||
final viewState = context.read<ViewStateConductor>().getOrCreateController(entry).value;
|
||||
final viewportSize = viewState.viewportSize;
|
||||
final contentSize = viewState.contentSize;
|
||||
final scale = viewState.scale;
|
||||
if (viewportSize == null || contentSize == null || contentSize.isEmpty || scale == null) return null;
|
||||
|
||||
final center = (contentSize / 2 - viewState.position / scale) as Size;
|
||||
final regionSize = viewportSize / scale;
|
||||
final regionTopLeft = (center - regionSize / 2) as Offset;
|
||||
return Rect.fromLTWH(regionTopLeft.dx, regionTopLeft.dy, regionSize.width, regionSize.height);
|
||||
}
|
||||
|
||||
Future<Uint8List?> _getBytes(BuildContext context, Rect displayRegion) async {
|
||||
final viewState = context.read<ViewStateConductor>().getOrCreateController(entry).value;
|
||||
final scale = viewState.scale;
|
||||
|
||||
final displaySize = entry.displaySize;
|
||||
if (displaySize.isEmpty) return null;
|
||||
if (displaySize.isEmpty || scale == null) return null;
|
||||
|
||||
var storageRegion = Rectangle(
|
||||
displayRegion.left,
|
||||
|
|
|
@ -9,7 +9,8 @@
|
|||
"albumGroupType",
|
||||
"albumMimeTypeMixed",
|
||||
"settingsWidgetOpenPage",
|
||||
"statsTopAlbumsSectionTitle"
|
||||
"statsTopAlbumsSectionTitle",
|
||||
"wallpaperUseScrollEffect"
|
||||
],
|
||||
|
||||
"el": [
|
||||
|
@ -22,7 +23,8 @@
|
|||
"albumGroupType",
|
||||
"albumMimeTypeMixed",
|
||||
"settingsWidgetOpenPage",
|
||||
"statsTopAlbumsSectionTitle"
|
||||
"statsTopAlbumsSectionTitle",
|
||||
"wallpaperUseScrollEffect"
|
||||
],
|
||||
|
||||
"es": [
|
||||
|
@ -51,7 +53,8 @@
|
|||
"settingsConfirmationAfterMoveToBinItems",
|
||||
"settingsWidgetOpenPage",
|
||||
"statsTopAlbumsSectionTitle",
|
||||
"viewerInfoLabelDescription"
|
||||
"viewerInfoLabelDescription",
|
||||
"wallpaperUseScrollEffect"
|
||||
],
|
||||
|
||||
"id": [
|
||||
|
@ -64,7 +67,8 @@
|
|||
"albumGroupType",
|
||||
"albumMimeTypeMixed",
|
||||
"settingsWidgetOpenPage",
|
||||
"statsTopAlbumsSectionTitle"
|
||||
"statsTopAlbumsSectionTitle",
|
||||
"wallpaperUseScrollEffect"
|
||||
],
|
||||
|
||||
"it": [
|
||||
|
@ -77,7 +81,8 @@
|
|||
"albumGroupType",
|
||||
"albumMimeTypeMixed",
|
||||
"settingsWidgetOpenPage",
|
||||
"statsTopAlbumsSectionTitle"
|
||||
"statsTopAlbumsSectionTitle",
|
||||
"wallpaperUseScrollEffect"
|
||||
],
|
||||
|
||||
"ja": [
|
||||
|
@ -107,7 +112,8 @@
|
|||
"settingsViewerGestureSideTapNext",
|
||||
"settingsWidgetOpenPage",
|
||||
"statsTopAlbumsSectionTitle",
|
||||
"viewerInfoLabelDescription"
|
||||
"viewerInfoLabelDescription",
|
||||
"wallpaperUseScrollEffect"
|
||||
],
|
||||
|
||||
"nl": [
|
||||
|
@ -120,7 +126,8 @@
|
|||
"albumGroupType",
|
||||
"albumMimeTypeMixed",
|
||||
"settingsWidgetOpenPage",
|
||||
"statsTopAlbumsSectionTitle"
|
||||
"statsTopAlbumsSectionTitle",
|
||||
"wallpaperUseScrollEffect"
|
||||
],
|
||||
|
||||
"pt": [
|
||||
|
@ -133,7 +140,8 @@
|
|||
"albumGroupType",
|
||||
"albumMimeTypeMixed",
|
||||
"settingsWidgetOpenPage",
|
||||
"statsTopAlbumsSectionTitle"
|
||||
"statsTopAlbumsSectionTitle",
|
||||
"wallpaperUseScrollEffect"
|
||||
],
|
||||
|
||||
"ru": [
|
||||
|
@ -146,7 +154,8 @@
|
|||
"albumGroupType",
|
||||
"albumMimeTypeMixed",
|
||||
"settingsWidgetOpenPage",
|
||||
"statsTopAlbumsSectionTitle"
|
||||
"statsTopAlbumsSectionTitle",
|
||||
"wallpaperUseScrollEffect"
|
||||
],
|
||||
|
||||
"tr": [
|
||||
|
@ -204,7 +213,8 @@
|
|||
"settingsWidgetOpenPage",
|
||||
"statsTopAlbumsSectionTitle",
|
||||
"viewerSetWallpaperButtonLabel",
|
||||
"viewerInfoLabelDescription"
|
||||
"viewerInfoLabelDescription",
|
||||
"wallpaperUseScrollEffect"
|
||||
],
|
||||
|
||||
"zh": [
|
||||
|
@ -217,6 +227,7 @@
|
|||
"albumGroupType",
|
||||
"albumMimeTypeMixed",
|
||||
"settingsWidgetOpenPage",
|
||||
"statsTopAlbumsSectionTitle"
|
||||
"statsTopAlbumsSectionTitle",
|
||||
"wallpaperUseScrollEffect"
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue