Browse Source

Merge pull request #265 from provokateurin/feature/files-navigation

Add proper files navigation
pull/266/head
Kate 2 years ago committed by GitHub
parent
commit
e146c81397
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      packages/neon/neon/lib/neon.dart
  2. 104
      packages/neon/neon/lib/src/app.dart
  3. 8
      packages/neon/neon/lib/src/pages/home.dart
  4. 10
      packages/neon/neon/lib/src/pages/login.dart
  5. 51
      packages/neon/neon/lib/src/router.dart
  6. 260
      packages/neon/neon_files/lib/widgets/browser_view.dart
  7. 8
      packages/neon/neon_news/lib/pages/article.dart
  8. 6
      packages/neon/neon_notes/lib/pages/note.dart

1
packages/neon/neon/lib/neon.dart

@ -66,6 +66,7 @@ part 'src/platform/abstract.dart';
part 'src/platform/android.dart'; part 'src/platform/android.dart';
part 'src/platform/linux.dart'; part 'src/platform/linux.dart';
part 'src/platform/platform.dart'; part 'src/platform/platform.dart';
part 'src/router.dart';
part 'src/utils/account_options.dart'; part 'src/utils/account_options.dart';
part 'src/utils/app_implementation.dart'; part 'src/utils/app_implementation.dart';
part 'src/utils/bloc.dart'; part 'src/utils/bloc.dart';

104
packages/neon/neon/lib/src/app.dart

@ -25,7 +25,15 @@ class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver, tray.Tra
late NeonPlatform _platform; late NeonPlatform _platform;
late GlobalOptions _globalOptions; late GlobalOptions _globalOptions;
late AccountsBloc _accountsBloc; late AccountsBloc _accountsBloc;
final _globalPopups = const GlobalPopups(); late final _routerDelegate = AppRouter(
navigatorKey: _navigatorKey,
accountsBloc: _accountsBloc,
onThemeChanged: (final nextcloudTheme) {
setState(() {
_nextcloudTheme = nextcloudTheme;
});
},
);
NextcloudCoreServerCapabilities_Ocs_Data_Capabilities_Theming? _nextcloudTheme; NextcloudCoreServerCapabilities_Ocs_Data_Capabilities_Theming? _nextcloudTheme;
final _platformBrightness = BehaviorSubject<Brightness>.seeded(WidgetsBinding.instance.window.platformBrightness); final _platformBrightness = BehaviorSubject<Brightness>.seeded(WidgetsBinding.instance.window.platformBrightness);
@ -55,50 +63,6 @@ class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver, tray.Tra
} }
WidgetsBinding.instance.addPostFrameCallback((final _) async { WidgetsBinding.instance.addPostFrameCallback((final _) async {
widget.accountsBloc.activeAccount.listen((final activeAccount) async {
FlutterNativeSplash.remove();
if (activeAccount == null) {
await _navigatorKey.currentState!.pushAndRemoveUntil(
MaterialPageRoute(
builder: (final context) => const LoginPage(),
),
(final _) => false,
);
} else {
const settings = RouteSettings(
name: 'home',
);
Widget builder(final context) => Scaffold(
resizeToAvoidBottomInset: false,
body: Stack(
children: [
_globalPopups,
HomePage(
account: activeAccount,
onThemeChanged: (final nextcloudTheme) {
setState(() {
_nextcloudTheme = nextcloudTheme;
});
},
),
],
),
);
await _navigatorKey.currentState!.pushAndRemoveUntil(
widget.globalOptions.navigationMode.value == NavigationMode.drawer
? MaterialPageRoute(
settings: settings,
builder: builder,
)
: _NoAnimationPageRoute(
settings: settings,
builder: builder,
),
(final _) => false,
);
}
});
final localizations = await appLocalizationsFromSystem(); final localizations = await appLocalizationsFromSystem();
if (!mounted) { if (!mounted) {
@ -310,36 +274,30 @@ class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver, tray.Tra
option: widget.globalOptions.themeOLEDAsDark, option: widget.globalOptions.themeOLEDAsDark,
builder: (final context, final themeOLEDAsDark) => OptionBuilder( builder: (final context, final themeOLEDAsDark) => OptionBuilder(
option: widget.globalOptions.themeKeepOriginalAccentColor, option: widget.globalOptions.themeKeepOriginalAccentColor,
builder: (final context, final themeKeepOriginalAccentColor) { builder: (final context, final themeKeepOriginalAccentColor) => StreamBuilder<Account?>(
if (themeMode == null || !platformBrightnessSnapshot.hasData || themeOLEDAsDark == null) { stream: widget.accountsBloc.activeAccount,
return Container(); builder: (final context, final activeAccountSnapshot) {
} if (themeMode == null || !platformBrightnessSnapshot.hasData || themeOLEDAsDark == null) {
return MaterialApp( return Container();
localizationsDelegates: AppLocalizations.localizationsDelegates, }
supportedLocales: AppLocalizations.supportedLocales,
navigatorKey: _navigatorKey, FlutterNativeSplash.remove();
theme: getThemeFromNextcloudTheme( return MaterialApp.router(
_nextcloudTheme, localizationsDelegates: AppLocalizations.localizationsDelegates,
themeMode, supportedLocales: AppLocalizations.supportedLocales,
platformBrightnessSnapshot.data!, theme: getThemeFromNextcloudTheme(
oledAsDark: themeOLEDAsDark, _nextcloudTheme,
keepOriginalAccentColor: _nextcloudTheme == null || (themeKeepOriginalAccentColor ?? false), themeMode,
), platformBrightnessSnapshot.data!,
home: Container(), oledAsDark: themeOLEDAsDark,
); keepOriginalAccentColor: _nextcloudTheme == null || (themeKeepOriginalAccentColor ?? false),
}, ),
routerDelegate: _routerDelegate,
);
},
),
), ),
), ),
), ),
); );
} }
class _NoAnimationPageRoute extends MaterialPageRoute {
_NoAnimationPageRoute({
required super.builder,
super.settings,
});
@override
Duration get transitionDuration => Duration.zero;
}

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

