Compare commits

...

5 Commits

Author SHA1 Message Date
Nikolas Rimikis 22ac4ed496
perf(neon_notifications): do not use the spread operator for building lists 1 year ago
Nikolas Rimikis 9b460d4b3a
perf(neon_notes): do not use the spread operator for building lists 1 year ago
Nikolas Rimikis 4f4e161063
perf(neon_news): do not use the spread operator for building lists 1 year ago
Nikolas Rimikis 84cc5744d1
perf(neon_files): do not use the spread operator for building lists 1 year ago
Nikolas Rimikis 550aaf81e5
perf(neon): do not use the spread operator for building lists 1 year ago
  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
  11. 3
      packages/neon/neon_files/lib/dialogs/choose_create.dart
  12. 11
      packages/neon/neon_files/lib/options.dart
  13. 122
      packages/neon/neon_files/lib/pages/details.dart
  14. 9
      packages/neon/neon_files/lib/widgets/actions.dart
  15. 19
      packages/neon/neon_files/lib/widgets/file_list_tile.dart
  16. 25
      packages/neon/neon_news/lib/dialogs/add_feed.dart
  17. 4
      packages/neon/neon_news/lib/pages/article.dart
  18. 46
      packages/neon/neon_news/lib/widgets/articles_view.dart
  19. 235
      packages/neon/neon_news/lib/widgets/feeds_view.dart
  20. 21
      packages/neon/neon_notes/lib/dialogs/create_note.dart
  21. 14
      packages/neon/neon_notes/lib/dialogs/select_category.dart
  22. 24
      packages/neon/neon_notes/lib/widgets/notes_view.dart
  23. 15
      packages/neon/neon_notifications/lib/pages/main.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( await tray.trayManager.setContextMenu(
tray.Menu( tray.Menu(
items: [ items: [
for (final app in _appImplementations) ...[ ..._appImplementations.map(
tray.MenuItem( (final app) => tray.MenuItem(
key: 'app_${app.id}', key: 'app_${app.id}',
label: app.nameFromLocalization(localizations), label: app.nameFromLocalization(localizations),
// TODO: Add icons which should work on macOS and Windows // TODO: Add icons which should work on macOS and Windows
), ),
], ),
tray.MenuItem.separator(), tray.MenuItem.separator(),
tray.MenuItem( tray.MenuItem(
key: 'show_hide', key: 'show_hide',

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

@ -67,56 +67,55 @@ class _LoginPageState extends State<LoginPage> {
branding.name, branding.name,
style: Theme.of(context).textTheme.titleLarge, style: Theme.of(context).textTheme.titleLarge,
), ),
if (branding.showLoginWithNextcloud) ...[ if (branding.showLoginWithNextcloud)
const SizedBox( Padding(
height: 10, padding: const EdgeInsets.only(top: 10),
child: Text(NeonLocalizations.of(context).loginWorksWith),
), ),
Text(NeonLocalizations.of(context).loginWorksWith), if (branding.showLoginWithNextcloud)
const SizedBox( Padding(
height: 10, padding: const EdgeInsets.only(top: 10),
), child: Semantics(
Semantics( label: NeonLocalizations.of(context).nextcloud,
label: NeonLocalizations.of(context).nextcloud, child: const NextcloudLogo(),
child: const NextcloudLogo(), ),
), ),
], Padding(
const SizedBox( padding: const EdgeInsets.only(top: 50),
height: 50, child: Form(
), key: _formKey,
Form( child: TextFormField(
key: _formKey, focusNode: _focusNode,
child: TextFormField( controller: _controller,
focusNode: _focusNode, decoration: InputDecoration(
controller: _controller, hintText: 'https://...',
decoration: InputDecoration( labelText: NeonLocalizations.of(context).loginUsingServerAddress,
hintText: 'https://...', suffixIcon: IconButton(
labelText: NeonLocalizations.of(context).loginUsingServerAddress, icon: const Icon(Icons.arrow_forward),
suffixIcon: IconButton( onPressed: () {
icon: const Icon(Icons.arrow_forward), login(_controller.text);
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) ...[ if (NeonPlatform.instance.canUseCamera)
const SizedBox( Padding(
height: 50, padding: const EdgeInsets.only(top: 50),
), child: IconButton(
IconButton( tooltip: NeonLocalizations.of(context).loginUsingQRcode,
tooltip: NeonLocalizations.of(context).loginUsingQRcode, icon: const Icon(
icon: const Icon( Icons.qr_code_scanner_rounded,
Icons.qr_code_scanner_rounded, size: 60,
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( builder: (final context, final state) => Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
if (state.hasError) ...[ if (state.hasError)
Builder( Builder(
builder: (final context) { builder: (final context) {
final details = NeonError.getDetails(state.error); final details = NeonError.getDetails(state.error);
@ -77,7 +77,6 @@ class _LoginCheckAccountPageState extends State<LoginCheckAccountPage> {
); );
}, },
), ),
],
_buildAccountTile(state), _buildAccountTile(state),
Align( Align(
alignment: Alignment.bottomRight, 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( return Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
if (state.hasError) ...[ if (state.hasError)
NeonValidationTile( NeonValidationTile(
title: NeonError.getDetails(state.error).getText(context), title: NeonError.getDetails(state.error).getText(context),
state: ValidationState.failure, state: ValidationState.failure,
), ),
],
_buildServerVersionTile(state), _buildServerVersionTile(state),
_buildMaintenanceModeTile(state), _buildMaintenanceModeTile(state),
Align( 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/router.dart';
import 'package:neon/src/widgets/error.dart'; import 'package:neon/src/widgets/error.dart';
import 'package:neon/src/widgets/linear_progress_indicator.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'; import 'package:url_launcher/url_launcher_string.dart';
@internal @internal
@ -65,29 +66,35 @@ class _LoginFlowPageState extends State<LoginFlowPage> {
subject: bloc.init, subject: bloc.init,
builder: (final context, final init) => Column( builder: (final context, final init) => Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: _buildChildren(init).toList(),
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),
),
],
],
), ),
), ),
), ),
), ),
), ),
); );
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( final body = SettingsList(
categories: [ categories: _buildCategories(context).toList(),
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),
],
],
),
],
],
],
); );
return Scaffold( 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/option_settings_tile.dart';
import 'package:neon/src/settings/widgets/settings_category.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_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/settings/widgets/text_settings_tile.dart';
import 'package:neon/src/theme/branding.dart'; import 'package:neon/src/theme/branding.dart';
import 'package:neon/src/theme/dialog.dart'; import 'package:neon/src/theme/dialog.dart';
@ -89,7 +88,6 @@ class _SettingsPageState extends State<SettingsPage> {
final globalOptions = NeonProvider.of<GlobalOptions>(context); final globalOptions = NeonProvider.of<GlobalOptions>(context);
final accountsBloc = NeonProvider.of<AccountsBloc>(context); final accountsBloc = NeonProvider.of<AccountsBloc>(context);
final appImplementations = NeonProvider.of<Iterable<AppImplementation>>(context); final appImplementations = NeonProvider.of<Iterable<AppImplementation>>(context);
final branding = Branding.of(context);
final appBar = AppBar( final appBar = AppBar(
title: Text(NeonLocalizations.of(context).settings), title: Text(NeonLocalizations.of(context).settings),
@ -116,24 +114,7 @@ class _SettingsPageState extends State<SettingsPage> {
final body = SettingsList( final body = SettingsList(
initialCategory: widget.initialCategory?.name, initialCategory: widget.initialCategory?.name,
categories: [ categories: [
SettingsCategory( buildAppCategory(),
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);
},
),
],
],
],
),
SettingsCategory( SettingsCategory(
title: Text(NeonLocalizations.of(context).optionsCategoryTheme), title: Text(NeonLocalizations.of(context).optionsCategoryTheme),
key: ValueKey(SettingsCategories.theme.name), key: ValueKey(SettingsCategories.theme.name),
@ -159,7 +140,7 @@ class _SettingsPageState extends State<SettingsPage> {
], ],
), ),
if (NeonPlatform.instance.canUsePushNotifications) buildNotificationsCategory(), if (NeonPlatform.instance.canUsePushNotifications) buildNotificationsCategory(),
if (NeonPlatform.instance.canUseWindowManager) ...[ if (NeonPlatform.instance.canUseWindowManager)
SettingsCategory( SettingsCategory(
title: Text(NeonLocalizations.of(context).optionsCategoryStartup), title: Text(NeonLocalizations.of(context).optionsCategoryStartup),
key: ValueKey(SettingsCategories.startup.name), 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( SettingsCategory(
title: Text(NeonLocalizations.of(context).optionsCategorySystemTray), title: Text(NeonLocalizations.of(context).optionsCategorySystemTray),
key: ValueKey(SettingsCategories.systemTray.name), key: ValueKey(SettingsCategories.systemTray.name),
@ -186,120 +166,8 @@ class _SettingsPageState extends State<SettingsPage> {
), ),
], ],
), ),
],
...buildAccountCategory(), ...buildAccountCategory(),
SettingsCategory( buildOtherCategory(),
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);
}
}
},
),
],
),
], ],
); );
@ -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() { Widget buildNotificationsCategory() {
final globalOptions = NeonProvider.of<GlobalOptions>(context); 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) { SettingsExportHelper _buildSettingsExportHelper(final BuildContext context) {
final globalOptions = NeonProvider.of<GlobalOptions>(context); final globalOptions = NeonProvider.of<GlobalOptions>(context);
final accountsBloc = NeonProvider.of<AccountsBloc>(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, overflow: TextOverflow.ellipsis,
), ),
), ),
if (userDetails.isLoading) Expanded(
const Expanded( child: NeonLinearProgressIndicator(
child: NeonLinearProgressIndicator(), visible: userDetails.isLoading,
), ),
),
if (userDetails.hasError) if (userDetails.hasError)
NeonError( NeonError(
userDetails.error, userDetails.error,

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

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

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

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

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

@ -77,7 +77,7 @@ class _FilesChooseCreateDialogState extends State<FilesChooseCreateDialog> {
} }
}, },
), ),
if (NeonPlatform.instance.canUseCamera) ...[ if (NeonPlatform.instance.canUseCamera)
ListTile( ListTile(
leading: Icon( leading: Icon(
MdiIcons.cameraPlus, MdiIcons.cameraPlus,
@ -94,7 +94,6 @@ class _FilesChooseCreateDialogState extends State<FilesChooseCreateDialog> {
} }
}, },
), ),
],
ListTile( ListTile(
leading: Icon( leading: Icon(
MdiIcons.folderPlus, MdiIcons.folderPlus,

11
packages/neon/neon_files/lib/options.dart

@ -67,9 +67,7 @@ class FilesAppSpecificOptions extends NextcloudAppOptions {
label: (final context) => FilesLocalizations.of(context).optionsUploadQueueParallelism, label: (final context) => FilesLocalizations.of(context).optionsUploadQueueParallelism,
defaultValue: 4, defaultValue: 4,
values: { values: {
for (var i = 1; i <= 16; i = i * 2) ...{ for (var i = 1; i <= 16; i = i * 2) i: (final _) => i.toString(),
i: (final _) => i.toString(),
},
}, },
); );
@ -80,9 +78,7 @@ class FilesAppSpecificOptions extends NextcloudAppOptions {
label: (final context) => FilesLocalizations.of(context).optionsDownloadQueueParallelism, label: (final context) => FilesLocalizations.of(context).optionsDownloadQueueParallelism,
defaultValue: 4, defaultValue: 4,
values: { values: {
for (var i = 1; i <= 16; i = i * 2) ...{ for (var i = 1; i <= 16; i = i * 2) i: (final _) => i.toString(),
i: (final _) => i.toString(),
},
}, },
); );
@ -96,9 +92,8 @@ class FilesAppSpecificOptions extends NextcloudAppOptions {
2 * 2024, 2 * 2024,
6 * 1024, 6 * 1024,
10 * 1024, 10 * 1024,
]) ...{ ])
_mb(i): (final _) => filesize(_mb(i)), _mb(i): (final _) => filesize(_mb(i)),
},
}; };
int _mb(final int i) => i * 1024 * 1024; int _mb(final int i) => i * 1024 * 1024;

