make fab moveable
This commit is contained in:
@@ -44,6 +44,7 @@ import 'package:weblibre/features/geckoview/features/browser/presentation/contro
|
|||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_fab.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_fab.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/draggable_fab.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/sheets/view_tab.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/sheets/view_tab.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_grid_view.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_grid_view.dart';
|
||||||
@@ -182,7 +183,8 @@ class _TabBar extends HookConsumerWidget {
|
|||||||
useOnStreamChange(
|
useOnStreamChange(
|
||||||
pointerMoveEvents,
|
pointerMoveEvents,
|
||||||
onData: (event) {
|
onData: (event) {
|
||||||
if (!ref.read(generalSettingsWithDefaultsProvider).autoHideTabBar) return;
|
if (!ref.read(generalSettingsWithDefaultsProvider).autoHideTabBar)
|
||||||
|
return;
|
||||||
final diff = event.dy;
|
final diff = event.dy;
|
||||||
if (diff < 0) {
|
if (diff < 0) {
|
||||||
if (diffAcc.value > 0) {
|
if (diffAcc.value > 0) {
|
||||||
@@ -333,8 +335,8 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
// This ensures top-of-page content is always accessible
|
// This ensures top-of-page content is always accessible
|
||||||
final browserTopOffset =
|
final browserTopOffset =
|
||||||
(tabBarPosition == TabBarPosition.top && topToolbarVisible)
|
(tabBarPosition == TabBarPosition.top && topToolbarVisible)
|
||||||
? topAppBarTotalHeight
|
? topAppBarTotalHeight
|
||||||
: 0.0;
|
: 0.0;
|
||||||
|
|
||||||
// Theme with dynamic snackbar margin to position above bottom toolbar
|
// Theme with dynamic snackbar margin to position above bottom toolbar
|
||||||
final themeData = Theme.of(context).copyWith(
|
final themeData = Theme.of(context).copyWith(
|
||||||
@@ -443,14 +445,11 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Layer 4: FAB (animates position with toolbar visibility)
|
// Layer 4: FAB (draggable via long press)
|
||||||
AnimatedPositioned(
|
DraggableFab(
|
||||||
duration: _AnimatedToolbar._kAnimationDuration,
|
bottomToolbarVisible: bottomToolbarVisible,
|
||||||
curve: Curves.easeInOutQuart,
|
bottomAppBarHeight: bottomAppBarTotalHeight,
|
||||||
right: 16,
|
bottomSafeArea: bottomSafeArea,
|
||||||
bottom: bottomToolbarVisible
|
|
||||||
? bottomAppBarTotalHeight + 16
|
|
||||||
: 16 + bottomSafeArea,
|
|
||||||
child: const BrowserFab(),
|
child: const BrowserFab(),
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -460,9 +459,7 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
curve: Curves.easeInOutQuart,
|
curve: Curves.easeInOutQuart,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: bottomToolbarVisible
|
bottom: bottomToolbarVisible ? bottomAppBarTotalHeight : 0,
|
||||||
? bottomAppBarTotalHeight
|
|
||||||
: 0,
|
|
||||||
child: Consumer(
|
child: Consumer(
|
||||||
builder: (context, ref, child) {
|
builder: (context, ref, child) {
|
||||||
final value = ref.watch(
|
final value = ref.watch(
|
||||||
|
|||||||
+127
@@ -0,0 +1,127 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2024-2025 Fabian Freund.
|
||||||
|
*
|
||||||
|
* This file is part of WebLibre
|
||||||
|
* (see https://weblibre.eu).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
|
class DraggableFab extends HookConsumerWidget {
|
||||||
|
final Widget child;
|
||||||
|
final double fabSize;
|
||||||
|
final bool bottomToolbarVisible;
|
||||||
|
final double bottomAppBarHeight;
|
||||||
|
final double bottomSafeArea;
|
||||||
|
|
||||||
|
const DraggableFab({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
this.fabSize = 56.0,
|
||||||
|
required this.bottomToolbarVisible,
|
||||||
|
required this.bottomAppBarHeight,
|
||||||
|
required this.bottomSafeArea,
|
||||||
|
});
|
||||||
|
|
||||||
|
static const _edgePadding = 16.0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final mediaQuery = MediaQuery.of(context);
|
||||||
|
final screenSize = mediaQuery.size;
|
||||||
|
final padding = mediaQuery.padding;
|
||||||
|
|
||||||
|
final isDragging = useState(false);
|
||||||
|
final customOffset = useState<Offset?>(null);
|
||||||
|
final dragStartOffset = useRef<Offset?>(null);
|
||||||
|
final dragStartPosition = useRef<Offset?>(null);
|
||||||
|
|
||||||
|
// Calculate default position (bottom-right, respecting toolbar)
|
||||||
|
final defaultBottom = bottomToolbarVisible
|
||||||
|
? bottomAppBarHeight + _edgePadding
|
||||||
|
: _edgePadding + bottomSafeArea;
|
||||||
|
const defaultRight = _edgePadding;
|
||||||
|
final defaultLeft = screenSize.width - fabSize - defaultRight;
|
||||||
|
final defaultTop = screenSize.height - fabSize - defaultBottom;
|
||||||
|
|
||||||
|
// Current position: custom if set, otherwise default
|
||||||
|
final currentLeft = customOffset.value?.dx ?? defaultLeft;
|
||||||
|
final currentTop = customOffset.value?.dy ?? defaultTop;
|
||||||
|
|
||||||
|
return Positioned(
|
||||||
|
left: currentLeft,
|
||||||
|
top: currentTop,
|
||||||
|
child: GestureDetector(
|
||||||
|
onLongPressStart: (details) {
|
||||||
|
isDragging.value = true;
|
||||||
|
unawaited(HapticFeedback.mediumImpact());
|
||||||
|
dragStartOffset.value = Offset(currentLeft, currentTop);
|
||||||
|
dragStartPosition.value = details.globalPosition;
|
||||||
|
},
|
||||||
|
onLongPressMoveUpdate: (details) {
|
||||||
|
if (!isDragging.value ||
|
||||||
|
dragStartOffset.value == null ||
|
||||||
|
dragStartPosition.value == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final delta = details.globalPosition - dragStartPosition.value!;
|
||||||
|
final newOffset = dragStartOffset.value! + delta;
|
||||||
|
|
||||||
|
// Clamp to screen bounds
|
||||||
|
customOffset.value = _clampToBounds(
|
||||||
|
newOffset,
|
||||||
|
screenSize: screenSize,
|
||||||
|
padding: padding,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onLongPressEnd: (details) {
|
||||||
|
isDragging.value = false;
|
||||||
|
dragStartOffset.value = null;
|
||||||
|
dragStartPosition.value = null;
|
||||||
|
},
|
||||||
|
child: AnimatedScale(
|
||||||
|
duration: const Duration(milliseconds: 150),
|
||||||
|
scale: isDragging.value ? 1.1 : 1.0,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Offset _clampToBounds(
|
||||||
|
Offset offset, {
|
||||||
|
required Size screenSize,
|
||||||
|
required EdgeInsets padding,
|
||||||
|
}) {
|
||||||
|
const minEdgePadding = 8.0;
|
||||||
|
|
||||||
|
return Offset(
|
||||||
|
offset.dx.clamp(
|
||||||
|
padding.left + minEdgePadding,
|
||||||
|
screenSize.width - fabSize - padding.right - minEdgePadding,
|
||||||
|
),
|
||||||
|
offset.dy.clamp(
|
||||||
|
padding.top + minEdgePadding,
|
||||||
|
screenSize.height - fabSize - padding.bottom - minEdgePadding,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user