Files
feeddeck/app/lib/widgets/deck/deck_layout_small.dart
Rico Berger 6a3b481219 Allow Swiping Gestures to Switch Column (#246)
It is now possible to switch the column in a deck by swiping to the left
/ right.
2025-04-19 15:31:57 +02:00

269 lines
10 KiB
Dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:feeddeck/models/source.dart';
import 'package:feeddeck/repositories/app_repository.dart';
import 'package:feeddeck/repositories/layout_repository.dart';
import 'package:feeddeck/utils/constants.dart';
import 'package:feeddeck/widgets/column/column_layout.dart';
import 'package:feeddeck/widgets/column/create/create_column.dart';
import 'package:feeddeck/widgets/settings/settings.dart';
import 'package:feeddeck/widgets/source/source_icon.dart';
/// The [DeckLayoutSmall] implements the deck screen via tabs for screens
/// smaller then our defined breakpoint. It displayes all columns in a tab bar
/// and when the user clicks on an item in the tab bar it will update the tab
/// view to the corresponding column.
///
/// The tab bar also has two additional items, one to add a new column to the
/// current deck and one to go to the settings screen of the app.
class DeckLayoutSmall extends StatelessWidget {
const DeckLayoutSmall({super.key});
/// [_getInitialIndex] returns the initial index for the
/// [DefaultTabController]. The intial index is saved in the
/// [LayoutRepository] so that we can use it when a user switches between the
/// small and large layout or between decks. To not run into errors we have to
/// check that a column which should be used was not already deleted by a
/// user before returning the index. If a column was deleted we reset the
/// index to 0.
int _getInitialIndex(BuildContext context, int columnsLength) {
final deckLayoutSmallInitialTabIndex =
Provider.of<LayoutRepository>(
context,
listen: false,
).deckLayoutSmallInitialTabIndex;
if (deckLayoutSmallInitialTabIndex >= columnsLength) {
Provider.of<LayoutRepository>(context, listen: false)
.deckLayoutSmallInitialTabIndex = 0;
return 0;
}
return deckLayoutSmallInitialTabIndex;
}
/// [_buildTabs] returns all items for the tab bar. The items are generated
/// by looping thorugh the `columns` defined in the [AppRepository].
///
/// Each item in the tab bar will have an `icon` and an `title`. The icon will
/// be the icon for the first source in the column and the title will be the
/// column name.
List<Tab> _buildTabs(BuildContext context) {
AppRepository app = Provider.of<AppRepository>(context, listen: false);
final List<Tab> widgets = [];
for (var column in app.columns) {
widgets.add(
Tab(
key: ValueKey(column.id),
height: 56,
icon: SourceIcon(
type:
column.sources.isNotEmpty
? column.sources[0].type
: FDSourceType.none,
icon: column.sources.isNotEmpty ? column.sources[0].icon : null,
size: 24,
),
iconMargin: const EdgeInsets.only(bottom: 0),
child: Container(
constraints: const BoxConstraints(minWidth: 54, maxWidth: 54),
child: Text(
Characters(
column.name,
).replaceAll(Characters(''), Characters('\u{200B}')).toString(),
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 10,
overflow: TextOverflow.ellipsis,
),
),
),
),
);
}
return widgets;
}
/// [_buildViews] returns all the view for the tab view. The view for each
/// column is implemented in the [ColumnLayout] widget.
Widget _buildViews(BuildContext context) {
AppRepository app = Provider.of<AppRepository>(context, listen: false);
if (app.columns.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(Constants.spacingMiddle),
child: RichText(
textAlign: TextAlign.center,
text: const TextSpan(
style: TextStyle(color: Constants.onSurface, fontSize: 14.0),
children: [
TextSpan(
text: 'Add you first column by clicking on the plus icon (',
),
WidgetSpan(child: Icon(Icons.add, size: 14.0)),
TextSpan(text: ') in the tab bar on the bottom.'),
],
),
),
),
);
}
final List<Widget> widgets = [];
for (var column in app.columns) {
widgets.add(
ColumnLayout(
key: ValueKey(column.id),
column: column,
openDrawer: null,
),
);
}
/// Return the TabBarView. Since it was requested in
/// https://github.com/feeddeck/feeddeck/issues/228 we removed the
/// `physics: const NeverScrollableScrollPhysics()` property, so that a user
/// can switch between tabs by swiping to the left and to the right.
///
/// After testing this didn't conflicted with the other scroll and swipe
/// gestures of the children, so it should be save to activate. In case this
/// doesn't workout well in the long term, we should re-add the `physics`
/// property to the widget.
return TabBarView(
// physics: const NeverScrollableScrollPhysics(),
children: widgets,
);
}
@override
Widget build(BuildContext context) {
AppRepository app = Provider.of<AppRepository>(context, listen: true);
return DefaultTabController(
key: ValueKey(app.activeDeckId),
initialIndex: _getInitialIndex(context, app.columns.length),
length: app.columns.length,
child: Scaffold(
bottomNavigationBar: SafeArea(
child: Container(
decoration: const BoxDecoration(
border: Border(
top: BorderSide(color: Constants.dividerColor, width: 1),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Theme(
data: Theme.of(context).copyWith(
colorScheme: Theme.of(context).colorScheme.copyWith(
surfaceContainerHighest: Colors.transparent,
),
),
child: TabBar(
isScrollable: true,
tabAlignment: TabAlignment.start,
dividerHeight: 0,
onTap: (int index) {
/// When the user clicks on a tab we update the index in
/// the [LayoutRepository] so that we can use it as
/// initial index when the widget is rebuild (e.g. when
/// a user switches between the large and small layout).
Provider.of<LayoutRepository>(context, listen: false)
.deckLayoutSmallInitialTabIndex = index;
},
tabs: _buildTabs(context),
),
),
),
Row(
children: [
Container(
padding: const EdgeInsets.only(
left: Constants.spacingSmall,
right: Constants.spacingSmall,
),
decoration: const BoxDecoration(
border: Border(
left: BorderSide(color: Constants.dividerColor),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(
Icons.add,
color: Constants.onSurface,
),
onPressed: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
isDismissible: false,
useSafeArea: true,
backgroundColor: Colors.transparent,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(
Constants.spacingMiddle,
),
),
),
clipBehavior: Clip.antiAliasWithSaveLayer,
constraints: const BoxConstraints(
maxWidth: Constants.centeredFormMaxWidth,
),
builder: (BuildContext context) {
return const CreateColumn();
},
);
},
),
],
),
),
Container(
padding: const EdgeInsets.only(
right: Constants.spacingSmall,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(
Icons.settings,
color: Constants.onSurface,
),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder:
(BuildContext context) =>
const Settings(),
),
);
},
),
],
),
),
],
),
],
),
),
),
body: SafeArea(child: _buildViews(context)),
),
);
}
}