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(
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);
},
),
],
],
),
),

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(
leading: Icon(
MdiIcons.cameraPlus,
@ -94,7 +94,6 @@ class _FilesChooseCreateDialogState extends State<FilesChooseCreateDialog> {
}
},
),
],
ListTile(
leading: Icon(
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,
defaultValue: 4,
values: {
for (var i = 1; i <= 16; i = i * 2) ...{
i: (final _) => i.toString(),
},
for (var i = 1; i <= 16; i = i * 2) i: (final _) => i.toString(),
},
);
@ -80,9 +78,7 @@ class FilesAppSpecificOptions extends NextcloudAppOptions {
label: (final context) => FilesLocalizations.of(context).optionsDownloadQueueParallelism,
defaultValue: 4,
values: {
for (var i = 1; i <= 16; i = i * 2) ...{
i: (final _) => i.toString(),
},
for (var i = 1; i <= 16; i = i * 2) i: (final _) => i.toString(),
},
);
@ -96,9 +92,8 @@ class FilesAppSpecificOptions extends NextcloudAppOptions {
2 * 2024,
6 * 1024,
10 * 1024,
]) ...{
])
_mb(i): (final _) => filesize(_mb(i)),
},
};
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;
@override
Widget build(final BuildContext context) => Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
title: Text(details.name),
),
body: SafeArea(
child: ListView(
primary: true,
children: [
ColoredBox(
color: Theme.of(context).colorScheme.primary,
child: FilePreview(
bloc: bloc,
details: details,
color: Theme.of(context).colorScheme.onPrimary,
size: Size(
MediaQuery.of(context).size.width,
MediaQuery.of(context).size.height / 4,
),
Widget build(final BuildContext context) {
final l10n = FilesLocalizations.of(context);
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
title: Text(details.name),
),
body: SafeArea(
child: ListView(
primary: true,
children: [
ColoredBox(
color: Theme.of(context).colorScheme.primary,
child: FilePreview(
bloc: bloc,
details: details,
color: Theme.of(context).colorScheme.onPrimary,
size: Size(
MediaQuery.of(context).size.width,
MediaQuery.of(context).size.height / 4,
),
),
DataTable(
headingRowHeight: 0,
columns: const [
DataColumn(label: SizedBox()),
DataColumn(label: SizedBox()),
],
rows: [
for (final entry in {
details.isDirectory
? FilesLocalizations.of(context).detailsFolderName
: FilesLocalizations.of(context).detailsFileName: details.name,
FilesLocalizations.of(context).detailsParentFolder:
details.path.length == 1 ? '/' : details.path.sublist(0, details.path.length - 1).join('/'),
if (details.size != null) ...{
details.isDirectory
? FilesLocalizations.of(context).detailsFolderSize
: FilesLocalizations.of(context).detailsFileSize: filesize(details.size, 1),
},
if (details.lastModified != null) ...{
FilesLocalizations.of(context).detailsLastModified:
details.lastModified!.toLocal().toIso8601String(),
},
if (details.isFavorite != null) ...{
FilesLocalizations.of(context).detailsIsFavorite: details.isFavorite!
? FilesLocalizations.of(context).actionYes
: FilesLocalizations.of(context).actionNo,
},
}.entries) ...[
DataRow(
cells: [
DataCell(Text(entry.key)),
DataCell(Text(entry.value)),
],
),
],
],
),
],
),
),
DataTable(
headingRowHeight: 0,
columns: const [
DataColumn(label: SizedBox()),
DataColumn(label: SizedBox()),
],
rows: [
_buildDataRow(
details.isDirectory ? l10n.detailsFolderName : l10n.detailsFileName,
details.name,
),
_buildDataRow(
l10n.detailsParentFolder,
details.path.length == 1 ? '/' : details.path.sublist(0, details.path.length - 1).join('/'),
),
if (details.size != null)
_buildDataRow(
details.isDirectory ? l10n.detailsFolderSize : l10n.detailsFileSize,
filesize(details.size, 1),
),
if (details.lastModified != null)
_buildDataRow(
l10n.detailsLastModified,
details.lastModified!.toLocal().toIso8601String(),
),
if (details.isFavorite != null)
_buildDataRow(
l10n.detailsIsFavorite,
details.isFavorite! ? l10n.actionYes : l10n.actionNo,
),
],
),
],
),
),
);
}
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
Widget build(final BuildContext context) => PopupMenuButton<FilesFileAction>(
itemBuilder: (final context) => [
if (!details.isDirectory && NeonPlatform.instance.canUseSharing) ...[
if (!details.isDirectory && NeonPlatform.instance.canUseSharing)
PopupMenuItem(
value: FilesFileAction.share,
child: Text(FilesLocalizations.of(context).actionShare),
),
],
if (details.isFavorite != null) ...[
if (details.isFavorite != null)
PopupMenuItem(
value: FilesFileAction.toggleFavorite,
child: Text(
@ -134,7 +133,6 @@ class FileActions extends StatelessWidget {
: FilesLocalizations.of(context).addToFavorites,
),
),
],
PopupMenuItem(
value: FilesFileAction.details,
child: Text(FilesLocalizations.of(context).details),
@ -152,12 +150,11 @@ class FileActions extends StatelessWidget {
child: Text(FilesLocalizations.of(context).actionCopy),
),
// TODO: https://github.com/provokateurin/nextcloud-neon/issues/4
if (!details.isDirectory) ...[
if (!details.isDirectory)
PopupMenuItem(
value: FilesFileAction.sync,
child: Text(FilesLocalizations.of(context).actionSync),
),
],
PopupMenuItem(
value: FilesFileAction.delete,
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(
date: details.lastModified!,
),
if (details.size != null && details.size! > 0) ...[
const SizedBox(
width: 10,
if (details.size != null && details.size! > 0)
Padding(
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(

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

@ -71,30 +71,29 @@ class _NewsAddFeedDialogState extends State<NewsAddFeedDialog> {
submit();
},
),
if (widget.folderID == null) ...[
if (widget.folderID == null && folders.hasError)
Center(
child: NeonError(
folders.error,
onRetry: widget.bloc.refresh,
),
),
if (widget.folderID == null)
Center(
child: NeonLinearProgressIndicator(
visible: folders.isLoading,
),
),
if (folders.hasData) ...[
NewsFolderSelect(
folders: folders.requireData,
value: folder,
onChanged: (final f) {
setState(() {
folder = f;
});
},
),
],
],
if (widget.folderID == null && folders.hasData)
NewsFolderSelect(
folders: folders.requireData,
value: folder,
onChanged: (final f) {
setState(() {
folder = f;
});
},
),
ElevatedButton(
onPressed: folders.hasData ? submit : null,
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(
onPressed: () async {
await launchUrlString(
@ -157,6 +157,7 @@ class _NewsArticlePageState extends State<NewsArticlePage> {
tooltip: NewsLocalizations.of(context).articleOpenLink,
icon: const Icon(Icons.open_in_new),
),
if (widget.url != null)
IconButton(
onPressed: () async {
await Share.share(await _getURL());
@ -164,7 +165,6 @@ class _NewsArticlePageState extends State<NewsArticlePage> {
tooltip: NewsLocalizations.of(context).articleShare,
icon: const Icon(Icons.share),
),
],
],
),
body: SafeArea(

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

@ -63,30 +63,20 @@ class _NewsArticlesViewState extends State<NewsArticlesView> {
isExpanded: true,
value: selectedFilterTypeSnapshot.data,
items: [
FilterType.all,
FilterType.unread,
if (widget.bloc.listType == null) ...[
FilterType.starred,
],
].map<DropdownMenuItem<FilterType>>(
(final a) {
late final String label;
switch (a) {
case FilterType.all:
label = NewsLocalizations.of(context).articlesFilterAll;
case FilterType.unread:
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(),
_buildDropdownItem(
FilterType.all,
NewsLocalizations.of(context).articlesFilterAll,
),
_buildDropdownItem(
FilterType.unread,
NewsLocalizations.of(context).articlesFilterUnread,
),
if (widget.bloc.listType == null)
_buildDropdownItem(
FilterType.starred,
NewsLocalizations.of(context).articlesFilterStarred,
),
],
onChanged: (final 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(
final BuildContext context,
final news.Article article,
@ -116,13 +111,12 @@ class _NewsArticlesViewState extends State<NewsArticlesView> {
: Theme.of(context).textTheme.titleMedium!.copyWith(color: Theme.of(context).disabledColor),
),
),
if (article.mediaThumbnail != null) ...[
if (article.mediaThumbnail != null)
NeonUrlImage(
url: article.mediaThumbnail!,
size: const Size(100, 50),
fit: BoxFit.cover,
),
],
],
),
subtitle: Row(

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

@ -42,128 +42,131 @@ class NewsFeedsView extends StatelessWidget {
final BuildContext context,
final news.Feed feed,
final List<news.Folder> folders,
) =>
ListTile(
title: Text(
feed.title,
style: feed.unreadCount! == 0
? Theme.of(context).textTheme.titleMedium!.copyWith(color: Theme.of(context).disabledColor)
: null,
) {
Widget trailing = PopupMenuButton<NewsFeedAction>(
itemBuilder: (final context) => [
PopupMenuItem(
value: NewsFeedAction.showURL,
child: Text(NewsLocalizations.of(context).feedShowURL),
),
subtitle: feed.unreadCount! > 0
? Text(NewsLocalizations.of(context).articlesUnread(feed.unreadCount!))
: const SizedBox(),
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]);
}
}
},
),
],
PopupMenuItem(
value: NewsFeedAction.delete,
child: Text(NewsLocalizations.of(context).actionDelete),
),
onLongPress: () {
if (feed.unreadCount! > 0) {
bloc.markFeedAsRead(feed.id);
}
},
onTap: () async {
await Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (final context) => NewsFeedPage(
bloc: bloc,
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]);
}
}
},
);
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 {

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

@ -53,28 +53,27 @@ class _NotesCreateNoteDialogState extends State<NotesCreateNoteDialog> {
submit();
},
),
if (widget.category == null) ...[
if (widget.category == null && notes.hasError)
Center(
child: NeonError(
notes.error,
onRetry: widget.bloc.refresh,
),
),
if (widget.category == null)
Center(
child: NeonLinearProgressIndicator(
visible: notes.isLoading,
),
),
if (notes.hasData) ...[
NotesCategorySelect(
categories: notes.requireData.map((final note) => note.category).toSet().toList(),
onChanged: (final category) {
selectedCategory = category;
},
onSubmitted: submit,
),
],
],
if (widget.category == null && notes.hasData)
NotesCategorySelect(
categories: notes.requireData.map((final note) => note.category).toSet().toList(),
onChanged: (final category) {
selectedCategory = category;
},
onSubmitted: submit,
),
ElevatedButton(
onPressed: submit,
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(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Center(
child: NeonError(
notes.error,
onRetry: widget.bloc.refresh,
if (notes.hasError)
Center(
child: NeonError(
notes.error,
onRetry: widget.bloc.refresh,
),
),
),
Center(
child: NeonLinearProgressIndicator(
visible: notes.isLoading,
),
),
if (notes.hasData) ...[
if (notes.hasData)
NotesCategorySelect(
categories: notes.requireData.map((final note) => note.category).toSet().toList(),
initialValue: widget.initialCategory,
@ -56,7 +57,6 @@ class _NotesSelectCategoryDialogState extends State<NotesSelectCategoryDialog> {
},
onSubmitted: submit,
),
],
ElevatedButton(
onPressed: submit,
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(
date: DateTime.fromMillisecondsSinceEpoch(note.modified * 1000),
),
if (note.category.isNotEmpty) ...[
const SizedBox(
width: 8,
),
Icon(
MdiIcons.tag,
size: smallIconSize,
color: NotesCategoryColor.compute(note.category),
if (note.category.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Icon(
MdiIcons.tag,
size: smallIconSize,
color: NotesCategoryColor.compute(note.category),
),
),
const SizedBox(
width: 2,
if (note.category.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 2),
child: Text(note.category),
),
Text(note.category),
],
],
),
trailing: IconButton(

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

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

Loading…
Cancel
Save