Files
feeddeck/app/lib/widgets/deck/deck_layout_large.dart
2025-07-27 15:58:48 +02:00

294 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.
void _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.surface,
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(),
],
),
),
);
}
}