add fab insets avoid overlapping

This commit is contained in:
Fabian Freund
2026-02-22 17:30:52 +01:00
parent b14638e7dd
commit 5230d3cb6b
12 changed files with 340 additions and 229 deletions
@@ -29,6 +29,7 @@ import 'package:weblibre/features/bangs/domain/providers/bangs.dart';
import 'package:weblibre/features/bangs/domain/repositories/data.dart';
import 'package:weblibre/features/bangs/presentation/widgets/bang_details.dart';
import 'package:weblibre/presentation/widgets/failure_widget.dart';
import 'package:weblibre/presentation/widgets/floating_action_button_inset.dart';
class UserBangs extends HookConsumerWidget {
static const _userGroupFilter = [BangGroup.user];
@@ -45,6 +46,9 @@ class UserBangs extends HookConsumerWidget {
skipLoadingOnReload: true,
data: (bangs) {
return ListView.builder(
padding: EdgeInsets.only(
bottom: floatingActionButtonBottomInset(context),
),
itemCount: bangs.length,
itemBuilder: (context, index) {
final bang = bangs[index];
@@ -33,6 +33,7 @@ import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_preview.dart';
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
import 'package:weblibre/presentation/widgets/floating_action_button_inset.dart';
import 'package:weblibre/utils/ui_helper.dart';
class TabTreeDialog extends HookConsumerWidget {
@@ -108,13 +109,19 @@ class TabTreeDialog extends HookConsumerWidget {
extendBodyBehindAppBar: true,
body: Skeletonizer(
enabled: !tabs.hasValue || tabs.value?.isEmpty == true,
child: Padding(
padding: EdgeInsets.only(
bottom: floatingActionButtonBottomInset(context),
),
child: Skeleton.replace(
replacement: const Bone.square(),
child: GraphView.builder(
graph: graph,
algorithm: graphAlgo,
controller: graphViewController,
initialNode: selectedTabId.mapNotNull((tabId) => ValueKey(tabId)),
initialNode: selectedTabId.mapNotNull(
(tabId) => ValueKey(tabId),
),
paint: Paint()
..color = Theme.of(context).colorScheme.outline
..strokeWidth = 1
@@ -153,6 +160,7 @@ class TabTreeDialog extends HookConsumerWidget {
),
),
),
),
);
}
}
@@ -28,7 +28,7 @@ import 'package:weblibre/features/geckoview/features/browser/presentation/widget
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_tree_view.dart';
import 'package:weblibre/features/sync/domain/repositories/sync.dart';
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
import 'package:weblibre/presentation/hooks/scroll_visibility.dart';
import 'package:weblibre/presentation/widgets/floating_action_button_inset.dart';
class TabViewScreen extends HookConsumerWidget {
const TabViewScreen({super.key});
@@ -50,12 +50,17 @@ class TabViewScreen extends HookConsumerWidget {
final scrollController = useScrollController(keys: [tabsReorderable]);
// Track FAB visibility based on scroll direction
final isFabVisible = useScrollVisibility(scrollController);
return Dialog.fullscreen(
child: Scaffold(
body: SafeArea(
child: Builder(
builder: (safeAreaContext) {
return Padding(
padding: EdgeInsets.only(
bottom: isSyncedScope
? 0
: floatingActionButtonBottomInset(safeAreaContext),
),
child: switch (effectiveTabsViewMode) {
TabsViewMode.list => ViewTabListWidget(
key: ValueKey(tabsReorderable),
@@ -83,17 +88,13 @@ class TabViewScreen extends HookConsumerWidget {
},
),
},
);
},
),
),
floatingActionButton: isSyncedScope
? null
: AnimatedSlide(
duration: const Duration(milliseconds: 200),
offset: isFabVisible.value ? Offset.zero : const Offset(0, 2),
curve: Curves.easeInOut,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 200),
opacity: isFabVisible.value ? 1.0 : 0.0,
child: FloatingActionButton(
: FloatingActionButton(
onPressed: () async {
final settings = ref.read(
generalSettingsWithDefaultsProvider,
@@ -112,8 +113,6 @@ class TabViewScreen extends HookConsumerWidget {
child: const Icon(Icons.add),
),
),
),
),
);
}
}
@@ -47,6 +47,7 @@ import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/co
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab_search.dart';
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
import 'package:weblibre/presentation/widgets/floating_action_button_inset.dart';
class _TabDraggable extends HookConsumerWidget {
final TabEntity entity;
@@ -538,12 +539,22 @@ class ViewTabGridWidget extends HookConsumerWidget {
),
),
],
body: _TabGridView(
body: Padding(
padding: EdgeInsets.only(
bottom: showNewTabFab
? floatingActionButtonBottomInset(
context,
fabHeight: smallFloatingActionButtonHeight,
)
: 0,
),
child: _TabGridView(
scrollController: scrollController,
tabsReorderable: tabsReorderable,
onClose: onClose,
),
),
),
if (showNewTabFab)
AnimatedSlide(
duration: const Duration(milliseconds: 200),
@@ -46,6 +46,7 @@ import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/ta
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab_search.dart';
import 'package:weblibre/features/sync/domain/repositories/sync.dart';
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
import 'package:weblibre/presentation/widgets/floating_action_button_inset.dart';
class _TabDraggable extends HookConsumerWidget {
final TabEntity entity;
@@ -567,12 +568,22 @@ class ViewTabListWidget extends HookConsumerWidget {
),
),
],
body: _TabListView(
body: Padding(
padding: EdgeInsets.only(
bottom: (showNewTabFab && !isSyncedScope)
? floatingActionButtonBottomInset(
context,
fabHeight: smallFloatingActionButtonHeight,
)
: 0,
),
child: _TabListView(
scrollController: scrollController,
tabsReorderable: tabsReorderable,
onClose: onClose,
),
),
),
if (showNewTabFab && !isSyncedScope)
AnimatedSlide(
duration: const Duration(milliseconds: 200),
@@ -38,6 +38,7 @@ import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selec
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab_search.dart';
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
import 'package:weblibre/presentation/widgets/floating_action_button_inset.dart';
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
class _TabTreePreview extends HookConsumerWidget {
@@ -280,7 +281,14 @@ class ViewTabTreesWidget extends HookConsumerWidget {
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: GridView.builder(
controller: scrollController,
padding: EdgeInsets.zero,
padding: EdgeInsets.only(
bottom: showNewTabFab
? floatingActionButtonBottomInset(
context,
fabHeight: smallFloatingActionButtonHeight,
)
: 0,
),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
//Sync values for itemHeight calculation _calculateItemHeight
childAspectRatio: 0.75,
@@ -36,6 +36,7 @@ import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/co
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/gecko_inference.dart';
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
import 'package:weblibre/presentation/widgets/failure_widget.dart';
import 'package:weblibre/presentation/widgets/floating_action_button_inset.dart';
import 'package:weblibre/presentation/widgets/selectable_chips.dart';
class ContainerDraftSuggestionsScreen extends HookConsumerWidget {
@@ -55,7 +56,9 @@ class ContainerDraftSuggestionsScreen extends HookConsumerWidget {
return Scaffold(
appBar: AppBar(title: const Text('Draft Containers')),
body: SafeArea(
child: Padding(
child: Builder(
builder: (safeAreaContext) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: suggestionsAsync.when(
skipLoadingOnReload: true,
@@ -118,7 +121,8 @@ class ContainerDraftSuggestionsScreen extends HookConsumerWidget {
);
},
),
itemBadgeCount: (container) => container.tabIds.length,
itemBadgeCount: (container) =>
container.tabIds.length,
availableItems: suggestions!,
selectedItem: selectedContainer.value,
onSelected: (item) {
@@ -131,6 +135,13 @@ class ContainerDraftSuggestionsScreen extends HookConsumerWidget {
if (selectedContainer.value != null)
Expanded(
child: GridView.builder(
padding: EdgeInsets.only(
bottom: (selectedTabs?.isNotEmpty ?? false)
? floatingActionButtonBottomInset(
safeAreaContext,
)
: 0,
),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
//Sync values for itemHeight calculation _calculateItemHeight
childAspectRatio: 0.75,
@@ -140,7 +151,8 @@ class ContainerDraftSuggestionsScreen extends HookConsumerWidget {
),
itemCount: selectedContainer.value!.tabIds.length,
itemBuilder: (context, index) {
final tabId = selectedContainer.value!.tabIds[index];
final tabId =
selectedContainer.value!.tabIds[index];
final equatable = selectedContainer.value!;
return (selectedContainerTabs.value[equatable]
@@ -189,6 +201,8 @@ class ContainerDraftSuggestionsScreen extends HookConsumerWidget {
},
loading: () => const Center(child: CircularProgressIndicator()),
),
);
},
),
),
floatingActionButton: (selectedTabs?.isNotEmpty ?? false)
@@ -33,6 +33,7 @@ import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/c
import 'package:weblibre/features/tor/presentation/controllers/start_tor_proxy.dart';
import 'package:weblibre/features/tor/presentation/widgets/tor_dialog.dart';
import 'package:weblibre/presentation/widgets/failure_widget.dart';
import 'package:weblibre/presentation/widgets/floating_action_button_inset.dart';
class ContainerListScreen extends HookConsumerWidget {
const ContainerListScreen({super.key});
@@ -53,6 +54,9 @@ class ContainerListScreen extends HookConsumerWidget {
builder: (context, controller) {
return ListView.builder(
controller: controller,
padding: EdgeInsets.only(
bottom: floatingActionButtonBottomInset(context),
),
itemCount: containers.length,
itemBuilder: (context, index) {
final container = containers[index];
@@ -164,6 +168,9 @@ class ContainerListScreen extends HookConsumerWidget {
),
),
loading: () => ListView.builder(
padding: EdgeInsets.only(
bottom: floatingActionButtonBottomInset(context),
),
itemCount: 3,
itemBuilder: (context, index) => ContainerListTile(
ContainerData(id: 'null', color: Colors.transparent),
@@ -31,6 +31,7 @@ import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart'
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/container_list_tile.dart';
import 'package:weblibre/presentation/widgets/failure_widget.dart';
import 'package:weblibre/presentation/widgets/floating_action_button_inset.dart';
class ContainerSelectionScreen extends HookConsumerWidget {
const ContainerSelectionScreen({super.key});
@@ -49,6 +50,9 @@ class ContainerSelectionScreen extends HookConsumerWidget {
builder: (context, controller) {
return ListView.builder(
controller: controller,
padding: EdgeInsets.only(
bottom: floatingActionButtonBottomInset(context),
),
itemCount: containers.length,
itemBuilder: (context, index) {
final container = containers[index];
@@ -71,6 +75,9 @@ class ContainerSelectionScreen extends HookConsumerWidget {
),
),
loading: () => ListView.builder(
padding: EdgeInsets.only(
bottom: floatingActionButtonBottomInset(context),
),
itemCount: 3,
itemBuilder: (context, index) => ContainerListTile(
ContainerData(id: Namespace.nil.value, color: Colors.transparent),
@@ -26,6 +26,7 @@ import 'package:weblibre/core/filesystem.dart';
import 'package:weblibre/core/routing/routes.dart';
import 'package:weblibre/features/user/domain/repositories/profile.dart';
import 'package:weblibre/presentation/widgets/failure_widget.dart';
import 'package:weblibre/presentation/widgets/floating_action_button_inset.dart';
class ProfileListScreen extends HookConsumerWidget {
const ProfileListScreen({super.key});
@@ -49,6 +50,9 @@ class ProfileListScreen extends HookConsumerWidget {
body: usersAsync.when(
skipLoadingOnReload: true,
data: (profiles) => ListView.builder(
padding: EdgeInsets.only(
bottom: floatingActionButtonBottomInset(context),
),
itemCount: profiles.length,
itemBuilder: (context, index) {
final profile = profiles[index];
@@ -26,6 +26,7 @@ import 'package:weblibre/features/web_feed/domain/providers.dart';
import 'package:weblibre/features/web_feed/presentation/controllers/fetch_articles.dart';
import 'package:weblibre/features/web_feed/presentation/widgets/feed_card.dart';
import 'package:weblibre/presentation/widgets/failure_widget.dart';
import 'package:weblibre/presentation/widgets/floating_action_button_inset.dart';
class FeedListScreen extends HookConsumerWidget {
const FeedListScreen({super.key});
@@ -76,6 +77,9 @@ class FeedListScreen extends HookConsumerWidget {
.fetchAllArticles();
},
child: ListView.builder(
padding: EdgeInsets.only(
bottom: floatingActionButtonBottomInset(context),
),
itemCount: feeds.length,
itemBuilder: (context, i) {
return FeedCard(feed: feeds[i]);
@@ -0,0 +1,34 @@
/*
* Copyright (c) 2024-2026 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 'package:flutter/material.dart';
const standardFloatingActionButtonHeight = kMinInteractiveDimension;
const smallFloatingActionButtonHeight = 40.0;
double floatingActionButtonBottomInset(
BuildContext context, {
double fabHeight = standardFloatingActionButtonHeight,
double extraPadding = 8.0,
}) {
return fabHeight +
kFloatingActionButtonMargin +
MediaQuery.paddingOf(context).bottom +
extraPadding;
}