122
packages/neon/neon_files/lib/pages/details.dart

@ -11,66 +11,72 @@ class FilesDetailsPage extends StatelessWidget {
final FileDetails details; final FileDetails details;
@override @override
Widget build(final BuildContext context) => Scaffold( Widget build(final BuildContext context) {
resizeToAvoidBottomInset: false, final l10n = FilesLocalizations.of(context);
appBar: AppBar(
title: Text(details.name), return Scaffold(
), resizeToAvoidBottomInset: false,
body: SafeArea( appBar: AppBar(
child: ListView( title: Text(details.name),
primary: true, ),
children: [ body: SafeArea(
ColoredBox( child: ListView(
color: Theme.of(context).colorScheme.primary, primary: true,
child: FilePreview( children: [
bloc: bloc, ColoredBox(
details: details, color: Theme.of(context).colorScheme.primary,
color: Theme.of(context).colorScheme.onPrimary, child: FilePreview(
size: Size( bloc: bloc,
MediaQuery.of(context).size.width, details: details,
MediaQuery.of(context).size.height / 4, color: Theme.of(context).colorScheme.onPrimary,
), size: Size(
MediaQuery.of(context).size.width,
MediaQuery.of(context).size.height / 4,
), ),
), ),
DataTable( ),
headingRowHeight: 0, DataTable(
columns: const [ headingRowHeight: 0,
DataColumn(label: SizedBox()), columns: const [
DataColumn(label: SizedBox()), DataColumn(label: SizedBox()),
], DataColumn(label: SizedBox()),
rows: [ ],
for (final entry in { rows: [
details.isDirectory _buildDataRow(
? FilesLocalizations.of(context).detailsFolderName details.isDirectory ? l10n.detailsFolderName : l10n.detailsFileName,
: FilesLocalizations.of(context).detailsFileName: details.name, details.name,
FilesLocalizations.of(context).detailsParentFolder: ),
details.path.length == 1 ? '/' : details.path.sublist(0, details.path.length - 1).join('/'), _buildDataRow(
if (details.size != null) ...{ l10n.detailsParentFolder,
details.isDirectory details.path.length == 1 ? '/' : details.path.sublist(0, details.path.length - 1).join('/'),
? FilesLocalizations.of(context).detailsFolderSize ),
: FilesLocalizations.of(context).detailsFileSize: filesize(details.size, 1), if (details.size != null)
}, _buildDataRow(
if (details.lastModified != null) ...{ details.isDirectory ? l10n.detailsFolderSize : l10n.detailsFileSize,
FilesLocalizations.of(context).detailsLastModified: filesize(details.size, 1),
details.lastModified!.toLocal().toIso8601String(), ),
}, if (details.lastModified != null)
if (details.isFavorite != null) ...{ _buildDataRow(
FilesLocalizations.of(context).detailsIsFavorite: details.isFavorite! l10n.detailsLastModified,
? FilesLocalizations.of(context).actionYes details.lastModified!.toLocal().toIso8601String(),
: FilesLocalizations.of(context).actionNo, ),
}, if (details.isFavorite != null)
}.entries) ...[ _buildDataRow(
DataRow( l10n.detailsIsFavorite,
cells: [ details.isFavorite! ? l10n.actionYes : l10n.actionNo,
DataCell(Text(entry.key)), ),
DataCell(Text(entry.value)), ],
], ),
), ],
],
],
),
],
),
), ),
),
);
}
DataRow _buildDataRow(final String key, final String value) => DataRow(
cells: [
DataCell(Text(key)),
DataCell(Text(value)),
],
); );
} }

