mirror of
https://github.com/feeddeck/feeddeck.git
synced 2026-04-28 18:38:34 -05:00
294 lines
11 KiB
Dart
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(),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|