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 createState() => _DeckLayoutLargeState(); } class _DeckLayoutLargeState extends State { final GlobalKey _scaffoldKey = GlobalKey(); 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 _buildDestinations() { AppRepository app = Provider.of(context, listen: false); final List 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(context, listen: false); final List 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(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( 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(), ], ), ), ); } }