Browse Source

perf(neon): do not use the spread operator for building lists

Signed-off-by: Nikolas Rimikis <leptopoda@users.noreply.github.com>
pull/1094/head
Nikolas Rimikis 1 year ago
parent
commit
550aaf81e5
No known key found for this signature in database
GPG Key ID: 85ED1DE9786A4FF2
  1. 6
      packages/neon/neon/lib/src/app.dart
  2. 83
      packages/neon/neon/lib/src/pages/login.dart
  3. 3
      packages/neon/neon/lib/src/pages/login_check_account.dart
  4. 3
      packages/neon/neon/lib/src/pages/login_check_server_status.dart
  5. 45
      packages/neon/neon/lib/src/pages/login_flow.dart
  6. 39
      packages/neon/neon/lib/src/pages/nextcloud_app_settings.dart
  7. 281
      packages/neon/neon/lib/src/pages/settings.dart
  8. 7
      packages/neon/neon/lib/src/widgets/account_tile.dart
  9. 118
      packages/neon/neon/lib/src/widgets/app_bar.dart
  10. 6
      packages/neon/neon/lib/src/widgets/unified_search_results.dart

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

@ -120,13 +120,13 @@ class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver, tray.Tra
await tray.trayManager.setContextMenu(
tray.Menu(
items: [
for (final app in _appImplementations) ...[
tray.MenuItem(
..._appImplementations.map(
(final app) => tray.MenuItem(
key: 'app_${app.id}',
label: app.nameFromLocalization(localizations),
// TODO: Add icons which should work on macOS and Windows
),
],
),
tray.MenuItem.separator(),
tray.MenuItem(
key: 'show_hide',

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

@ -67,56 +67,55 @@ class _LoginPageState extends State<LoginPage> {
branding.name,
style: Theme.of(context).textTheme.titleLarge,
),
if (branding.showLoginWithNextcloud) ...[
const SizedBox(
height: 10,
if (branding.showLoginWithNextcloud)
Padding(
padding: const EdgeInsets.only(top: 10),
child: Text(NeonLocalizations.of(context).loginWorksWith),
),
Text(NeonLocalizations.of(context).loginWorksWith),
const SizedBox(
height: 10,
),
Semantics(
label: NeonLocalizations.of(context).nextcloud,
child: const NextcloudLogo(),
if (branding.showLoginWithNextcloud)
Padding(
padding: const EdgeInsets.only(top: 10),
child: Semantics(
label: NeonLocalizations.of(context).nextcloud,
child: const NextcloudLogo(),
),
),
],
const SizedBox(
height: 50,
),
Form(
key: _formKey,
child: TextFormField(
focusNode: _focusNode,
controller: _controller,
decoration: InputDecoration(
hintText: 'https://...',
labelText: NeonLocalizations.of(context).loginUsingServerAddress,
suffixIcon: IconButton(
icon: const Icon(Icons.arrow_forward),
onPressed: () {
login(_controller.text);
},
Padding(
padding: const EdgeInsets.only(top: 50),
child: Form(
key: _formKey,
child: TextFormField(
focusNode: _focusNode,
controller: _controller,
decoration: InputDecoration(
hintText: 'https://...',
labelText: NeonLocalizations.of(context).loginUsingServerAddress,
suffixIcon: IconButton(
icon: const Icon(Icons.arrow_forward),
onPressed: () {
login(_controller.text);
},
),
),
keyboardType: TextInputType.url,
validator: (final input) => validateHttpUrl(context, input),
onFieldSubmitted: login,
autofillHints: const [AutofillHints.url],
),
keyboardType: TextInputType.url,
validator: (final input) => validateHttpUrl(context, input),
onFieldSubmitted: login,
autofillHints: const [AutofillHints.url],
),
),
if (NeonPlatform.instance.canUseCamera) ...[
const SizedBox(
height: 50,
),
IconButton(
tooltip: NeonLocalizations.of(context).loginUsingQRcode,
icon: const Icon(
Icons.qr_code_scanner_rounded,
size: 60,
if (NeonPlatform.instance.canUseCamera)
Padding(
padding: const EdgeInsets.only(top: 50),
child: IconButton(
tooltip: NeonLocalizations.of(context).loginUsingQRcode,
icon: const Icon(
Icons.qr_code_scanner_rounded,
size: 60,
),
onPressed: () => const LoginQRcodeRoute().go(context),
),
onPressed: () => const LoginQRcodeRoute().go(context),
),
],
],
),
),

3
packages/neon/neon/lib/src/pages/login_check_account.dart

@ -65,7 +65,7 @@ class _LoginCheckAccountPageState extends State<LoginCheckAccountPage> {
builder: (final context, final state) => Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (state.hasError) ...[
if (state.hasError)
Builder(
builder: (final context) {
final details = NeonError.getDetails(state.error);
@ -77,7 +77,6 @@ class _LoginCheckAccountPageState extends State<LoginCheckAccountPage> {
);
},
),
],
_buildAccountTile(state),
Align(
alignment: Alignment.bottomRight,

3
packages/neon/neon/lib/src/pages/login_check_server_status.dart

@ -65,12 +65,11 @@ class _LoginCheckServerStatusPageState extends State<LoginCheckServerStatusPage>
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (state.hasError) ...[
if (state.hasError)
NeonValidationTile(
title: NeonError.getDetails(state.error).getText(context),
state: ValidationState.failure,
),
],
_buildServerVersionTile(state),
_buildMaintenanceModeTile(state),
Align(

45
packages/neon/neon/lib/src/pages/login_flow.dart

@ -6,6 +6,7 @@ import 'package:neon/src/blocs/login_flow.dart';
import 'package:neon/src/router.dart';
import 'package:neon/src/widgets/error.dart';
import 'package:neon/src/widgets/linear_progress_indicator.dart';
import 'package:nextcloud/core.dart' as core;
import 'package:url_launcher/url_launcher_string.dart';
@internal
@ -65,29 +66,35 @@ class _LoginFlowPageState extends State<LoginFlowPage> {
subject: bloc.init,
builder: (final context, final init) => Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
NeonLinearProgressIndicator(
visible: init.isLoading,
),
NeonError(
init.error,
onRetry: bloc.refresh,
),
if (init.hasData) ...[
Text(NeonLocalizations.of(context).loginSwitchToBrowserWindow),
const SizedBox(
height: 10,
),
ElevatedButton(
onPressed: bloc.refresh,
child: Text(NeonLocalizations.of(context).loginOpenAgain),
),
],
],
children: _buildChildren(init).toList(),
),
),
),
),
),
);
Iterable<Widget> _buildChildren(final Result<core.LoginFlowV2> init) sync* {
yield NeonLinearProgressIndicator(
visible: init.isLoading,
);
if (init.hasError) {
yield NeonError(
init.error,
onRetry: bloc.refresh,
);
}
if (init.hasData) {
yield Text(NeonLocalizations.of(context).loginSwitchToBrowserWindow);
yield Padding(
padding: const EdgeInsets.only(top: 10),
child: ElevatedButton(
onPressed: bloc.refresh,
child: Text(NeonLocalizations.of(context).loginOpenAgain),
),
);
}
}
}

39
packages/neon/neon/lib/src/pages/nextcloud_app_settings.dart

@ -39,23 +39,7 @@ class NextcloudAppSettingsPage extends StatelessWidget {
);
final body = SettingsList(
categories: [
for (final category in [...appImplementation.options.categories, null]) ...[
if (appImplementation.options.options.where((final option) => option.category == category).isNotEmpty) ...[
SettingsCategory(
title: Text(
category != null ? category.name(context) : NeonLocalizations.of(context).optionsCategoryOther,
),
tiles: [
for (final option
in appImplementation.options.options.where((final option) => option.category == category)) ...[
OptionSettingsTile(option: option),
],
],
),
],
],
],
categories: _buildCategories(context).toList(),
);
return Scaffold(
@ -71,4 +55,25 @@ class NextcloudAppSettingsPage extends StatelessWidget {
),
);
}
Iterable<Widget> _buildCategories(final BuildContext context) sync* {
final appOptions = appImplementation.options;
final categories = [...appOptions.categories, null];
for (final category in categories) {
final matchedOptions = appOptions.options.where((final option) => option.category == category);
if (matchedOptions.isNotEmpty) {
yield SettingsCategory(
title: Text(
category != null ? category.name(context) : NeonLocalizations.of(context).optionsCategoryOther,
),
tiles: [
...matchedOptions.map(
(final option) => OptionSettingsTile(option: option),
),
],
);
}
}
}
}

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

@ -14,7 +14,6 @@ import 'package:neon/src/settings/widgets/custom_settings_tile.dart';
import 'package:neon/src/settings/widgets/option_settings_tile.dart';
import 'package:neon/src/settings/widgets/settings_category.dart';
import 'package:neon/src/settings/widgets/settings_list.dart';
import 'package:neon/src/settings/widgets/settings_tile.dart';
import 'package:neon/src/settings/widgets/text_settings_tile.dart';
import 'package:neon/src/theme/branding.dart';
import 'package:neon/src/theme/dialog.dart';
@ -89,7 +88,6 @@ class _SettingsPageState extends State<SettingsPage> {
final globalOptions = NeonProvider.of<GlobalOptions>(context);
final accountsBloc = NeonProvider.of<AccountsBloc>(context);
final appImplementations = NeonProvider.of<Iterable<AppImplementation>>(context);
final branding = Branding.of(context);
final appBar = AppBar(
title: Text(NeonLocalizations.of(context).settings),
@ -116,24 +114,7 @@ class _SettingsPageState extends State<SettingsPage> {
final body = SettingsList(
initialCategory: widget.initialCategory?.name,
categories: [
SettingsCategory(
hasLeading: true,
title: Text(NeonLocalizations.of(context).settingsApps),
key: ValueKey(SettingsCategories.apps.name),
tiles: <SettingsTile>[
for (final appImplementation in appImplementations) ...[
if (appImplementation.options.options.isNotEmpty) ...[
CustomSettingsTile(
leading: appImplementation.buildIcon(),
title: Text(appImplementation.name(context)),
onTap: () {
NextcloudAppSettingsRoute(appid: appImplementation.id).go(context);
},
),
],
],
],
),
buildAppCategory(),
SettingsCategory(
title: Text(NeonLocalizations.of(context).optionsCategoryTheme),
key: ValueKey(SettingsCategories.theme.name),
@ -159,7 +140,7 @@ class _SettingsPageState extends State<SettingsPage> {
],
),
if (NeonPlatform.instance.canUsePushNotifications) buildNotificationsCategory(),
if (NeonPlatform.instance.canUseWindowManager) ...[
if (NeonPlatform.instance.canUseWindowManager)
SettingsCategory(
title: Text(NeonLocalizations.of(context).optionsCategoryStartup),
key: ValueKey(SettingsCategories.startup.name),
@ -172,8 +153,7 @@ class _SettingsPageState extends State<SettingsPage> {
),
],
),
],
if (NeonPlatform.instance.canUseWindowManager && NeonPlatform.instance.canUseSystemTray) ...[
if (NeonPlatform.instance.canUseWindowManager && NeonPlatform.instance.canUseSystemTray)
SettingsCategory(
title: Text(NeonLocalizations.of(context).optionsCategorySystemTray),
key: ValueKey(SettingsCategories.systemTray.name),
@ -186,120 +166,8 @@ class _SettingsPageState extends State<SettingsPage> {
),
],
),
],
...buildAccountCategory(),
SettingsCategory(
hasLeading: true,
title: Text(NeonLocalizations.of(context).optionsCategoryOther),
key: ValueKey(SettingsCategories.other.name),
tiles: [
if (branding.sourceCodeURL != null)
CustomSettingsTile(
leading: Icon(
Icons.code,
color: Theme.of(context).colorScheme.primary,
),
title: Text(NeonLocalizations.of(context).sourceCode),
onTap: () async {
await launchUrlString(
branding.sourceCodeURL!,
mode: LaunchMode.externalApplication,
);
},
),
if (branding.issueTrackerURL != null)
CustomSettingsTile(
leading: Icon(
MdiIcons.textBoxEditOutline,
color: Theme.of(context).colorScheme.primary,
),
title: Text(NeonLocalizations.of(context).issueTracker),
onTap: () async {
await launchUrlString(
branding.issueTrackerURL!,
mode: LaunchMode.externalApplication,
);
},
),
CustomSettingsTile(
leading: Icon(
MdiIcons.scriptText,
color: Theme.of(context).colorScheme.primary,
),
title: Text(NeonLocalizations.of(context).licenses),
onTap: () async {
showLicensePage(
context: context,
applicationName: branding.name,
applicationIcon: branding.logo,
applicationLegalese: branding.legalese,
applicationVersion: NeonProvider.of<PackageInfo>(context).version,
);
},
),
CustomSettingsTile(
leading: Icon(
MdiIcons.export,
color: Theme.of(context).colorScheme.primary,
),
title: Text(NeonLocalizations.of(context).settingsExport),
onTap: () async {
final settingsExportHelper = _buildSettingsExportHelper(context);
try {
final fileName = 'nextcloud-neon-settings-${DateTime.now().millisecondsSinceEpoch ~/ 1000}.json';
final data = settingsExportHelper.exportToFile();
await saveFileWithPickDialog(fileName, data);
} catch (e, s) {
debugPrint(e.toString());
debugPrint(s.toString());
if (mounted) {
NeonError.showSnackbar(context, e);
}
}
},
),
CustomSettingsTile(
leading: Icon(
MdiIcons.import,
color: Theme.of(context).colorScheme.primary,
),
title: Text(NeonLocalizations.of(context).settingsImport),
onTap: () async {
final settingsExportHelper = _buildSettingsExportHelper(context);
try {
final result = await FilePicker.platform.pickFiles(
withReadStream: true,
);
if (result == null) {
return;
}
if (!result.files.single.path!.endsWith('.json')) {
if (mounted) {
NeonError.showSnackbar(
context,
NeonLocalizations.of(context).settingsImportWrongFileExtension,
);
}
return;
}
await settingsExportHelper.applyFromFile(result.files.single.readStream);
} catch (e, s) {
debugPrint(e.toString());
debugPrint(s.toString());
if (mounted) {
NeonError.showSnackbar(context, e);
}
}
},
),
],
),
buildOtherCategory(),
],
);
@ -317,6 +185,30 @@ class _SettingsPageState extends State<SettingsPage> {
);
}
Widget buildAppCategory() {
final appImplementations = NeonProvider.of<Iterable<AppImplementation>>(context);
final appsWithOptions = appImplementations.where(
(final app) => app.options.options.isNotEmpty,
);
final tiles = appsWithOptions.map(
(final appImplementation) => CustomSettingsTile(
leading: appImplementation.buildIcon(),
title: Text(appImplementation.name(context)),
onTap: () {
NextcloudAppSettingsRoute(appid: appImplementation.id).go(context);
},
),
);
return SettingsCategory(
hasLeading: true,
title: Text(NeonLocalizations.of(context).settingsApps),
key: ValueKey(SettingsCategories.apps.name),
tiles: tiles.toList(),
);
}
Widget buildNotificationsCategory() {
final globalOptions = NeonProvider.of<GlobalOptions>(context);
@ -416,6 +308,123 @@ class _SettingsPageState extends State<SettingsPage> {
}
}
Widget buildOtherCategory() {
final branding = Branding.of(context);
return SettingsCategory(
hasLeading: true,
title: Text(NeonLocalizations.of(context).optionsCategoryOther),
key: ValueKey(SettingsCategories.other.name),
tiles: [
if (branding.sourceCodeURL != null)
CustomSettingsTile(
leading: Icon(
Icons.code,
color: Theme.of(context).colorScheme.primary,
),
title: Text(NeonLocalizations.of(context).sourceCode),
onTap: () async {
await launchUrlString(
branding.sourceCodeURL!,
mode: LaunchMode.externalApplication,
);
},
),
if (branding.issueTrackerURL != null)
CustomSettingsTile(
leading: Icon(
MdiIcons.textBoxEditOutline,
color: Theme.of(context).colorScheme.primary,
),
title: Text(NeonLocalizations.of(context).issueTracker),
onTap: () async {
await launchUrlString(
branding.issueTrackerURL!,
mode: LaunchMode.externalApplication,
);
},
),
CustomSettingsTile(
leading: Icon(
MdiIcons.scriptText,
color: Theme.of(context).colorScheme.primary,
),
title: Text(NeonLocalizations.of(context).licenses),
onTap: () async {
showLicensePage(
context: context,
applicationName: branding.name,
applicationIcon: branding.logo,
applicationLegalese: branding.legalese,
applicationVersion: NeonProvider.of<PackageInfo>(context).version,
);
},
),
CustomSettingsTile(
leading: Icon(
MdiIcons.export,
color: Theme.of(context).colorScheme.primary,
),
title: Text(NeonLocalizations.of(context).settingsExport),
onTap: () async {
final settingsExportHelper = _buildSettingsExportHelper(context);
try {
final fileName = 'nextcloud-neon-settings-${DateTime.now().millisecondsSinceEpoch ~/ 1000}.json';
final data = settingsExportHelper.exportToFile();
await saveFileWithPickDialog(fileName, data);
} catch (e, s) {
debugPrint(e.toString());
debugPrint(s.toString());
if (mounted) {
NeonError.showSnackbar(context, e);
}
}
},
),
CustomSettingsTile(
leading: Icon(
MdiIcons.import,
color: Theme.of(context).colorScheme.primary,
),
title: Text(NeonLocalizations.of(context).settingsImport),
onTap: () async {
final settingsExportHelper = _buildSettingsExportHelper(context);
try {
final result = await FilePicker.platform.pickFiles(
withReadStream: true,
);
if (result == null) {
return;
}
if (!result.files.single.path!.endsWith('.json')) {
if (mounted) {
NeonError.showSnackbar(
context,
NeonLocalizations.of(context).settingsImportWrongFileExtension,
);
}
return;
}
await settingsExportHelper.applyFromFile(result.files.single.readStream);
} catch (e, s) {
debugPrint(e.toString());
debugPrint(s.toString());
if (mounted) {
NeonError.showSnackbar(context, e);
}
}
},
),
],
);
}
SettingsExportHelper _buildSettingsExportHelper(final BuildContext context) {
final globalOptions = NeonProvider.of<GlobalOptions>(context);
final accountsBloc = NeonProvider.of<AccountsBloc>(context);

7
packages/neon/neon/lib/src/widgets/account_tile.dart

@ -64,10 +64,11 @@ class NeonAccountTile extends StatelessWidget {
overflow: TextOverflow.ellipsis,
),
),
if (userDetails.isLoading)
const Expanded(
child: NeonLinearProgressIndicator(),
Expanded(
child: NeonLinearProgressIndicator(
visible: userDetails.isLoading,
),
),
if (userDetails.hasError)
NeonError(
userDetails.error,

118
packages/neon/neon/lib/src/widgets/app_bar.dart

@ -1,6 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:intersperse/intersperse.dart';
import 'package:meta/meta.dart';
import 'package:neon/l10n/localizations.dart';
import 'package:neon/src/bloc/result.dart';
@ -72,53 +73,47 @@ class _NeonAppBarState extends State<NeonAppBar> {
stream: unifiedSearchBloc.enabled,
builder: (final context, final unifiedSearchEnabledSnapshot) {
final unifiedSearchEnabled = unifiedSearchEnabledSnapshot.data ?? false;
return AppBar(
title: unifiedSearchEnabled
? null
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
if (activeAppSnapshot.hasData) ...[
Flexible(
child: Text(
activeAppSnapshot.requireData.name(context),
),
),
],
if (appImplementations.hasError) ...[
const SizedBox(
width: 8,
),
NeonError(
appImplementations.error,
onRetry: appsBloc.refresh,
type: NeonErrorType.iconOnly,
),
],
if (appImplementations.isLoading) ...[
const SizedBox(
width: 8,
),
Expanded(
child: NeonLinearProgressIndicator(
color: Theme.of(context).appBarTheme.foregroundColor,
),
),
],
],
),
if (accounts.length > 1) ...[
Text(
account.humanReadableID,
style: Theme.of(context).textTheme.bodySmall,
),
],
],
Widget header = Row(
children: [
if (activeAppSnapshot.hasData)
Flexible(
child: Text(
activeAppSnapshot.requireData.name(context),
),
),
if (appImplementations.hasError)
NeonError(
appImplementations.error,
onRetry: appsBloc.refresh,
type: NeonErrorType.iconOnly,
),
if (appImplementations.isLoading)
Expanded(
child: NeonLinearProgressIndicator(
color: Theme.of(context).appBarTheme.foregroundColor,
),
),
].intersperse(const SizedBox(width: 8)).toList(),
);
if (accounts.length > 1) {
header = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
header,
Text(
account.humanReadableID,
style: Theme.of(context).textTheme.bodySmall,
),
],
);
}
return AppBar(
title: unifiedSearchEnabled ? null : header,
actions: [
if (unifiedSearchEnabled) ...[
if (unifiedSearchEnabled)
Flexible(
child: SearchBar(
focusNode: _searchBarFocusNode,
@ -137,10 +132,9 @@ class _NeonAppBarState extends State<NeonAppBar> {
),
],
),
),
] else ...[
)
else
const SearchIconButton(),
],
const NotificationIconButton(),
const AccountSwitcherButton(),
],
@ -217,21 +211,25 @@ class _NotificationIconButtonState extends State<NotificationIconButton> {
Future<void> _openNotifications(
final NotificationsAppInterface app,
) async {
Widget title = Text(app.name(context));
if (_accounts.length > 1) {
title = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
title,
Text(
_account.humanReadableID,
style: Theme.of(context).textTheme.bodySmall,
),
],
);
}
final page = Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(app.name(context)),
if (_accounts.length > 1) ...[
Text(
_account.humanReadableID,
style: Theme.of(context).textTheme.bodySmall,
),
],
],
),
title: title,
),
body: SafeArea(
child: app.page,

6
packages/neon/neon/lib/src/widgets/unified_search_results.dart

@ -81,7 +81,7 @@ class NeonUnifiedSearchResults extends StatelessWidget {
NeonLinearProgressIndicator(
visible: result.isLoading,
),
if (entries.isEmpty) ...[
if (entries.isEmpty)
AdaptiveListTile(
leading: const Icon(
Icons.close,
@ -89,8 +89,7 @@ class NeonUnifiedSearchResults extends StatelessWidget {
),
title: Text(NeonLocalizations.of(context).searchNoResults),
),
],
for (final entry in entries) ...[
for (final entry in entries)
AdaptiveListTile(
leading: NeonImageWrapper(
size: const Size.square(largeIconSize),
@ -102,7 +101,6 @@ class NeonUnifiedSearchResults extends StatelessWidget {
context.go(entry.resourceUrl);
},
),
],
],
),
),

Loading…
Cancel
Save