Browse Source

Merge pull request #387 from Leptopoda/feature/adaptive-layout

adaptive layout; rework drawer
pull/389/head
Nikolas Rimikis 2 years ago committed by GitHub
parent
commit
c192ae834e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      packages/neon/neon/lib/neon.dart
  2. 314
      packages/neon/neon/lib/src/pages/home.dart
  3. 2
      packages/neon/neon/lib/src/pages/settings.dart
  4. 31
      packages/neon/neon/lib/src/utils/app_implementation.dart
  5. 2
      packages/neon/neon/lib/src/utils/global_options.dart
  6. 1
      packages/neon/neon/lib/src/widgets/app_implementation_icon.dart
  7. 210
      packages/neon/neon/lib/src/widgets/drawer.dart
  8. 137
      packages/neon/neon/lib/src/widgets/drawer_destination.dart
  9. 1
      packages/neon/neon/pubspec.yaml
  10. 3
      packages/neon/neon_files/lib/neon_files.dart
  11. 2
      packages/neon/neon_news/lib/neon_news.dart
  12. 3
      packages/neon/neon_notes/lib/neon_notes.dart
  13. 3
      packages/neon/neon_notifications/lib/neon_notifications.dart
  14. 1
      packages/neon/neon_notifications/lib/pages/main.dart

2
packages/neon/neon/lib/neon.dart

@ -25,6 +25,8 @@ import 'package:neon/src/blocs/blocs.dart';
import 'package:neon/src/models/account.dart';
import 'package:neon/src/models/push_notification.dart';
import 'package:neon/src/router.dart';
import 'package:neon/src/widgets/drawer.dart';
import 'package:neon/src/widgets/drawer_destination.dart';
import 'package:nextcloud/nextcloud.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path/path.dart' as p;

314
packages/neon/neon/lib/src/pages/home.dart

