Browse Source

Merge pull request #886 from nextcloud/refactor/neon

Refactor/neon
pull/979/head
Nikolas Rimikis 1 year ago committed by GitHub
parent
commit
66bfc32c26
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      packages/neon/neon/lib/l10n/en.arb
  2. 6
      packages/neon/neon/lib/l10n/localizations.dart
  3. 3
      packages/neon/neon/lib/l10n/localizations_en.dart
  4. 12
      packages/neon/neon/lib/src/blocs/apps.dart
  5. 10
      packages/neon/neon/lib/src/blocs/push_notifications.dart
  6. 2
      packages/neon/neon/lib/src/blocs/user_statuses.dart
  7. 4
      packages/neon/neon/lib/src/models/app_implementation.dart
  8. 10
      packages/neon/neon/lib/src/settings/models/storage.dart
  9. 4
      packages/neon/neon/lib/src/settings/widgets/account_settings_tile.dart
  10. 5
      packages/neon/neon/lib/src/utils/account_options.dart
  11. 42
      packages/neon/neon/lib/src/utils/global_options.dart
  12. 10
      packages/neon/neon/lib/src/utils/global_popups.dart
  13. 15
      packages/neon/neon/lib/src/utils/user_agent.dart
  14. 45
      packages/neon/neon/lib/src/widgets/account_tile.dart
  15. 16
      packages/neon/neon/lib/src/widgets/app_bar.dart
  16. 11
      packages/neon/neon/lib/src/widgets/app_implementation_icon.dart
  17. 4
      packages/neon/neon/lib/src/widgets/linear_progress_indicator.dart
  18. 14
      packages/neon/neon/lib/src/widgets/nextcloud_logo.dart

7
packages/neon/neon/lib/l10n/en.arb

