mirror of
https://github.com/feeddeck/feeddeck.git
synced 2026-05-02 12:27:56 -05:00
This commit improves the handling of tabs in the samll deck layout. We are now saving the selected tabs index in the newly added "LayoutRepository" so that we can reuse the selected tab when a user switches between the small and large layout. We can now also set the tab which should be initially selected in the large layout when a user selects a column in the navigation rail. Last but not least we can also reset the initial tab index when a user selects a new deck in the settings, so that we always display the first column instead of the column with the same index as it was selected in the previous deck.
316 lines
11 KiB
Dart
316 lines
11 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import 'package:provider/provider.dart';
|
|
import 'package:scroll_to_index/scroll_to_index.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/general/logo.dart';
|
|
import 'package:feeddeck/widgets/settings/settings.dart';
|
|
import 'package:feeddeck/widgets/source/source_icon.dart';
|
|
|
|
/// The [DeckLayoutLarge] implements the deck screen via a navigation rail for
|
|
/// screens larger then our defined breakpoint. It displays a navigation rail on
|
|
/// the left side of the screen and all columns of the deck.
|
|
class DeckLayoutLarge extends StatefulWidget {
|
|
const DeckLayoutLarge({super.key});
|
|
|
|
@override
|
|
State<DeckLayoutLarge> createState() => _DeckLayoutLargeState();
|
|
}
|
|
|
|
class _DeckLayoutLargeState extends State<DeckLayoutLarge> {
|
|
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
|
final AutoScrollController _scrollController = AutoScrollController();
|
|
Widget _drawer = Container();
|
|
|
|
/// [_openDrawer] opens the provided [widget] in the drawer of the scaffold,
|
|
/// by setting the [_drawer] state first and then opening the drawer.
|
|
_openDrawer(Widget widget) {
|
|
setState(() {
|
|
_drawer = widget;
|
|
});
|
|
_scaffoldKey.currentState?.openDrawer();
|
|
}
|
|
|
|
/// [_buildDestinations] returns a list of destinations for the
|
|
/// [NavigationRail]. The destination are created based on the columns in the
|
|
/// [AppRepository]. Each destination will have the column name as `title` and
|
|
/// the icon of the first source in a column as `icon`.
|
|
///
|
|
/// Since the [NavigationRail] needs at least two destinations, we will fill
|
|
/// the destinations with some invisible destinations, when the number of
|
|
/// columns is less then 2.
|
|
List<NavigationRailDestination> _buildDestinations() {
|
|
AppRepository app = Provider.of<AppRepository>(context, listen: false);
|
|
final List<NavigationRailDestination> widgets = [];
|
|
|
|
for (var column in app.columns) {
|
|
widgets.add(
|
|
NavigationRailDestination(
|
|
padding: const EdgeInsets.only(
|
|
top: Constants.spacingSmall + Constants.spacingExtraSmall,
|
|
),
|
|
icon: SourceIcon(
|
|
type: column.sources.isNotEmpty
|
|
? column.sources[0].type
|
|
: FDSourceType.none,
|
|
icon: column.sources.isNotEmpty ? column.sources[0].icon : null,
|
|
size: 32,
|
|
),
|
|
label: Container(
|
|
padding: const EdgeInsets.only(
|
|
top: Constants.spacingExtraSmall,
|
|
),
|
|
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,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
for (var i = widgets.length; i < 2; i++) {
|
|
widgets.add(
|
|
const NavigationRailDestination(
|
|
icon: Icon(
|
|
Icons.circle,
|
|
color: Colors.transparent,
|
|
),
|
|
label: Text(''),
|
|
),
|
|
);
|
|
}
|
|
|
|
return widgets;
|
|
}
|
|
|
|
/// [_buildColumns] builds the columns view which is displayed next to the
|
|
/// navigation rail. Each column is implemented via the [ColumnLayout] widget
|
|
/// which we are already using for smaller screens, but we are adding an
|
|
/// additional border to the columns for a better seperation.
|
|
Widget _buildColumns() {
|
|
AppRepository app = Provider.of<AppRepository>(context, listen: false);
|
|
final List<Widget> widgets = [];
|
|
|
|
if (app.columns.isEmpty) {
|
|
return Expanded(
|
|
child: 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 sidebar on the left side.',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
for (var i = 0; i < app.columns.length; i++) {
|
|
widgets.add(
|
|
AutoScrollTag(
|
|
key: ValueKey(app.columns[i].id),
|
|
controller: _scrollController,
|
|
index: i,
|
|
child: Container(
|
|
width: Constants.columnWidth,
|
|
decoration: const BoxDecoration(
|
|
border: Border(
|
|
right: BorderSide(
|
|
color: Colors.black,
|
|
width: Constants.columnSpacing,
|
|
),
|
|
),
|
|
),
|
|
child: ColumnLayout(
|
|
key: ValueKey(app.columns[i].id),
|
|
column: app.columns[i],
|
|
openDrawer: _openDrawer,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
return Expanded(
|
|
child: ListView(
|
|
padding: EdgeInsets.zero,
|
|
scrollDirection: Axis.horizontal,
|
|
controller: _scrollController,
|
|
children: widgets,
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_scrollController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
/// We have to subscribe to all changes in the [AppRepository] so that we
|
|
/// can retrigger the [_buildDestinations] and [_buildColumns] functions on
|
|
/// a change.
|
|
Provider.of<AppRepository>(context, listen: true);
|
|
|
|
return Scaffold(
|
|
key: _scaffoldKey,
|
|
|
|
/// The drawer is used to display the [CreateColumn] widget, so that a
|
|
/// user can add a new column without leaving the screen.
|
|
drawer: ClipRRect(
|
|
borderRadius: const BorderRadius.only(
|
|
topRight: Radius.circular(Constants.spacingMiddle),
|
|
bottomRight: Radius.circular(Constants.spacingMiddle),
|
|
),
|
|
child: Drawer(
|
|
width: Constants.columnWidth,
|
|
child: _drawer,
|
|
),
|
|
),
|
|
body: SafeArea(
|
|
child: Row(
|
|
children: [
|
|
SingleChildScrollView(
|
|
child: ConstrainedBox(
|
|
constraints: BoxConstraints(
|
|
minHeight: MediaQuery.of(context).size.height -
|
|
MediaQuery.of(context).padding.top -
|
|
MediaQuery.of(context).padding.bottom,
|
|
),
|
|
child: IntrinsicHeight(
|
|
child: Theme(
|
|
data: Theme.of(context).copyWith(
|
|
highlightColor: Colors.transparent,
|
|
splashFactory: NoSplash.splashFactory,
|
|
),
|
|
child: NavigationRail(
|
|
backgroundColor: Constants.background,
|
|
selectedIndex: null,
|
|
|
|
/// When a user selects a destination in the navigation
|
|
/// rail we scroll to the corresponding column by using
|
|
/// the `scroll_to_index` package.
|
|
onDestinationSelected: (int index) {
|
|
/// Before we scroll to the corresponding column, we
|
|
/// also update the [deckLayoutSmallInitialTabIndex] in
|
|
/// the [LayoutRepository] so that the correct tab is
|
|
/// also selected when a user switches to the small
|
|
/// layout.
|
|
Provider.of<LayoutRepository>(
|
|
context,
|
|
listen: false,
|
|
).deckLayoutSmallInitialTabIndex = index;
|
|
|
|
_scrollController.scrollToIndex(
|
|
index,
|
|
preferPosition: AutoScrollPosition.end,
|
|
);
|
|
},
|
|
labelType: NavigationRailLabelType.all,
|
|
leading: Container(
|
|
padding: const EdgeInsets.only(
|
|
top: Constants.spacingSmall,
|
|
bottom: Constants.spacingSmall,
|
|
),
|
|
child: const Logo(size: 32.0),
|
|
),
|
|
|
|
/// We add two additional items to the navigation rail via
|
|
/// the trailing property. These items are used to allow a
|
|
/// user to create a new column and to go to the settings
|
|
/// of the app.
|
|
trailing: Expanded(
|
|
child: Align(
|
|
alignment: Alignment.bottomCenter,
|
|
child: Wrap(
|
|
direction: Axis.vertical,
|
|
children: [
|
|
IconButton(
|
|
icon: const Icon(Icons.add),
|
|
onPressed: () {
|
|
_openDrawer(const CreateColumn());
|
|
},
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.settings),
|
|
onPressed: () {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (BuildContext context) =>
|
|
const Settings(),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
const SizedBox(
|
|
height: Constants.spacingMiddle,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
destinations: _buildDestinations(),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const VerticalDivider(
|
|
color: Constants.dividerColor,
|
|
width: 1,
|
|
thickness: 1,
|
|
),
|
|
SizedBox(
|
|
width: Constants.columnSpacing,
|
|
height: MediaQuery.of(context).size.height,
|
|
child: const DecoratedBox(
|
|
decoration: BoxDecoration(color: Colors.black),
|
|
),
|
|
),
|
|
_buildColumns(),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|