9
packages/neon/neon_files/lib/widgets/actions.dart

@ -119,13 +119,12 @@ class FileActions extends StatelessWidget {
@override @override
Widget build(final BuildContext context) => PopupMenuButton<FilesFileAction>( Widget build(final BuildContext context) => PopupMenuButton<FilesFileAction>(
itemBuilder: (final context) => [ itemBuilder: (final context) => [
if (!details.isDirectory && NeonPlatform.instance.canUseSharing) ...[ if (!details.isDirectory && NeonPlatform.instance.canUseSharing)
PopupMenuItem( PopupMenuItem(
value: FilesFileAction.share, value: FilesFileAction.share,
child: Text(FilesLocalizations.of(context).actionShare), child: Text(FilesLocalizations.of(context).actionShare),
), ),
], if (details.isFavorite != null)
if (details.isFavorite != null) ...[
PopupMenuItem( PopupMenuItem(
value: FilesFileAction.toggleFavorite, value: FilesFileAction.toggleFavorite,
child: Text( child: Text(
@ -134,7 +133,6 @@ class FileActions extends StatelessWidget {
: FilesLocalizations.of(context).addToFavorites, : FilesLocalizations.of(context).addToFavorites,
), ),
), ),
],
PopupMenuItem( PopupMenuItem(
value: FilesFileAction.details, value: FilesFileAction.details,
child: Text(FilesLocalizations.of(context).details), child: Text(FilesLocalizations.of(context).details),
@ -152,12 +150,11 @@ class FileActions extends StatelessWidget {
child: Text(FilesLocalizations.of(context).actionCopy), child: Text(FilesLocalizations.of(context).actionCopy),
), ),
// TODO: https://github.com/provokateurin/nextcloud-neon/issues/4 // TODO: https://github.com/provokateurin/nextcloud-neon/issues/4
if (!details.isDirectory) ...[ if (!details.isDirectory)
PopupMenuItem( PopupMenuItem(
value: FilesFileAction.sync, value: FilesFileAction.sync,
child: Text(FilesLocalizations.of(context).actionSync), child: Text(FilesLocalizations.of(context).actionSync),
), ),
],
PopupMenuItem( PopupMenuItem(
value: FilesFileAction.delete, value: FilesFileAction.delete,
child: Text(FilesLocalizations.of(context).actionDelete), child: Text(FilesLocalizations.of(context).actionDelete),

19
packages/neon/neon_files/lib/widgets/file_list_tile.dart

@ -56,17 +56,16 @@ class FileListTile extends StatelessWidget {
RelativeTime( RelativeTime(
date: details.lastModified!, date: details.lastModified!,
), ),
if (details.size != null && details.size! > 0) ...[ if (details.size != null && details.size! > 0)
const SizedBox( Padding(
width: 10, padding: const EdgeInsets.only(left: 10),
child: Text(
filesize(details.size, 1),
style: DefaultTextStyle.of(context).style.copyWith(
color: Colors.grey,
),
),
), ),
Text(
filesize(details.size, 1),
style: DefaultTextStyle.of(context).style.copyWith(
color: Colors.grey,
),
),
],
], ],
), ),
leading: _FileIcon( leading: _FileIcon(

25
packages/neon/neon_news/lib/dialogs/add_feed.dart

@ -71,30 +71,29 @@ class _NewsAddFeedDialogState extends State<NewsAddFeedDialog> {
submit(); submit();
}, },
), ),
if (widget.folderID == null) ...[ if (widget.folderID == null && folders.hasError)
Center( Center(
child: NeonError( child: NeonError(
folders.error, folders.error,
onRetry: widget.bloc.refresh, onRetry: widget.bloc.refresh,
), ),
), ),
if (widget.folderID == null)
Center( Center(
child: NeonLinearProgressIndicator( child: NeonLinearProgressIndicator(
visible: folders.isLoading, visible: folders.isLoading,
), ),
), ),
if (folders.hasData) ...[ if (widget.folderID == null && folders.hasData)
NewsFolderSelect( NewsFolderSelect(
folders: folders.requireData, folders: folders.requireData,
value: folder, value: folder,
onChanged: (final f) { onChanged: (final f) {
setState(() { setState(() {
folder = f; folder = f;
}); });
}, },
), ),
],
],
ElevatedButton( ElevatedButton(
onPressed: folders.hasData ? submit : null, onPressed: folders.hasData ? submit : null,
child: Text(NewsLocalizations.of(context).feedAdd), child: Text(NewsLocalizations.of(context).feedAdd),

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

@ -146,7 +146,7 @@ class _NewsArticlePageState extends State<NewsArticlePage> {
); );
}, },
), ),
if (widget.url != null) ...[ if (widget.url != null)
IconButton( IconButton(
onPressed: () async { onPressed: () async {
await launchUrlString( await launchUrlString(
@ -157,6 +157,7 @@ class _NewsArticlePageState extends State<NewsArticlePage> {
tooltip: NewsLocalizations.of(context).articleOpenLink, tooltip: NewsLocalizations.of(context).articleOpenLink,
icon: const Icon(Icons.open_in_new), icon: const Icon(Icons.open_in_new),
), ),
if (widget.url != null)
IconButton( IconButton(
onPressed: () async { onPressed: () async {
await Share.share(await _getURL()); await Share.share(await _getURL());
@ -164,7 +165,6 @@ class _NewsArticlePageState extends State<NewsArticlePage> {
tooltip: NewsLocalizations.of(context).articleShare, tooltip: NewsLocalizations.of(context).articleShare,
icon: const Icon(Icons.share), icon: const Icon(Icons.share),
), ),
],
], ],
), ),
body: SafeArea( body: SafeArea(

46
packages/neon/neon_news/lib/widgets/articles_view.dart

@ -63,30 +63,20 @@ class _NewsArticlesViewState extends State<NewsArticlesView> {
isExpanded: true, isExpanded: true,
value: selectedFilterTypeSnapshot.data, value: selectedFilterTypeSnapshot.data,
items: [ items: [
FilterType.all, _buildDropdownItem(
FilterType.unread, FilterType.all,
if (widget.bloc.listType == null) ...[ NewsLocalizations.of(context).articlesFilterAll,
FilterType.starred, ),
], _buildDropdownItem(
].map<DropdownMenuItem<FilterType>>( FilterType.unread,
(final a) { NewsLocalizations.of(context).articlesFilterUnread,
late final String label; ),
switch (a) { if (widget.bloc.listType == null)
case FilterType.all: _buildDropdownItem(
label = NewsLocalizations.of(context).articlesFilterAll; FilterType.starred,
case FilterType.unread: NewsLocalizations.of(context).articlesFilterStarred,
label = NewsLocalizations.of(context).articlesFilterUnread; ),
case FilterType.starred: ],
label = NewsLocalizations.of(context).articlesFilterStarred;
default:
throw Exception('FilterType $a should not be shown');
}
return DropdownMenuItem(
value: a,
child: Text(label),
);
},
).toList(),
onChanged: (final value) { onChanged: (final value) {
widget.bloc.setFilterType(value!); widget.bloc.setFilterType(value!);
}, },
@ -99,6 +89,11 @@ class _NewsArticlesViewState extends State<NewsArticlesView> {
), ),
); );
DropdownMenuItem<FilterType> _buildDropdownItem(final FilterType value, final String label) => DropdownMenuItem(
value: value,
child: Text(label),
);
Widget _buildArticle( Widget _buildArticle(
final BuildContext context, final BuildContext context,
final news.Article article, final news.Article article,
@ -116,13 +111,12 @@ class _NewsArticlesViewState extends State<NewsArticlesView> {
: Theme.of(context).textTheme.titleMedium!.copyWith(color: Theme.of(context).disabledColor), : Theme.of(context).textTheme.titleMedium!.copyWith(color: Theme.of(context).disabledColor),
), ),
), ),
if (article.mediaThumbnail != null) ...[ if (article.mediaThumbnail != null)
NeonUrlImage( NeonUrlImage(
url: article.mediaThumbnail!, url: article.mediaThumbnail!,
size: const Size(100, 50), size: const Size(100, 50),
fit: BoxFit.cover, fit: BoxFit.cover,
), ),
],
], ],
), ),
subtitle: Row( subtitle: Row(

235
packages/neon/neon_news/lib/widgets/feeds_view.dart

@ -42,128 +42,131 @@ class NewsFeedsView extends StatelessWidget {
final BuildContext context, final BuildContext context,
final news.Feed feed, final news.Feed feed,
final List<news.Folder> folders, final List<news.Folder> folders,
) => ) {
ListTile( Widget trailing = PopupMenuButton<NewsFeedAction>(
title: Text( itemBuilder: (final context) => [
feed.title, PopupMenuItem(
style: feed.unreadCount! == 0 value: NewsFeedAction.showURL,
? Theme.of(context).textTheme.titleMedium!.copyWith(color: Theme.of(context).disabledColor) child: Text(NewsLocalizations.of(context).feedShowURL),
: null,
), ),
subtitle: feed.unreadCount! > 0 PopupMenuItem(
? Text(NewsLocalizations.of(context).articlesUnread(feed.unreadCount!)) value: NewsFeedAction.delete,
: const SizedBox(), child: Text(NewsLocalizations.of(context).actionDelete),
leading: NewsFeedIcon(feed: feed),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (feed.updateErrorCount > 0) ...[
IconButton(
onPressed: () async {
await showDialog<void>(
context: context,
builder: (final context) => NewsFeedUpdateErrorDialog(
feed: feed,
),
);
},
tooltip: NewsLocalizations.of(context).feedShowErrorMessage,
iconSize: 30,
icon: Text(
feed.updateErrorCount.toString(),
style: TextStyle(
color: Theme.of(context).colorScheme.error,
),
),
),
],
PopupMenuButton<NewsFeedAction>(
itemBuilder: (final context) => [
PopupMenuItem(
value: NewsFeedAction.showURL,
child: Text(NewsLocalizations.of(context).feedShowURL),
),
PopupMenuItem(
value: NewsFeedAction.delete,
child: Text(NewsLocalizations.of(context).actionDelete),
),
PopupMenuItem(
value: NewsFeedAction.rename,
child: Text(NewsLocalizations.of(context).actionRename),
),
if (folders.isNotEmpty) ...[
PopupMenuItem(
value: NewsFeedAction.move,
child: Text(NewsLocalizations.of(context).actionMove),
),
],
],
onSelected: (final action) async {
switch (action) {
case NewsFeedAction.showURL:
await showDialog<void>(
context: context,
builder: (final context) => NewsFeedShowURLDialog(
feed: feed,
),
);
case NewsFeedAction.delete:
if (!context.mounted) {
return;
}
if (await showConfirmationDialog(
context,
NewsLocalizations.of(context).feedRemoveConfirm(feed.title),
)) {
bloc.removeFeed(feed.id);
}
case NewsFeedAction.rename:
if (!context.mounted) {
return;
}
final result = await showRenameDialog(
context: context,
title: NewsLocalizations.of(context).feedRename,
value: feed.title,
);
if (result != null) {
bloc.renameFeed(feed.id, result);
}
case NewsFeedAction.move:
if (!context.mounted) {
return;
}
final result = await showDialog<List<int?>>(
context: context,
builder: (final context) => NewsMoveFeedDialog(
folders: folders,
feed: feed,
),
);
if (result != null) {
bloc.moveFeed(feed.id, result[0]);
}
}
},
),
],
), ),
onLongPress: () { PopupMenuItem(
if (feed.unreadCount! > 0) { value: NewsFeedAction.rename,
bloc.markFeedAsRead(feed.id); child: Text(NewsLocalizations.of(context).actionRename),
} ),
}, if (folders.isNotEmpty)
onTap: () async { PopupMenuItem(
await Navigator.of(context).push( value: NewsFeedAction.move,
MaterialPageRoute<void>( child: Text(NewsLocalizations.of(context).actionMove),
builder: (final context) => NewsFeedPage( ),
bloc: bloc, ],
onSelected: (final action) async {
switch (action) {
case NewsFeedAction.showURL:
await showDialog<void>(
context: context,
builder: (final context) => NewsFeedShowURLDialog(
feed: feed, feed: feed,
), ),
);
case NewsFeedAction.delete:
if (!context.mounted) {
return;
}
if (await showConfirmationDialog(
context,
NewsLocalizations.of(context).feedRemoveConfirm(feed.title),
)) {
bloc.removeFeed(feed.id);
}
case NewsFeedAction.rename:
if (!context.mounted) {
return;
}
final result = await showRenameDialog(
context: context,
title: NewsLocalizations.of(context).feedRename,
value: feed.title,
);
if (result != null) {
bloc.renameFeed(feed.id, result);
}
case NewsFeedAction.move:
if (!context.mounted) {
return;
}
final result = await showDialog<List<int?>>(
context: context,
builder: (final context) => NewsMoveFeedDialog(
folders: folders,
feed: feed,
),
);
if (result != null) {
bloc.moveFeed(feed.id, result[0]);
}
}
},
);
if (feed.updateErrorCount > 0) {
trailing = Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: () async {
await showDialog<void>(
context: context,
builder: (final context) => NewsFeedUpdateErrorDialog(
feed: feed,
),
);
},
tooltip: NewsLocalizations.of(context).feedShowErrorMessage,
iconSize: 30,
icon: Text(
feed.updateErrorCount.toString(),
style: TextStyle(
color: Theme.of(context).colorScheme.error,
),
), ),
); ),
}, trailing,
],
); );
}
return ListTile(
title: Text(
feed.title,
style: feed.unreadCount! == 0
? Theme.of(context).textTheme.titleMedium!.copyWith(color: Theme.of(context).disabledColor)
: null,
),
subtitle: feed.unreadCount! > 0
? Text(NewsLocalizations.of(context).articlesUnread(feed.unreadCount!))
: const SizedBox(),
leading: NewsFeedIcon(feed: feed),
trailing: trailing,
onLongPress: () {
if (feed.unreadCount! > 0) {
bloc.markFeedAsRead(feed.id);
}
},
onTap: () async {
await Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (final context) => NewsFeedPage(
bloc: bloc,
feed: feed,
),
),
);
},
);
}
} }
enum NewsFeedAction { enum NewsFeedAction {

21
packages/neon/neon_notes/lib/dialogs/create_note.dart

@ -53,28 +53,27 @@ class _NotesCreateNoteDialogState extends State<NotesCreateNoteDialog> {
submit(); submit();
}, },
), ),
if (widget.category == null) ...[ if (widget.category == null && notes.hasError)
Center( Center(
child: NeonError( child: NeonError(
notes.error, notes.error,
onRetry: widget.bloc.refresh, onRetry: widget.bloc.refresh,
), ),
), ),
if (widget.category == null)
Center( Center(
child: NeonLinearProgressIndicator( child: NeonLinearProgressIndicator(
visible: notes.isLoading, visible: notes.isLoading,
), ),
), ),
if (notes.hasData) ...[ if (widget.category == null && notes.hasData)
NotesCategorySelect( NotesCategorySelect(
categories: notes.requireData.map((final note) => note.category).toSet().toList(), categories: notes.requireData.map((final note) => note.category).toSet().toList(),
onChanged: (final category) { onChanged: (final category) {
selectedCategory = category; selectedCategory = category;
}, },
onSubmitted: submit, onSubmitted: submit,
), ),
],
],
ElevatedButton( ElevatedButton(
onPressed: submit, onPressed: submit,
child: Text(NotesLocalizations.of(context).noteCreate), child: Text(NotesLocalizations.of(context).noteCreate),

14
packages/neon/neon_notes/lib/dialogs/select_category.dart

@ -36,18 +36,19 @@ class _NotesSelectCategoryDialogState extends State<NotesSelectCategoryDialog> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
Center( if (notes.hasError)
child: NeonError( Center(
notes.error, child: NeonError(
onRetry: widget.bloc.refresh, notes.error,
onRetry: widget.bloc.refresh,
),
), ),
),
Center( Center(
child: NeonLinearProgressIndicator( child: NeonLinearProgressIndicator(
visible: notes.isLoading, visible: notes.isLoading,
), ),
), ),
if (notes.hasData) ...[ if (notes.hasData)
NotesCategorySelect( NotesCategorySelect(
categories: notes.requireData.map((final note) => note.category).toSet().toList(), categories: notes.requireData.map((final note) => note.category).toSet().toList(),
initialValue: widget.initialCategory, initialValue: widget.initialCategory,
@ -56,7 +57,6 @@ class _NotesSelectCategoryDialogState extends State<NotesSelectCategoryDialog> {
}, },
onSubmitted: submit, onSubmitted: submit,
), ),
],
ElevatedButton( ElevatedButton(
onPressed: submit, onPressed: submit,
child: Text(NotesLocalizations.of(context).noteSetCategory), child: Text(NotesLocalizations.of(context).noteSetCategory),

24
packages/neon/neon_notes/lib/widgets/notes_view.dart

@ -45,20 +45,20 @@ class NotesView extends StatelessWidget {
RelativeTime( RelativeTime(
date: DateTime.fromMillisecondsSinceEpoch(note.modified * 1000), date: DateTime.fromMillisecondsSinceEpoch(note.modified * 1000),
), ),
if (note.category.isNotEmpty) ...[ if (note.category.isNotEmpty)
const SizedBox( Padding(
width: 8, padding: const EdgeInsets.only(top: 8),
), child: Icon(
Icon( MdiIcons.tag,
MdiIcons.tag, size: smallIconSize,
size: smallIconSize, color: NotesCategoryColor.compute(note.category),
color: NotesCategoryColor.compute(note.category), ),
), ),
const SizedBox( if (note.category.isNotEmpty)
width: 2, Padding(
padding: const EdgeInsets.only(top: 2),
child: Text(note.category),
), ),
Text(note.category),
],
], ],
), ),
trailing: IconButton( trailing: IconButton(

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

@ -61,15 +61,14 @@ class _NotificationsMainPageState extends State<NotificationsMainPage> {
subtitle: Column( subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (notification.message.isNotEmpty) ...[ if (notification.message.isNotEmpty)
Text( Padding(
notification.message, padding: const EdgeInsets.only(bottom: 5),
overflow: TextOverflow.ellipsis, child: Text(
), notification.message,
const SizedBox( overflow: TextOverflow.ellipsis,
height: 5, ),
), ),
],
RelativeTime( RelativeTime(
date: DateTime.parse(notification.datetime), date: DateTime.parse(notification.datetime),
), ),

Loading…
Cancel
Save