@ -51,7 +51,7 @@
"errorNoCompatibleNextcloudAppsFound": "No compatible Nextcloud apps could be found.\nWe are working hard to implement more and more apps!", "errorNoCompatibleNextcloudAppsFound": "No compatible Nextcloud apps could be found.\nWe are working hard to implement more and more apps!",
"errorServerInMaintenanceMode": "The server is in maintenance mode. Please try again later or contact the server admin.", "errorServerInMaintenanceMode": "The server is in maintenance mode. Please try again later or contact the server admin.",
"errorMissingPermission": "Permission for {name} is missing", "errorMissingPermission": "Permission for {name} is missing",
"@errorMissingPermission" : { "@errorMissingPermission": {
"placeholders": { "placeholders": {
"name": { "name": {
"type": "String" "type": "String"
@ -59,7 +59,7 @@
} }
}, },
"errorUnsupportedAppVersions": "Sorry, the version of the following apps on your Nextcloud instance are not supported. \n {names} \n Please contact your administrator to resolve the issues.", "errorUnsupportedAppVersions": "Sorry, the version of the following apps on your Nextcloud instance are not supported. \n {names} \n Please contact your administrator to resolve the issues.",
"@errorUnsupportedAppVersions" : { "@errorUnsupportedAppVersions": {
"placeholders": { "placeholders": {
"names": { "names": {
"type": "String" "type": "String"
@ -70,7 +70,7 @@
"errorInvalidURL": "Invalid URL provided", "errorInvalidURL": "Invalid URL provided",
"errorInvalidQRcode": "Invalid QR-Code provided", "errorInvalidQRcode": "Invalid QR-Code provided",
"errorRouteNotFound": "Route not found: {route}", "errorRouteNotFound": "Route not found: {route}",
"@errorRouteNotFound" : { "@errorRouteNotFound": {
"placeholders": { "placeholders": {
"route": { "route": {
"type": "String" "type": "String"
@ -152,7 +152,6 @@
"globalOptionsNavigationMode": "Navigation mode", "globalOptionsNavigationMode": "Navigation mode",
"globalOptionsNavigationModeDrawer": "Drawer", "globalOptionsNavigationModeDrawer": "Drawer",
"globalOptionsNavigationModeDrawerAlwaysVisible": "Drawer always visible", "globalOptionsNavigationModeDrawerAlwaysVisible": "Drawer always visible",
"globalOptionsNavigationModeQuickBar": "Quick bar",
"accountOptionsRemove": "Remove account", "accountOptionsRemove": "Remove account",
"accountOptionsRemoveConfirm": "Are you sure you want to remove the account {id}?", "accountOptionsRemoveConfirm": "Are you sure you want to remove the account {id}?",
"@accountOptionsRemoveConfirm": { "@accountOptionsRemoveConfirm": {

6
packages/neon/neon/lib/l10n/localizations.dart

@ -635,12 +635,6 @@ abstract class AppLocalizations {
/// **'Drawer always visible'** /// **'Drawer always visible'**
String get globalOptionsNavigationModeDrawerAlwaysVisible; String get globalOptionsNavigationModeDrawerAlwaysVisible;
/// No description provided for @globalOptionsNavigationModeQuickBar.
///
/// In en, this message translates to:
/// **'Quick bar'**
String get globalOptionsNavigationModeQuickBar;
/// No description provided for @accountOptionsRemove. /// No description provided for @accountOptionsRemove.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

3
packages/neon/neon/lib/l10n/localizations_en.dart

@ -314,9 +314,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get globalOptionsNavigationModeDrawerAlwaysVisible => 'Drawer always visible'; String get globalOptionsNavigationModeDrawerAlwaysVisible => 'Drawer always visible';
@override
String get globalOptionsNavigationModeQuickBar => 'Quick bar';
@override @override
String get accountOptionsRemove => 'Remove account'; String get accountOptionsRemove => 'Remove account';

12
packages/neon/neon/lib/src/blocs/apps.dart

@ -27,8 +27,6 @@ abstract interface class AppsBlocEvents {
@internal @internal
abstract interface class AppsBlocStates { abstract interface class AppsBlocStates {
BehaviorSubject<Result<List<core.NavigationEntry>>> get apps;
BehaviorSubject<Result<Iterable<AppImplementation>>> get appImplementations; BehaviorSubject<Result<Iterable<AppImplementation>>> get appImplementations;
BehaviorSubject<Result<NotificationsAppInterface?>> get notificationsAppImplementation; BehaviorSubject<Result<NotificationsAppInterface?>> get notificationsAppImplementation;
@ -48,7 +46,7 @@ class AppsBloc extends InteractiveBloc implements AppsBlocEvents, AppsBlocStates
this._account, this._account,
this._allAppImplementations, this._allAppImplementations,
) { ) {
apps.listen((final result) { _apps.listen((final result) {
appImplementations appImplementations
.add(result.transform((final data) => _filteredAppImplementations(data.map((final a) => a.id)))); .add(result.transform((final data) => _filteredAppImplementations(data.map((final a) => a.id))));
}); });
@ -162,10 +160,11 @@ class AppsBloc extends InteractiveBloc implements AppsBlocEvents, AppsBlocStates
final AccountsBloc _accountsBloc; final AccountsBloc _accountsBloc;
final Account _account; final Account _account;
final Iterable<AppImplementation> _allAppImplementations; final Iterable<AppImplementation> _allAppImplementations;
final _apps = BehaviorSubject<Result<List<core.NavigationEntry>>>();
@override @override
void dispose() { void dispose() {
unawaited(apps.close()); unawaited(_apps.close());
unawaited(appImplementations.close()); unawaited(appImplementations.close());
unawaited(notificationsAppImplementation.close()); unawaited(notificationsAppImplementation.close());
unawaited(activeApp.close()); unawaited(activeApp.close());
@ -182,9 +181,6 @@ class AppsBloc extends InteractiveBloc implements AppsBlocEvents, AppsBlocStates
BehaviorSubject<Result<Iterable<AppImplementation<Bloc, NextcloudAppOptions>>>> appImplementations = BehaviorSubject<Result<Iterable<AppImplementation<Bloc, NextcloudAppOptions>>>> appImplementations =
BehaviorSubject(); BehaviorSubject();
@override
BehaviorSubject<Result<List<core.NavigationEntry>>> apps = BehaviorSubject();
@override @override
BehaviorSubject<Result<NotificationsAppInterface?>> notificationsAppImplementation = BehaviorSubject(); BehaviorSubject<Result<NotificationsAppInterface?>> notificationsAppImplementation = BehaviorSubject();
@ -199,7 +195,7 @@ class AppsBloc extends InteractiveBloc implements AppsBlocEvents, AppsBlocStates
await RequestManager.instance.wrapNextcloud( await RequestManager.instance.wrapNextcloud(
_account.id, _account.id,
'apps-apps', 'apps-apps',
apps, _apps,
_account.client.core.navigation.getAppsNavigationRaw(), _account.client.core.navigation.getAppsNavigationRaw(),
(final response) => response.body.ocs.data.toList(), (final response) => response.body.ocs.data.toList(),
); );

10
packages/neon/neon/lib/src/blocs/push_notifications.dart

@ -35,7 +35,7 @@ class PushNotificationsBloc extends Bloc implements PushNotificationsBlocEvents,
} }
final AccountsBloc _accountsBloc; final AccountsBloc _accountsBloc;
late final _storage = const AppStorage(StorageKeys.notifications); late final _storage = const AppStorage(StorageKeys.lastEndpoint);
final GlobalOptions _globalOptions; final GlobalOptions _globalOptions;
StreamSubscription<List<Account>>? _accountsListener; StreamSubscription<List<Account>>? _accountsListener;
@ -46,8 +46,6 @@ class PushNotificationsBloc extends Bloc implements PushNotificationsBlocEvents,
_globalOptions.pushNotificationsEnabled.removeListener(_pushNotificationsEnabledListener); _globalOptions.pushNotificationsEnabled.removeListener(_pushNotificationsEnabledListener);
} }
String _keyLastEndpoint(final Account account) => 'last-endpoint-${account.id}';
Future<void> _pushNotificationsEnabledListener() async { Future<void> _pushNotificationsEnabledListener() async {
if (_globalOptions.pushNotificationsEnabled.value) { if (_globalOptions.pushNotificationsEnabled.value) {
await _setupUnifiedPush(); await _setupUnifiedPush();
@ -72,7 +70,7 @@ class PushNotificationsBloc extends Bloc implements PushNotificationsBlocEvents,
return; return;
} }
if (_storage.getString(_keyLastEndpoint(account)) == endpoint) { if (_storage.getString(account.id) == endpoint) {
debugPrint('Endpoint not changed'); debugPrint('Endpoint not changed');
return; return;
} }
@ -85,7 +83,7 @@ class PushNotificationsBloc extends Bloc implements PushNotificationsBlocEvents,
proxyServer: '$endpoint#', // This is a hack to make the Nextcloud server directly push to the endpoint proxyServer: '$endpoint#', // This is a hack to make the Nextcloud server directly push to the endpoint
); );
await _storage.setString(_keyLastEndpoint(account), endpoint); await _storage.setString(account.id, endpoint);
debugPrint( debugPrint(
'Account $instance registered for push notifications ${json.encode(subscription.body.ocs.data.toJson())}', 'Account $instance registered for push notifications ${json.encode(subscription.body.ocs.data.toJson())}',
@ -117,7 +115,7 @@ class PushNotificationsBloc extends Bloc implements PushNotificationsBlocEvents,
try { try {
await account.client.notifications.push.removeDevice(); await account.client.notifications.push.removeDevice();
await UnifiedPush.unregister(account.id); await UnifiedPush.unregister(account.id);
await _storage.remove(_keyLastEndpoint(account)); await _storage.remove(account.id);
} catch (e) { } catch (e) {
debugPrint('Failed to unregister device: $e'); debugPrint('Failed to unregister device: $e');
} }

2
packages/neon/neon/lib/src/blocs/user_statuses.dart

@ -66,7 +66,7 @@ class UserStatusesBloc extends InteractiveBloc implements UserStatusesBlocEvents
var isAway = false; var isAway = false;
if (NeonPlatform.instance.canUseWindowManager) { if (NeonPlatform.instance.canUseWindowManager) {
final focused = await windowManager.isFocused(); final focused = await windowManager.isFocused();
final visible = await windowManager.isFocused(); final visible = await windowManager.isVisible();
isAway = !focused || !visible; isAway = !focused || !visible;
} }
try { try {

4
packages/neon/neon/lib/src/models/app_implementation.dart

@ -21,11 +21,9 @@ import 'package:vector_graphics/vector_graphics.dart';
@immutable @immutable
abstract class AppImplementation<T extends Bloc, R extends NextcloudAppOptions> implements Disposable { abstract class AppImplementation<T extends Bloc, R extends NextcloudAppOptions> implements Disposable {
AppImplementation();
String get id; String get id;
LocalizationsDelegate<Object> get localizationsDelegate; LocalizationsDelegate<Object> get localizationsDelegate;
List<Locale> get supportedLocales; Iterable<Locale> get supportedLocales;
String nameFromLocalization(final AppLocalizations localizations) => localizations.appImplementationName(id); String nameFromLocalization(final AppLocalizations localizations) => localizations.appImplementationName(id);
String name(final BuildContext context) => nameFromLocalization(AppLocalizations.of(context)); String name(final BuildContext context) => nameFromLocalization(AppLocalizations.of(context));

10
packages/neon/neon/lib/src/settings/models/storage.dart

@ -102,23 +102,23 @@ final class SingleValueStorage {
@internal @internal
final class AppStorage implements SettingsStorage { final class AppStorage implements SettingsStorage {
const AppStorage( const AppStorage(
this.key, [ this.groupKey, [
this.suffix, this.suffix,
]); ]);
final StorageKeys key; final StorageKeys groupKey;
final String? suffix; final String? suffix;
String get id => suffix ?? key.value; String get id => suffix ?? groupKey.value;
@visibleForTesting @visibleForTesting
String formatKey(final String key) { String formatKey(final String key) {
if (suffix != null) { if (suffix != null) {
return '${this.key.value}-$suffix-$key'; return '${groupKey.value}-$suffix-$key';
} }
return '${this.key.value}-$key'; return '${groupKey.value}-$key';
} }
bool containsKey(final String key) => NeonStorage.database.containsKey(formatKey(key)); bool containsKey(final String key) => NeonStorage.database.containsKey(formatKey(key));

4
packages/neon/neon/lib/src/settings/widgets/account_settings_tile.dart

@ -8,21 +8,19 @@ import 'package:neon/src/widgets/account_tile.dart';
class AccountSettingsTile extends SettingsTile { class AccountSettingsTile extends SettingsTile {
const AccountSettingsTile({ const AccountSettingsTile({
required this.account, required this.account,
this.color,
this.trailing, this.trailing,
this.onTap, this.onTap,
super.key, super.key,
}); });
final Account account; final Account account;
final Color? color;
final Widget? trailing; final Widget? trailing;
final GestureTapCallback? onTap; final GestureTapCallback? onTap;
@override @override
Widget build(final BuildContext context) => NeonAccountTile( Widget build(final BuildContext context) => NeonAccountTile(
account: account, account: account,
color: color,
trailing: trailing, trailing: trailing,
onTap: onTap, onTap: onTap,
); );

5
packages/neon/neon/lib/src/utils/account_options.dart

@ -19,10 +19,7 @@ class AccountSpecificOptions extends OptionsCollection {
initialApp.values = { initialApp.values = {
null: (final context) => AppLocalizations.of(context).accountOptionsAutomatic, null: (final context) => AppLocalizations.of(context).accountOptionsAutomatic,
for (final app in result.requireData) ...{ }..addEntries(result.requireData.map((final app) => MapEntry(app.id, app.name)));
app.id: app.name,
},
};
}); });
} }

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

@ -26,7 +26,7 @@ class GlobalOptions extends OptionsCollection {
void _rememberLastUsedAccountListener() { void _rememberLastUsedAccountListener() {
initialAccount.enabled = !rememberLastUsedAccount.value; initialAccount.enabled = !rememberLastUsedAccount.value;
if (rememberLastUsedAccount.value) { if (rememberLastUsedAccount.value) {
initialAccount.value = null; initialAccount.reset();
} else { } else {
// Only override the initial account if there already has been a value, // Only override the initial account if there already has been a value,
// which means it's not the initial emit from rememberLastUsedAccount // which means it's not the initial emit from rememberLastUsedAccount
@ -44,7 +44,7 @@ class GlobalOptions extends OptionsCollection {
pushNotificationsEnabled.value = false; pushNotificationsEnabled.value = false;
} }
} else { } else {
pushNotificationsDistributor.value = null; pushNotificationsDistributor.reset();
} }
} }
@ -91,26 +91,29 @@ class GlobalOptions extends OptionsCollection {
} }
void updateAccounts(final List<Account> accounts) { void updateAccounts(final List<Account> accounts) {
initialAccount.values = { initialAccount.values = Map.fromEntries(
for (final account in accounts) account.id: (final context) => account.humanReadableID, accounts.map(
}; (final account) => MapEntry(account.id, (final context) => account.humanReadableID),
),
);
if (accounts.tryFind(initialAccount.value) == null) { if (!initialAccount.values.containsKey(initialAccount.value)) {
initialAccount.reset(); initialAccount.reset();
} }
} }
Future<void> updateDistributors(final List<String> distributors) async { void updateDistributors(final List<String> distributors) {
pushNotificationsDistributor.values = { pushNotificationsDistributor.values = Map.fromEntries(
for (final distributor in distributors) ...{ distributors.map(
distributor: _distributorsMap[distributor] ?? (final _) => distributor, (final distributor) => MapEntry(distributor, _distributorsMap[distributor] ?? (final _) => distributor),
}, ),
}; );
final allowed = distributors.isNotEmpty; final allowed = pushNotificationsDistributor.values.containsKey(pushNotificationsDistributor.value);
pushNotificationsEnabled.enabled = allowed; pushNotificationsEnabled.enabled = allowed;
if (!allowed) { if (!allowed) {
pushNotificationsEnabled.value = false; pushNotificationsDistributor.reset();
pushNotificationsEnabled.reset();
} }
} }
@ -128,7 +131,7 @@ class GlobalOptions extends OptionsCollection {
late final themeOLEDAsDark = ToggleOption( late final themeOLEDAsDark = ToggleOption(
storage: storage, storage: storage,
key: GlobalOptionKeys.themeOledAsDark, key: GlobalOptionKeys.themeOLEDAsDark,
label: (final context) => AppLocalizations.of(context).globalOptionsThemeOLEDAsDark, label: (final context) => AppLocalizations.of(context).globalOptionsThemeOLEDAsDark,
defaultValue: false, defaultValue: false,
); );
@ -209,20 +212,17 @@ class GlobalOptions extends OptionsCollection {
defaultValue: Platform.isAndroid || Platform.isIOS ? NavigationMode.drawer : NavigationMode.drawerAlwaysVisible, defaultValue: Platform.isAndroid || Platform.isIOS ? NavigationMode.drawer : NavigationMode.drawerAlwaysVisible,
values: { values: {
NavigationMode.drawer: (final context) => AppLocalizations.of(context).globalOptionsNavigationModeDrawer, NavigationMode.drawer: (final context) => AppLocalizations.of(context).globalOptionsNavigationModeDrawer,
if (!Platform.isAndroid && !Platform.isIOS) ...{ if (!Platform.isAndroid && !Platform.isIOS)
NavigationMode.drawerAlwaysVisible: (final context) => NavigationMode.drawerAlwaysVisible: (final context) =>
AppLocalizations.of(context).globalOptionsNavigationModeDrawerAlwaysVisible, AppLocalizations.of(context).globalOptionsNavigationModeDrawerAlwaysVisible,
}, },
// ignore: deprecated_member_use_from_same_package
NavigationMode.quickBar: (final context) => AppLocalizations.of(context).globalOptionsNavigationModeQuickBar,
},
); );
} }
@internal @internal
enum GlobalOptionKeys implements Storable { enum GlobalOptionKeys implements Storable {
themeMode._('theme-mode'), themeMode._('theme-mode'),
themeOledAsDark._('theme-oled-as-dark'), themeOLEDAsDark._('theme-oled-as-dark'),
themeKeepOriginalAccentColor._('theme-keep-original-accent-color'), themeKeepOriginalAccentColor._('theme-keep-original-accent-color'),
pushNotificationsEnabled._('push-notifications-enabled'), pushNotificationsEnabled._('push-notifications-enabled'),
pushNotificationsDistributor._('push-notifications-distributor'), pushNotificationsDistributor._('push-notifications-distributor'),
@ -244,6 +244,4 @@ enum GlobalOptionKeys implements Storable {
enum NavigationMode { enum NavigationMode {
drawer, drawer,
drawerAlwaysVisible, drawerAlwaysVisible,
@Deprecated("The new design won't use this anymore")
quickBar,
} }

10
packages/neon/neon/lib/src/utils/global_popups.dart

@ -34,6 +34,7 @@ class GlobalPopups {
} }
_subscriptions.clear(); _subscriptions.clear();
_registered = false; _registered = false;
instance = null;
} }
void register(final BuildContext context) { void register(final BuildContext context) {
@ -42,12 +43,13 @@ class GlobalPopups {
return; return;
} }
_registered = true;
final globalOptions = NeonProvider.of<GlobalOptions>(context); final globalOptions = NeonProvider.of<GlobalOptions>(context);
final firstLaunchBloc = NeonProvider.of<FirstLaunchBloc>(context); final firstLaunchBloc = NeonProvider.of<FirstLaunchBloc>(context);
final nextPushBloc = NeonProvider.of<NextPushBloc>(context); final nextPushBloc = NeonProvider.of<NextPushBloc>(context);
if (NeonPlatform.instance.canUsePushNotifications) {
_subscriptions.addAll([ _subscriptions.addAll([
if (NeonPlatform.instance.canUsePushNotifications) ...[
firstLaunchBloc.onFirstLaunch.listen((final _) { firstLaunchBloc.onFirstLaunch.listen((final _) {
assert(context.mounted, 'Context should be mounted'); assert(context.mounted, 'Context should be mounted');
if (!globalOptions.pushNotificationsEnabled.enabled) { if (!globalOptions.pushNotificationsEnabled.enabled) {
@ -98,9 +100,7 @@ class GlobalPopups {
), ),
); );
}), }),
],
]); ]);
}
_registered = true;
} }
} }

15
packages/neon/neon/lib/src/utils/user_agent.dart

@ -1,8 +1,11 @@
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
late String? _userAgent; String? _userAgent;
/// Sets the user agent.
///
/// It can be accessed with [neonUserAgent].
@internal @internal
void buildUserAgent(final PackageInfo packageInfo) { void buildUserAgent(final PackageInfo packageInfo) {
var buildNumber = packageInfo.buildNumber; var buildNumber = packageInfo.buildNumber;
@ -12,5 +15,13 @@ void buildUserAgent(final PackageInfo packageInfo) {
_userAgent = 'Neon ${packageInfo.version}+$buildNumber'; _userAgent = 'Neon ${packageInfo.version}+$buildNumber';
} }
/// Gets the current user agent.
///
/// It must be set by calling [buildUserAgent] before. If not set a [StateError] will be thrown.
@internal @internal
String get neonUserAgent => _userAgent!; String get neonUserAgent {
if (_userAgent == null) {
throw StateError('The user agent has not been set up. Please use `buildUserAgent` before.');
}
return _userAgent!;
}

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

@ -1,4 +1,5 @@
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/src/bloc/result_builder.dart'; import 'package:neon/src/bloc/result_builder.dart';
import 'package:neon/src/blocs/accounts.dart'; import 'package:neon/src/blocs/accounts.dart';
@ -13,21 +14,15 @@ import 'package:nextcloud/provisioning_api.dart' as provisioning_api;
class NeonAccountTile extends StatelessWidget { class NeonAccountTile extends StatelessWidget {
const NeonAccountTile({ const NeonAccountTile({
required this.account, required this.account,
this.color,
this.trailing, this.trailing,
this.onTap, this.onTap,
this.textColor,
this.dense = false,
this.showStatus = true, this.showStatus = true,
super.key, super.key,
}); });
final Account account; final Account account;
final Color? color;
final Widget? trailing; final Widget? trailing;
final GestureTapCallback? onTap; final GestureTapCallback? onTap;
final Color? textColor;
final bool dense;
final bool showStatus; final bool showStatus;
@override @override
@ -35,16 +30,7 @@ class NeonAccountTile extends StatelessWidget {
final userDetailsBloc = NeonProvider.of<AccountsBloc>(context).getUserDetailsBlocFor(account); final userDetailsBloc = NeonProvider.of<AccountsBloc>(context).getUserDetailsBlocFor(account);
return ListTile( return ListTile(
textColor: textColor,
onTap: onTap, onTap: onTap,
dense: dense,
contentPadding: dense ? EdgeInsets.zero : null,
visualDensity: dense
? const VisualDensity(
horizontal: -4,
vertical: -4,
)
: null,
leading: NeonUserAvatar( leading: NeonUserAvatar(
account: account, account: account,
showStatus: showStatus, showStatus: showStatus,
@ -54,46 +40,29 @@ class NeonAccountTile extends StatelessWidget {
stream: userDetailsBloc.userDetails, stream: userDetailsBloc.userDetails,
builder: (final context, final userDetails) => Row( builder: (final context, final userDetails) => Row(
children: [ children: [
if (userDetails.hasData) ...[ if (userDetails.hasData)
Flexible( Flexible(
child: Text( child: Text(
userDetails.requireData.displayname, userDetails.requireData.displayname,
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
color: textColor,
),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
], if (userDetails.isLoading)
if (userDetails.isLoading) ...[ const Expanded(
const SizedBox( child: NeonLinearProgressIndicator(),
width: 5,
),
Expanded(
child: NeonLinearProgressIndicator(
color: textColor,
),
),
],
if (userDetails.hasError) ...[
const SizedBox(
width: 5,
), ),
if (userDetails.hasError)
NeonError( NeonError(
userDetails.error, userDetails.error,
onlyIcon: true, onlyIcon: true,
iconSize: 24, iconSize: 24,
onRetry: userDetailsBloc.refresh, onRetry: userDetailsBloc.refresh,
), ),
], ].intersperse(const SizedBox(width: 5)).toList(),
],
), ),
), ),
subtitle: Text( subtitle: Text(
account.humanReadableID, account.humanReadableID,
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: textColor,
),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
); );

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

@ -251,25 +251,19 @@ class _NotificationIconButtonState extends State<NotificationIconButton> {
final notificationsImplementationData = notificationsAppImplementation.data!; final notificationsImplementationData = notificationsAppImplementation.data!;
final notificationBloc = notificationsImplementationData.getBloc(_account); final notificationBloc = notificationsImplementationData.getBloc(_account);
return StreamBuilder<int>(
stream: notificationsImplementationData.getUnreadCounter(notificationBloc),
builder: (final context, final unreadCounterSnapshot) {
final unreadCount = unreadCounterSnapshot.data ?? 0;
return IconButton( return IconButton(
key: Key('app-${notificationsImplementationData.id}'), key: Key('app-${notificationsImplementationData.id}'),
onPressed: () async { onPressed: () async {
await _openNotifications(notificationsImplementationData); await _openNotifications(notificationsImplementationData);
}, },
tooltip: AppLocalizations.of(context).appImplementationName(notificationsImplementationData.id), tooltip: AppLocalizations.of(context).appImplementationName(notificationsImplementationData.id),
icon: NeonAppImplementationIcon( icon: StreamBuilder<int>(
stream: notificationsImplementationData.getUnreadCounter(notificationBloc),
builder: (final context, final unreadCounterSnapshot) => NeonAppImplementationIcon(
appImplementation: notificationsImplementationData, appImplementation: notificationsImplementationData,
unreadCount: unreadCount, unreadCount: unreadCounterSnapshot.data,
color: unreadCount > 0 ),
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onBackground,
), ),
);
},
); );
}, },
); );

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

@ -1,4 +1,4 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/material.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:neon/src/models/app_implementation.dart'; import 'package:neon/src/models/app_implementation.dart';
@ -6,7 +6,7 @@ import 'package:neon/src/models/app_implementation.dart';
class NeonAppImplementationIcon extends StatelessWidget { class NeonAppImplementationIcon extends StatelessWidget {
const NeonAppImplementationIcon({ const NeonAppImplementationIcon({
required this.appImplementation, required this.appImplementation,
this.unreadCount = 0, this.unreadCount,
this.color, this.color,
this.size, this.size,
super.key, super.key,
@ -14,7 +14,7 @@ class NeonAppImplementationIcon extends StatelessWidget {
final AppImplementation appImplementation; final AppImplementation appImplementation;
final int unreadCount; final int? unreadCount;
final Color? color; final Color? color;
@ -22,6 +22,11 @@ class NeonAppImplementationIcon extends StatelessWidget {
@override @override
Widget build(final BuildContext context) { Widget build(final BuildContext context) {
final unreadCount = this.unreadCount ?? 0;
final color = this.color ??
(unreadCount > 0 ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.onBackground);
final icon = Container( final icon = Container(
margin: const EdgeInsets.all(5), margin: const EdgeInsets.all(5),
child: appImplementation.buildIcon( child: appImplementation.buildIcon(

4
packages/neon/neon/lib/src/widgets/linear_progress_indicator.dart

@ -17,14 +17,12 @@ class NeonLinearProgressIndicator extends StatelessWidget {
@override @override
Widget build(final BuildContext context) => Container( Widget build(final BuildContext context) => Container(
margin: margin, margin: margin,
child: SizedBox( constraints: BoxConstraints.loose(const Size.fromHeight(3)),
height: 3,
child: visible child: visible
? LinearProgressIndicator( ? LinearProgressIndicator(
color: color, color: color,
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
) )
: null, : null,
),
); );
} }

14
packages/neon/neon/lib/src/widgets/nextcloud_logo.dart

@ -2,15 +2,25 @@ import 'package:flutter/widgets.dart';
import 'package:neon/l10n/localizations.dart'; import 'package:neon/l10n/localizations.dart';
import 'package:vector_graphics/vector_graphics.dart'; import 'package:vector_graphics/vector_graphics.dart';
/// The Nextcloud logo, in widget form.
///
/// For guidelines on using the Nextcloud logo, visit https://nextcloud.com/trademarks.
class NextcloudLogo extends StatelessWidget { class NextcloudLogo extends StatelessWidget {
/// Creates a widget that shows the Nextcloud logo.
const NextcloudLogo({ const NextcloudLogo({
this.size = 100,
super.key, super.key,
}); });
/// The size of the logo in logical pixels.
///
/// The logo will be fit into a square this size.
final double size;
@override @override
Widget build(final BuildContext context) => VectorGraphic( Widget build(final BuildContext context) => VectorGraphic(
width: 100, width: size,
height: 100, height: size,
loader: const AssetBytesLoader( loader: const AssetBytesLoader(
'assets/logo_nextcloud.svg.vec', 'assets/logo_nextcloud.svg.vec',
packageName: 'neon', packageName: 'neon',

Loading…
Cancel
Save