@ -202,15 +202,15 @@ class _HomePageState extends State<HomePage> {
) => ) =>
OptionBuilder<NavigationMode>( OptionBuilder<NavigationMode>(
option: _globalOptions.navigationMode, option: _globalOptions.navigationMode,
builder: (final context, final navigationMode) => WillPopScope( builder: (final context, final navigationMode) => BackButtonListener(
onWillPop: () async { onBackButtonPressed: () async {
if (_scaffoldKey.currentState!.isDrawerOpen) { if (_scaffoldKey.currentState!.isDrawerOpen) {
Navigator.pop(context); Navigator.pop(context);
return true; return false;
} }
_scaffoldKey.currentState!.openDrawer(); _scaffoldKey.currentState!.openDrawer();
return false; return true;
}, },
child: Builder( child: Builder(
builder: (final context) { builder: (final context) {

10
packages/neon/neon/lib/src/pages/login.dart

@ -90,18 +90,18 @@ class _LoginPageState extends State<LoginPage> {
@override @override
Widget build(final BuildContext context) => StreamBuilder<List<Account>>( Widget build(final BuildContext context) => StreamBuilder<List<Account>>(
stream: _accountsBloc.accounts, stream: _accountsBloc.accounts,
builder: (final context, final accountsSnapshot) => WillPopScope( builder: (final context, final accountsSnapshot) => BackButtonListener(
onWillPop: () async { onBackButtonPressed: () async {
if (accountsSnapshot.data?.isNotEmpty ?? false) { if (accountsSnapshot.data?.isNotEmpty ?? false) {
return true; return false;
} }
if ((await _loginBloc.serverURL.first) == null) { if ((await _loginBloc.serverURL.first) == null) {
return true; return false;
} }
_loginBloc.setServerURL(null); _loginBloc.setServerURL(null);
return false; return true;
}, },
child: StreamBuilder<String?>( child: StreamBuilder<String?>(
stream: _loginBloc.serverURL, stream: _loginBloc.serverURL,

51
packages/neon/neon/lib/src/router.dart

@ -0,0 +1,51 @@
part of '../neon.dart';
// ignore: prefer_mixin
class AppRouter extends RouterDelegate<Account> with ChangeNotifier, PopNavigatorRouterDelegateMixin<Account> {
AppRouter({
required this.navigatorKey,
required this.accountsBloc,
required this.onThemeChanged,
});
final AccountsBloc accountsBloc;
final Function(NextcloudTheme? theme) onThemeChanged;
final _globalPopups = const GlobalPopups();
@override
final GlobalKey<NavigatorState> navigatorKey;
@override
Future setNewRoutePath(final Account? configuration) async {}
@override
Account? get currentConfiguration => accountsBloc.activeAccount.valueOrNull;
@override
Widget build(final BuildContext context) => Navigator(
key: navigatorKey,
onPopPage: (final route, final result) => route.didPop(result),
pages: [
if (currentConfiguration == null) ...[
const MaterialPage(
child: LoginPage(),
),
] else ...[
MaterialPage(
child: Scaffold(
resizeToAvoidBottomInset: false,
body: Stack(
children: [
_globalPopups,
HomePage(
account: currentConfiguration!,
onThemeChanged: onThemeChanged,
),
],
),
),
),
],
],
);
}

260
packages/neon/neon_files/lib/widgets/browser_view.dart

@ -46,142 +46,154 @@ class _FilesBrowserViewState extends State<FilesBrowserView> {
!uploadTasksSnapshot.hasData || !uploadTasksSnapshot.hasData ||
!downloadTasksSnapshot.hasData !downloadTasksSnapshot.hasData
? Container() ? Container()
: Scaffold( : BackButtonListener(
resizeToAvoidBottomInset: false, onBackButtonPressed: () async {
floatingActionButton: widget.enableCreateActions final path = pathSnapshot.data!;
? FloatingActionButton( if (path.isNotEmpty) {
onPressed: () async { widget.bloc.setPath(path.sublist(0, path.length - 1));
await showDialog( return true;
context: context, }
builder: (final context) => FilesChooseCreateDialog( return false;
bloc: widget.filesBloc, },
basePath: widget.bloc.path.value, child: Scaffold(
), resizeToAvoidBottomInset: false,
); floatingActionButton: widget.enableCreateActions
}, ? FloatingActionButton(
child: const Icon(Icons.add), onPressed: () async {
) await showDialog(
: null, context: context,
body: NeonListView<Widget>( builder: (final context) => FilesChooseCreateDialog(
scrollKey: 'files-${pathSnapshot.data!.join('/')}', bloc: widget.filesBloc,
withFloatingActionButton: true, basePath: widget.bloc.path.value,
items: [ ),
for (final uploadTask in files.data == null );
? <UploadTask>[] },
: uploadTasksSnapshot.data!.where( child: const Icon(Icons.add),
(final task) => )
files.data!.where((final file) => _pathMatchesFile(task.path, file.name)).isEmpty, : null,
)) ...[ body: NeonListView<Widget>(
StreamBuilder<int>( scrollKey: 'files-${pathSnapshot.data!.join('/')}',
stream: uploadTask.progress, withFloatingActionButton: true,
builder: (final context, final uploadTaskProgressSnapshot) => items: [
!uploadTaskProgressSnapshot.hasData for (final uploadTask in files.data == null
? Container() ? <UploadTask>[]
: _buildFile( : uploadTasksSnapshot.data!.where(
context: context, (final task) => files.data!
details: FileDetails( .where((final file) => _pathMatchesFile(task.path, file.name))
path: uploadTask.path, .isEmpty,
isDirectory: false, )) ...[
size: uploadTask.size, StreamBuilder<int>(
etag: null, stream: uploadTask.progress,
mimeType: null, builder: (final context, final uploadTaskProgressSnapshot) =>
lastModified: uploadTask.lastModified, !uploadTaskProgressSnapshot.hasData
hasPreview: null, ? Container()
isFavorite: null, : _buildFile(
context: context,
details: FileDetails(
path: uploadTask.path,
isDirectory: false,
size: uploadTask.size,
etag: null,
mimeType: null,
lastModified: uploadTask.lastModified,
hasPreview: null,
isFavorite: null,
),
uploadProgress: uploadTaskProgressSnapshot.data,
downloadProgress: null,
), ),
uploadProgress: uploadTaskProgressSnapshot.data, ),
downloadProgress: null, ],
), if (files.data != null) ...[
), for (final file in files.data!) ...[
], if (!widget.onlyShowDirectories || file.isDirectory) ...[
if (files.data != null) ...[ Builder(
for (final file in files.data!) ...[ builder: (final context) {
if (!widget.onlyShowDirectories || file.isDirectory) ...[ final matchingUploadTasks = uploadTasksSnapshot.data!
Builder( .where((final task) => _pathMatchesFile(task.path, file.name));
builder: (final context) { final matchingDownloadTasks = downloadTasksSnapshot.data!
final matchingUploadTasks = uploadTasksSnapshot.data! .where((final task) => _pathMatchesFile(task.path, file.name));
.where((final task) => _pathMatchesFile(task.path, file.name));
final matchingDownloadTasks = downloadTasksSnapshot.data!
.where((final task) => _pathMatchesFile(task.path, file.name));
return StreamBuilder<int?>( return StreamBuilder<int?>(
stream: matchingUploadTasks.isNotEmpty stream: matchingUploadTasks.isNotEmpty
? matchingUploadTasks.first.progress ? matchingUploadTasks.first.progress
: Stream.value(null),
builder: (final context, final uploadTaskProgressSnapshot) => StreamBuilder<int?>(
stream: matchingDownloadTasks.isNotEmpty
? matchingDownloadTasks.first.progress
: Stream.value(null), : Stream.value(null),
builder: (final context, final downloadTaskProgressSnapshot) => _buildFile( builder: (final context, final uploadTaskProgressSnapshot) =>
context: context, StreamBuilder<int?>(
details: FileDetails( stream: matchingDownloadTasks.isNotEmpty
path: [...widget.bloc.path.value, file.name], ? matchingDownloadTasks.first.progress
isDirectory: matchingUploadTasks.isEmpty && file.isDirectory, : Stream.value(null),
size: matchingUploadTasks.isNotEmpty builder: (final context, final downloadTaskProgressSnapshot) => _buildFile(
? matchingUploadTasks.first.size context: context,
: file.size!, details: FileDetails(
etag: matchingUploadTasks.isNotEmpty ? null : file.etag, path: [...widget.bloc.path.value, file.name],
mimeType: matchingUploadTasks.isNotEmpty ? null : file.mimeType, isDirectory: matchingUploadTasks.isEmpty && file.isDirectory,
lastModified: matchingUploadTasks.isNotEmpty size: matchingUploadTasks.isNotEmpty
? matchingUploadTasks.first.lastModified ? matchingUploadTasks.first.size
: file.lastModified!, : file.size!,
hasPreview: matchingUploadTasks.isNotEmpty ? null : file.hasPreview, etag: matchingUploadTasks.isNotEmpty ? null : file.etag,
isFavorite: matchingUploadTasks.isNotEmpty ? null : file.favorite, mimeType: matchingUploadTasks.isNotEmpty ? null : file.mimeType,
lastModified: matchingUploadTasks.isNotEmpty
? matchingUploadTasks.first.lastModified
: file.lastModified!,
hasPreview: matchingUploadTasks.isNotEmpty ? null : file.hasPreview,
isFavorite: matchingUploadTasks.isNotEmpty ? null : file.favorite,
),
uploadProgress: uploadTaskProgressSnapshot.data,
downloadProgress: downloadTaskProgressSnapshot.data,
), ),
uploadProgress: uploadTaskProgressSnapshot.data,
downloadProgress: downloadTaskProgressSnapshot.data,
), ),
), );
); },
}, ),
), ],
], ],
], ],
], ],
], isLoading: files.loading,
isLoading: files.loading, error: files.error,
error: files.error, onRefresh: widget.bloc.refresh,
onRefresh: widget.bloc.refresh, builder: (final context, final widget) => widget,
builder: (final context, final widget) => widget, topScrollingChildren: [
topScrollingChildren: [ Align(
Align( alignment: Alignment.topLeft,
alignment: Alignment.topLeft, child: Container(
child: Container( margin: const EdgeInsets.symmetric(
margin: const EdgeInsets.symmetric( horizontal: 10,
horizontal: 10, ),
), child: Wrap(
child: Wrap( crossAxisAlignment: WrapCrossAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center, children: <Widget>[
children: <Widget>[ SizedBox(
SizedBox( height: 40,
height: 40, child: InkWell(
child: InkWell( onTap: () {
onTap: () { widget.bloc.setPath([]);
widget.bloc.setPath([]); },
}, child: const Icon(Icons.house),
child: const Icon(Icons.house), ),
),
),
for (var i = 0; i < pathSnapshot.data!.length; i++) ...[
InkWell(
onTap: () {
widget.bloc.setPath(pathSnapshot.data!.sublist(0, i + 1));
},
child: Text(pathSnapshot.data![i]),
), ),
], for (var i = 0; i < pathSnapshot.data!.length; i++) ...[
] InkWell(
.intersperse( onTap: () {
const Icon( widget.bloc.setPath(pathSnapshot.data!.sublist(0, i + 1));
Icons.keyboard_arrow_right, },
size: 40, child: Text(pathSnapshot.data![i]),
), ),
) ],
.toList(), ]
.intersperse(
const Icon(
Icons.keyboard_arrow_right,
size: 40,
),
)
.toList(),
),
), ),
), ),
), ],
], ),
), ),
), ),
), ),

8
packages/neon/neon_news/lib/pages/article.dart

@ -81,17 +81,17 @@ class _NewsArticlePageState extends State<NewsArticlePage> {
} }
@override @override
Widget build(final BuildContext context) => WillPopScope( Widget build(final BuildContext context) => BackButtonListener(
onWillPop: () async { onBackButtonPressed: () async {
if (_webviewController != null && await _webviewController!.canGoBack()) { if (_webviewController != null && await _webviewController!.canGoBack()) {
await _webviewController!.goBack(); await _webviewController!.goBack();
return false; return true;
} }
if (mounted && Provider.of<NeonPlatform>(context, listen: false).canUseWakelock) { if (mounted && Provider.of<NeonPlatform>(context, listen: false).canUseWakelock) {
await Wakelock.disable(); await Wakelock.disable();
} }
return true; return false;
}, },
child: Scaffold( child: Scaffold(
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,

6
packages/neon/neon_notes/lib/pages/note.dart

@ -63,12 +63,12 @@ class _NotesNotePageState extends State<NotesNotePage> {
} }
@override @override
Widget build(final BuildContext context) => WillPopScope( Widget build(final BuildContext context) => BackButtonListener(
onWillPop: () async { onBackButtonPressed: () async {
if (Provider.of<NeonPlatform>(context, listen: false).canUseWakelock) { if (Provider.of<NeonPlatform>(context, listen: false).canUseWakelock) {
await Wakelock.disable(); await Wakelock.disable();
} }
return true; return false;
}, },
child: Scaffold( child: Scaffold(
resizeToAvoidBottomInset: true, resizeToAvoidBottomInset: true,

Loading…
Cancel
Save