@ -187,235 +187,11 @@ class _HomePageState extends State<HomePage> {
return const Scaffold();
}
final isQuickBar = navigationMode == NavigationMode.quickBar;
final drawer = Builder(
builder: (final context) => Drawer(
width: isQuickBar ? kQuickBarWidth : null,
child: Container(
padding: isQuickBar ? const EdgeInsets.all(5) : null,
child: Column(
children: [
Expanded(
child: Scrollbar(
controller: drawerScrollController,
interactive: true,
child: ListView(
controller: drawerScrollController,
// Needed for the drawer header to also render in the statusbar
padding: EdgeInsets.zero,
children: [
Builder(
builder: (final context) {
if (accountsSnapshot.hasData) {
if (isQuickBar) {
return Column(
children: [
if (accounts.length != 1) ...[
for (final account in accounts) ...[
Container(
margin: const EdgeInsets.symmetric(
vertical: 5,
),
child: IconButton(
onPressed: () {
_accountsBloc.setActiveAccount(account);
},
tooltip: account.client.humanReadableID,
icon: IntrinsicHeight(
child: NeonUserAvatar(
account: account,
),
),
),
),
],
Container(
margin: const EdgeInsets.only(
top: 10,
),
child: Divider(
height: 5,
color: Theme.of(context).appBarTheme.foregroundColor,
),
),
],
],
);
}
return DrawerHeader(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (capabilities.data != null) ...[
if (capabilities.data!.capabilities.theming?.name != null) ...[
Text(
capabilities.data!.capabilities.theming!.name!,
style: DefaultTextStyle.of(context).style.copyWith(
color: Theme.of(context).appBarTheme.foregroundColor,
),
),
],
if (capabilities.data!.capabilities.theming?.logo != null) ...[
Flexible(
child: NeonCachedUrlImage(
url: capabilities.data!.capabilities.theming!.logo!,
),
),
],
] else ...[
NeonException(
capabilities.error,
onRetry: _capabilitiesBloc.refresh,
),
NeonLinearProgressIndicator(
visible: capabilities.loading,
),
],
if (accounts.length != 1) ...[
DropdownButtonHideUnderline(
child: DropdownButton<String>(
isExpanded: true,
dropdownColor: Theme.of(context).colorScheme.primary,
iconEnabledColor: Theme.of(context).colorScheme.onBackground,
value: _account.id,
items: accounts
.map<DropdownMenuItem<String>>(
(final account) => DropdownMenuItem<String>(
value: account.id,
child: NeonAccountTile(
account: account,
dense: true,
textColor:
Theme.of(context).appBarTheme.foregroundColor,
),
),
)
.toList(),
onChanged: (final id) {
if (id != null) {
_accountsBloc.setActiveAccount(accounts.find(id)!);
}
},
),
),
],
],
),
);
}
return Container();
},
),
NeonException(
appImplementations.error,
onlyIcon: isQuickBar,
onRetry: _appsBloc.refresh,
),
NeonLinearProgressIndicator(
visible: appImplementations.loading,
),
if (appImplementations.data != null) ...[
for (final appImplementation in appImplementations.data!) ...[
StreamBuilder<int>(
stream: appImplementation.getUnreadCounter(_appsBloc) ??
BehaviorSubject<int>.seeded(0),
builder: (final context, final unreadCounterSnapshot) {
final unreadCount = unreadCounterSnapshot.data ?? 0;
if (isQuickBar) {
return IconButton(
onPressed: () async {
await _appsBloc.setActiveApp(appImplementation.id);
},
tooltip: appImplementation.name(context),
icon: NeonAppImplementationIcon(
appImplementation: appImplementation,
unreadCount: unreadCount,
color: Theme.of(context).colorScheme.primary,
),
);
}
return ListTile(
key: Key('app-${appImplementation.id}'),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(appImplementation.name(context)),
if (unreadCount > 0) ...[
Text(
unreadCount.toString(),
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
],
],
),
leading: appImplementation.buildIcon(context),
minLeadingWidth: 0,
onTap: () async {
await _appsBloc.setActiveApp(appImplementation.id);
final drawerAlwaysVisible = navigationMode == NavigationMode.drawerAlwaysVisible;
if (!mounted) {
return;
}
Scaffold.maybeOf(context)?.closeDrawer();
},
);
},
),
],
],
],
),
),
),
if (isQuickBar) ...[
IconButton(
onPressed: () => const SettingsRoute().go(context),
tooltip: AppLocalizations.of(context).settings,
icon: Icon(
Icons.settings,
color: Theme.of(context).appBarTheme.foregroundColor,
),
),
] else ...[
ListTile(
key: const Key('settings'),
title: Text(AppLocalizations.of(context).settings),
leading: const Icon(Icons.settings),
minLeadingWidth: 0,
onTap: () async {
Scaffold.maybeOf(context)?.closeDrawer();
const SettingsRoute().go(context);
},
),
],
],
),
),
),
);
const drawer = NeonDrawer();
final appBar = AppBar(
scrolledUnderElevation: navigationMode != NavigationMode.drawer ? 0 : null,
automaticallyImplyLeading: navigationMode == NavigationMode.drawer,
leadingWidth: isQuickBar ? kQuickBarWidth : null,
leading: isQuickBar
? Container(
padding: const EdgeInsets.all(5),
child: capabilities.data?.capabilities.theming?.logo != null
? NeonCachedUrlImage(
url: capabilities.data!.capabilities.theming!.logo!,
svgColor: Theme.of(context).iconTheme.color,
)
: null,
)
: null,
automaticallyImplyLeading: !drawerAlwaysVisible,
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -461,7 +237,8 @@ class _HomePageState extends State<HomePage> {
actions: [
if (notificationsAppImplementation.data != null) ...[
StreamBuilder<int>(
stream: notificationsAppImplementation.data!.getUnreadCounter(_appsBloc),
stream: notificationsAppImplementation.data!
.getUnreadCounter(notificationsAppImplementation.data!.getBloc(account)),
builder: (final context, final unreadCounterSnapshot) {
final unreadCount = unreadCounterSnapshot.data ?? 0;
return IconButton(
@ -500,44 +277,52 @@ class _HomePageState extends State<HomePage> {
);
Widget body = Builder(
builder: (final context) => Row(
builder: (final context) => Column(
children: [
if (navigationMode == NavigationMode.quickBar) ...[
drawer,
],
Expanded(
child: Column(
children: [
if (appImplementations.data != null) ...[
if (appImplementations.data!.isEmpty) ...[
Expanded(
child: Center(
child: Text(
AppLocalizations.of(context).errorNoCompatibleNextcloudAppsFound,
textAlign: TextAlign.center,
),
),
),
] else ...[
if (activeAppIDSnapshot.hasData) ...[
Expanded(
child: appImplementations.data!.find(activeAppIDSnapshot.data!)!.page,
),
],
],
],
if (appImplementations.data != null) ...[
if (appImplementations.data!.isEmpty) ...[
Expanded(
child: Center(
child: Text(
AppLocalizations.of(context).errorNoCompatibleNextcloudAppsFound,
textAlign: TextAlign.center,
),
),
),
] else ...[
if (activeAppIDSnapshot.hasData) ...[
Expanded(
child: appImplementations.data!.find(activeAppIDSnapshot.data!)!.page,
),
],
),
),
],
],
],
),
);
body = MultiProvider(
providers: _appsBloc.appBlocProviders,
child: body,
child: Scaffold(
key: _scaffoldKey,
resizeToAvoidBottomInset: false,
drawer: !drawerAlwaysVisible ? drawer : null,
appBar: appBar,
body: body,
),
);
if (drawerAlwaysVisible) {
body = Row(
children: [
drawer,
Expanded(
child: body,
),
],
);
}
return WillPopScope(
onWillPop: () async {
if (_scaffoldKey.currentState!.isDrawerOpen) {
@ -548,22 +333,7 @@ class _HomePageState extends State<HomePage> {
_scaffoldKey.currentState!.openDrawer();
return false;
},
child: Row(
children: [
if (navigationMode == NavigationMode.drawerAlwaysVisible) ...[
drawer,
],
Expanded(
child: Scaffold(
key: _scaffoldKey,
resizeToAvoidBottomInset: false,
drawer: navigationMode == NavigationMode.drawer ? drawer : null,
appBar: appBar,
body: body,
),
),
],
),
child: body,
);
},
),

2
packages/neon/neon/lib/src/pages/settings.dart

@ -78,7 +78,7 @@ class _SettingsPageState extends State<SettingsPage> {
for (final appImplementation in appImplementations) ...[
if (appImplementation.options.options.isNotEmpty) ...[
CustomSettingsTile(
leading: appImplementation.buildIcon(context),
leading: appImplementation.buildIcon(),
title: Text(appImplementation.name(context)),
onTap: () {
NextcloudAppSettingsRoute(appid: appImplementation.id).go(context);

31
packages/neon/neon/lib/src/utils/app_implementation.dart

@ -37,21 +37,34 @@ abstract class AppImplementation<T extends Bloc, R extends NextcloudAppSpecificO
},
);
BehaviorSubject<int>? getUnreadCounter(final AppsBloc appsBloc);
BehaviorSubject<int>? getUnreadCounter(final T bloc) => null;
Widget get page;
Widget buildIcon(
final BuildContext context, {
NeonNavigationDestination destination(final BuildContext context) {
final accountsBloc = Provider.of<AccountsBloc>(context, listen: false);
final account = accountsBloc.activeAccount.value!;
final bloc = getBloc(account);
return NeonNavigationDestination(
label: name(context),
icon: buildIcon,
notificationCount: getUnreadCounter(bloc),
);
}
Widget buildIcon({
final Size size = const Size.square(32),
final Color? color,
}) =>
SizedBox.fromSize(
size: size,
child: SvgPicture.asset(
'assets/app.svg',
package: 'neon_$id',
colorFilter: ColorFilter.mode(color ?? Theme.of(context).colorScheme.primary, BlendMode.srcIn),
Builder(
builder: (final context) => SizedBox.fromSize(
size: size,
child: SvgPicture.asset(
'assets/app.svg',
package: 'neon_$id',
colorFilter: ColorFilter.mode(color ?? Theme.of(context).colorScheme.primary, BlendMode.srcIn),
),
),
);

2
packages/neon/neon/lib/src/utils/global_options.dart

@ -224,6 +224,7 @@ class GlobalOptions {
NavigationMode.drawerAlwaysVisible: (final context) =>
AppLocalizations.of(context).globalOptionsNavigationModeDrawerAlwaysVisible,
},
// ignore: deprecated_member_use_from_same_package
NavigationMode.quickBar: (final context) => AppLocalizations.of(context).globalOptionsNavigationModeQuickBar,
}),
);
@ -232,5 +233,6 @@ class GlobalOptions {
enum NavigationMode {
drawer,
drawerAlwaysVisible,
@Deprecated("The new design won't use this anymore")
quickBar,
}

1
packages/neon/neon/lib/src/widgets/app_implementation_icon.dart

@ -24,7 +24,6 @@ class NeonAppImplementationIcon extends StatelessWidget {
Container(
margin: const EdgeInsets.all(5),
child: appImplementation.buildIcon(
context,
size: size,
color: color,
),

210
packages/neon/neon/lib/src/widgets/drawer.dart

@ -0,0 +1,210 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:meta/meta.dart';
import 'package:neon/l10n/localizations.dart';
import 'package:neon/neon.dart';
import 'package:neon/src/router.dart';
import 'package:neon/src/widgets/drawer_destination.dart';
import 'package:provider/provider.dart';
@internal
class NeonDrawer extends StatelessWidget {
const NeonDrawer({
super.key,
});
@override
Widget build(final BuildContext context) {
final accountsBloc = Provider.of<AccountsBloc>(context, listen: false);
final appsBloc = accountsBloc.activeAppsBloc;
return StreamBuilder(
stream: appsBloc.appImplementations,
builder: (final context, final snapshot) {
if (snapshot.data?.data == null) {
return Container();
}
return _NeonDrawer(
apps: snapshot.data!.data!,
);
},
);
}
}
class _NeonDrawer extends StatefulWidget {
const _NeonDrawer({
required this.apps,
});
final Iterable<AppImplementation> apps;
@override
State<_NeonDrawer> createState() => __NeonDrawerState();
}
class __NeonDrawerState extends State<_NeonDrawer> with SingleTickerProviderStateMixin {
late TabController _tabController;
late AccountsBloc _accountsBloc;
late AppsBloc _appsBloc;
late List<AppImplementation> _apps;
int _activeApp = 0;
@override
void initState() {
super.initState();
_accountsBloc = Provider.of<AccountsBloc>(context, listen: false);
_appsBloc = _accountsBloc.activeAppsBloc;
_apps = widget.apps.toList();
_activeApp = _apps.indexWhere((final app) => app.id == _appsBloc.activeAppID.valueOrNull);
_tabController = TabController(
vsync: this,
length: widget.apps.length,
);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
void onAppChange(final int index) {
Scaffold.maybeOf(context)?.closeDrawer();
// selected item is not a registered app like the SettingsPage
if (index >= _apps.length) {
const SettingsRoute().go(context);
return;
}
setState(() {
_activeApp = index;
});
unawaited(_appsBloc.setActiveApp(_apps[index].id));
//context.goNamed(apps[index].routeName);
}
@override
Widget build(final BuildContext context) {
final appDestinations = _apps.map(
(final app) => NavigationDrawerDestinationExtension.fromNeonDestination(
app.destination(context),
),
);
final drawer = NavigationDrawer(
selectedIndex: _activeApp,
onDestinationSelected: onAppChange,
children: [
const NeonDrawerHeader(),
...appDestinations,
NavigationDrawerDestination(
icon: const Icon(Icons.settings),
label: Text(AppLocalizations.of(context).settings),
),
],
);
return drawer;
}
}
@internal
class NeonDrawerHeader extends StatelessWidget {
const NeonDrawerHeader({super.key});
@override
Widget build(final BuildContext context) {
final accountsBloc = Provider.of<AccountsBloc>(context, listen: false);
final capabilitiesBloc = accountsBloc.activeCapabilitiesBloc;
final accountSelecor = StreamBuilder<List<Account>>(
stream: accountsBloc.accounts,
builder: (final context, final accountsSnapshot) {
final accounts = accountsSnapshot.data;
if (accounts == null || accounts.length <= 1) {
return const SizedBox.shrink();
}
final items = accounts.map((final account) {
final child = NeonAccountTile(
account: account,
dense: true,
textColor: Theme.of(context).appBarTheme.foregroundColor,
);
return DropdownMenuItem(
value: account,
child: child,
);
}).toList();
return DropdownButtonHideUnderline(
child: DropdownButton(
isExpanded: true,
dropdownColor: Theme.of(context).colorScheme.primary,
iconEnabledColor: Theme.of(context).colorScheme.onBackground,
value: accountsBloc.activeAccount.value,
items: items,
onChanged: (final account) {
if (account == null) {
return;
}
accountsBloc.setActiveAccount(account);
},
),
);
},
);
return ResultBuilder<Capabilities>(
stream: capabilitiesBloc.capabilities,
builder: (final context, final capabilities) => DrawerHeader(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (capabilities.data != null) ...[
if (capabilities.data!.capabilities.theming?.name != null) ...[
Text(
capabilities.data!.capabilities.theming!.name!,
style: DefaultTextStyle.of(context).style.copyWith(
color: Theme.of(context).appBarTheme.foregroundColor,
),
),
],
if (capabilities.data!.capabilities.theming?.logo != null) ...[
Flexible(
child: NeonCachedUrlImage(
url: capabilities.data!.capabilities.theming!.logo!,
),
),
],
] else ...[
NeonException(
capabilities.error,
onRetry: capabilitiesBloc.refresh,
),
NeonLinearProgressIndicator(
visible: capabilities.loading,
),
],
accountSelecor,
],
),
),
);
}
}

137
packages/neon/neon/lib/src/widgets/drawer_destination.dart

@ -0,0 +1,137 @@
import 'package:flutter/material.dart';
import 'package:neon/neon.dart';
import 'package:rxdart/subjects.dart';
typedef DestinationIconBuilder = Widget Function({Size size, Color color});
class NeonNavigationDestination {
const NeonNavigationDestination({
required this.label,
required this.icon,
this.selectedIcon,
this.notificationCount,
});
final String label;
final DestinationIconBuilder icon;
final Widget? selectedIcon;
final BehaviorSubject<int>? notificationCount;
}
extension NavigationDestinationExtension on NavigationDestination {
static NavigationDestination fromNeonDestination(final NeonNavigationDestination neonDestination) =>
NavigationDestination(
label: neonDestination.label,
icon: neonDestination.icon(),
selectedIcon: neonDestination.selectedIcon,
);
}
extension NavigationRailDestinationExtension on NavigationRailDestination {
static NavigationRailDestination fromNeonDestination(final NeonNavigationDestination neonDestination) {
final iconWIdget = StreamBuilder(
stream: neonDestination.notificationCount,
initialData: 0,
builder: (final context, final snapshot) {
final colorScheme = Theme.of(context).colorScheme;
final color = snapshot.data! > 0 ? colorScheme.primary : colorScheme.onBackground;
const size = Size.square(kAvatarSize * 2 / 3);
final icon = Container(
margin: const EdgeInsets.all(5),
child: neonDestination.icon(size: size, color: color),
);
if (snapshot.data! <= 0) {
return icon;
}
final notificationIdicator = Builder(
builder: (final context) {
final style = TextStyle(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
);
return Text(
snapshot.data!.toString(),
style: style,
);
},
);
return Stack(
alignment: Alignment.bottomRight,
children: [
icon,
notificationIdicator,
],
);
},
);
return NavigationRailDestination(
label: Text(neonDestination.label),
icon: iconWIdget,
selectedIcon: neonDestination.selectedIcon,
);
}
}
extension NavigationDrawerDestinationExtension on NavigationDrawerDestination {
static NavigationDrawerDestination fromNeonDestination(final NeonNavigationDestination neonDestination) {
final labelWidget = StreamBuilder(
stream: neonDestination.notificationCount,
initialData: 0,
builder: (final context, final snapshot) {
final label = Text(neonDestination.label);
if (snapshot.data! <= 0) {
return label;
}
final notificationIdicator = Padding(
padding: const EdgeInsets.only(left: 12, right: 24),
child: Builder(
builder: (final context) {
final style = TextStyle(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
fontSize: 14,
);
return Text(
snapshot.data!.toString(),
style: style,
);
},
),
);
return Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
label,
notificationIdicator,
],
),
);
},
);
return NavigationDrawerDestination(
label: labelWidget,
icon: neonDestination.icon(),
selectedIcon: neonDestination.selectedIcon,
);
}
}
extension TabExtension on Tab {
static Tab fromNeonDestination(final NeonNavigationDestination neonDestination) => Tab(
text: neonDestination.label,
icon: neonDestination.icon(),
);
}

1
packages/neon/neon/pubspec.yaml

@ -26,6 +26,7 @@ dependencies:
intl: ^0.18.0
json_annotation: ^4.8.1
material_design_icons_flutter: ^7.0.7296
meta: ^1.9.1
nextcloud:
git:
url: https://github.com/provokateurin/nextcloud-neon

3
packages/neon/neon_files/lib/neon_files.dart

@ -63,7 +63,4 @@ class FilesApp extends AppImplementation<FilesBloc, FilesAppSpecificOptions> {
@override
Widget get page => const FilesMainPage();
@override
BehaviorSubject<int>? getUnreadCounter(final AppsBloc appsBloc) => null;
}

2
packages/neon/neon_news/lib/neon_news.dart

@ -71,5 +71,5 @@ class NewsApp extends AppImplementation<NewsBloc, NewsAppSpecificOptions> {
Widget get page => const NewsMainPage();
@override
BehaviorSubject<int> getUnreadCounter(final AppsBloc appsBloc) => appsBloc.getAppBloc<NewsBloc>(this).unreadCounter;
BehaviorSubject<int> getUnreadCounter(final NewsBloc bloc) => bloc.unreadCounter;
}

3
packages/neon/neon_notes/lib/neon_notes.dart

@ -60,7 +60,4 @@ class NotesApp extends AppImplementation<NotesBloc, NotesAppSpecificOptions> {
@override
Widget get page => const NotesMainPage();
@override
BehaviorSubject<int>? getUnreadCounter(final AppsBloc appsBloc) => null;
}

3
packages/neon/neon_notifications/lib/neon_notifications.dart

@ -41,6 +41,5 @@ class NotificationsApp extends AppImplementation<NotificationsBloc, Notification
Widget get page => const NotificationsMainPage();
@override
BehaviorSubject<int> getUnreadCounter(final AppsBloc appsBloc) =>
appsBloc.getAppBloc<NotificationsBloc>(this).unreadCounter;
BehaviorSubject<int> getUnreadCounter(final NotificationsBloc bloc) => bloc.unreadCounter;
}

1
packages/neon/neon_notifications/lib/pages/main.dart

@ -74,7 +74,6 @@ class _NotificationsMainPageState extends State<NotificationsMainPage> {
),
leading: app != null
? app.buildIcon(
context,
size: const Size.square(40),
)
: SizedBox.fromSize(

Loading…
Cancel
Save