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. 14
      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. 30
      packages/neon/neon/lib/src/widgets/app_bar.dart
  16. 11
      packages/neon/neon/lib/src/widgets/app_implementation_icon.dart
  17. 16
      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!",
"errorServerInMaintenanceMode": "The server is in maintenance mode. Please try again later or contact the server admin.",
"errorMissingPermission": "Permission for {name} is missing",
"@errorMissingPermission" : {
"@errorMissingPermission": {
"placeholders": {
"name": {
"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" : {
"@errorUnsupportedAppVersions": {
"placeholders": {
"names": {
"type": "String"
@ -70,7 +70,7 @@
"errorInvalidURL": "Invalid URL provided",
"errorInvalidQRcode": "Invalid QR-Code provided",
"errorRouteNotFound": "Route not found: {route}",
"@errorRouteNotFound" : {
"@errorRouteNotFound": {
"placeholders": {
"route": {
"type": "String"
@ -152,7 +152,6 @@
"globalOptionsNavigationMode": "Navigation mode",
"globalOptionsNavigationModeDrawer": "Drawer",
"globalOptionsNavigationModeDrawerAlwaysVisible": "Drawer always visible",
"globalOptionsNavigationModeQuickBar": "Quick bar",
"accountOptionsRemove": "Remove account",
"accountOptionsRemoveConfirm": "Are you sure you want to remove the account {id}?",
"@accountOptionsRemoveConfirm": {

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

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

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

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

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

@ -27,8 +27,6 @@ abstract interface class AppsBlocEvents {
@internal
abstract interface class AppsBlocStates {
BehaviorSubject<Result<List<core.NavigationEntry>>> get apps;
BehaviorSubject<Result<Iterable<AppImplementation>>> get appImplementations;
BehaviorSubject<Result<NotificationsAppInterface?>> get notificationsAppImplementation;
@ -48,7 +46,7 @@ class AppsBloc extends InteractiveBloc implements AppsBlocEvents, AppsBlocStates
this._account,
this._allAppImplementations,
) {
apps.listen((final result) {
_apps.listen((final result) {
appImplementations
.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 Account _account;
final Iterable<AppImplementation> _allAppImplementations;
final _apps = BehaviorSubject<Result<List<core.NavigationEntry>>>();
@override
void dispose() {
unawaited(apps.close());
unawaited(_apps.close());
unawaited(appImplementations.close());
unawaited(notificationsAppImplementation.close());
unawaited(activeApp.close());
@ -182,9 +181,6 @@ class AppsBloc extends InteractiveBloc implements AppsBlocEvents, AppsBlocStates
BehaviorSubject<Result<Iterable<AppImplementation<Bloc, NextcloudAppOptions>>>> appImplementations =
BehaviorSubject();
@override
BehaviorSubject<Result<List<core.NavigationEntry>>> apps = BehaviorSubject();
@override
BehaviorSubject<Result<NotificationsAppInterface?>> notificationsAppImplementation = BehaviorSubject();
@ -199,7 +195,7 @@ class AppsBloc extends InteractiveBloc implements AppsBlocEvents, AppsBlocStates
await RequestManager.instance.wrapNextcloud(
_account.id,
'apps-apps',
apps,
_apps,
_account.client.core.navigation.getAppsNavigationRaw(),
(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;
late final _storage = const AppStorage(StorageKeys.notifications);
late final _storage = const AppStorage(StorageKeys.lastEndpoint);
final GlobalOptions _globalOptions;
StreamSubscription<List<Account>>? _accountsListener;
@ -46,8 +46,6 @@ class PushNotificationsBloc extends Bloc implements PushNotificationsBlocEvents,
_globalOptions.pushNotificationsEnabled.removeListener(_pushNotificationsEnabledListener);
}
String _keyLastEndpoint(final Account account) => 'last-endpoint-${account.id}';
Future<void> _pushNotificationsEnabledListener() async {
if (_globalOptions.pushNotificationsEnabled.value) {
await _setupUnifiedPush();
@ -72,7 +70,7 @@ class PushNotificationsBloc extends Bloc implements PushNotificationsBlocEvents,
return;
}
if (_storage.getString(_keyLastEndpoint(account)) == endpoint) {
if (_storage.getString(account.id) == endpoint) {
debugPrint('Endpoint not changed');
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
);
await _storage.setString(_keyLastEndpoint(account), endpoint);
await _storage.setString(account.id, endpoint);
debugPrint(
'Account $instance registered for push notifications ${json.encode(subscription.body.ocs.data.toJson())}',
@ -117,7 +115,7 @@ class PushNotificationsBloc extends Bloc implements PushNotificationsBlocEvents,
try {
await account.client.notifications.push.removeDevice();
await UnifiedPush.unregister(account.id);
await _storage.remove(_keyLastEndpoint(account));
await _storage.remove(account.id);
} catch (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;
if (NeonPlatform.instance.canUseWindowManager) {
final focused = await windowManager.isFocused();
final visible = await windowManager.isFocused();
final visible = await windowManager.isVisible();
isAway = !focused || !visible;
}
try {

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

@ -21,11 +21,9 @@ import 'package:vector_graphics/vector_graphics.dart';
@immutable
abstract class AppImplementation<T extends Bloc, R extends NextcloudAppOptions> implements Disposable {
AppImplementation();
String get id;
LocalizationsDelegate<Object> get localizationsDelegate;
List<Locale> get supportedLocales;
Iterable<Locale> get supportedLocales;
String nameFromLocalization(final AppLocalizations localizations) => localizations.appImplementationName(id);
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
final class AppStorage implements SettingsStorage {
const AppStorage(
this.key, [
this.groupKey, [
this.suffix,
]);
final StorageKeys key;
final StorageKeys groupKey;
final String? suffix;
String get id => suffix ?? key.value;
String get id => suffix ?? groupKey.value;
@visibleForTesting
String formatKey(final String key) {
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));

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 {
const AccountSettingsTile({
required this.account,
this.color,
this.trailing,
this.onTap,
super.key,
});
final Account account;
final Color? color;
final Widget? trailing;
final GestureTapCallback? onTap;
@override
Widget build(final BuildContext context) => NeonAccountTile(
account: account,
color: color,
trailing: trailing,
onTap: onTap,
);

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

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

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

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

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

@ -34,6 +34,7 @@ class GlobalPopups {
}
_subscriptions.clear();
_registered = false;
instance = null;
}
void register(final BuildContext context) {
@ -42,12 +43,13 @@ class GlobalPopups {
return;
}
_registered = true;
final globalOptions = NeonProvider.of<GlobalOptions>(context);
final firstLaunchBloc = NeonProvider.of<FirstLaunchBloc>(context);
final nextPushBloc = NeonProvider.of<NextPushBloc>(context);
_subscriptions.addAll([
if (NeonPlatform.instance.canUsePushNotifications) ...[
if (NeonPlatform.instance.canUsePushNotifications) {
_subscriptions.addAll([
firstLaunchBloc.onFirstLaunch.listen((final _) {
assert(context.mounted, 'Context should be mounted');
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:package_info_plus/package_info_plus.dart';
late String? _userAgent;
String? _userAgent;
/// Sets the user agent.
///
/// It can be accessed with [neonUserAgent].
@internal
void buildUserAgent(final PackageInfo packageInfo) {
var buildNumber = packageInfo.buildNumber;
@ -12,5 +15,13 @@ void buildUserAgent(final PackageInfo packageInfo) {
_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
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:intersperse/intersperse.dart';
import 'package:meta/meta.dart';
import 'package:neon/src/bloc/result_builder.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 {
const NeonAccountTile({
required this.account,
this.color,
this.trailing,
this.onTap,
this.textColor,
this.dense = false,
this.showStatus = true,
super.key,
});
final Account account;
final Color? color;
final Widget? trailing;
final GestureTapCallback? onTap;
final Color? textColor;
final bool dense;
final bool showStatus;
@override
@ -35,16 +30,7 @@ class NeonAccountTile extends StatelessWidget {
final userDetailsBloc = NeonProvider.of<AccountsBloc>(context).getUserDetailsBlocFor(account);
return ListTile(
textColor: textColor,
onTap: onTap,
dense: dense,
contentPadding: dense ? EdgeInsets.zero : null,
visualDensity: dense
? const VisualDensity(
horizontal: -4,
vertical: -4,
)
: null,
leading: NeonUserAvatar(
account: account,
showStatus: showStatus,
@ -54,46 +40,29 @@ class NeonAccountTile extends StatelessWidget {
stream: userDetailsBloc.userDetails,
builder: (final context, final userDetails) => Row(
children: [
if (userDetails.hasData) ...[
if (userDetails.hasData)
Flexible(
child: Text(
userDetails.requireData.displayname,
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
color: textColor,
),
overflow: TextOverflow.ellipsis,
),
),
],
if (userDetails.isLoading) ...[
const SizedBox(
width: 5,
),
Expanded(
child: NeonLinearProgressIndicator(
color: textColor,
),
),
],
if (userDetails.hasError) ...[
const SizedBox(
width: 5,
if (userDetails.isLoading)
const Expanded(
child: NeonLinearProgressIndicator(),
),
if (userDetails.hasError)
NeonError(
userDetails.error,
onlyIcon: true,
iconSize: 24,
onRetry: userDetailsBloc.refresh,
),
],
],
].intersperse(const SizedBox(width: 5)).toList(),
),
),
subtitle: Text(
account.humanReadableID,
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: textColor,
),
overflow: TextOverflow.ellipsis,
),
);

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

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

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:neon/src/models/app_implementation.dart';
@ -6,7 +6,7 @@ import 'package:neon/src/models/app_implementation.dart';
class NeonAppImplementationIcon extends StatelessWidget {
const NeonAppImplementationIcon({
required this.appImplementation,
this.unreadCount = 0,
this.unreadCount,
this.color,
this.size,
super.key,
@ -14,7 +14,7 @@ class NeonAppImplementationIcon extends StatelessWidget {
final AppImplementation appImplementation;
final int unreadCount;
final int? unreadCount;
final Color? color;
@ -22,6 +22,11 @@ class NeonAppImplementationIcon extends StatelessWidget {
@override
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(
margin: const EdgeInsets.all(5),
child: appImplementation.buildIcon(

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

@ -17,14 +17,12 @@ class NeonLinearProgressIndicator extends StatelessWidget {
@override
Widget build(final BuildContext context) => Container(
margin: margin,
child: SizedBox(
height: 3,
child: visible
? LinearProgressIndicator(
color: color,
backgroundColor: backgroundColor,
)
: null,
),
constraints: BoxConstraints.loose(const Size.fromHeight(3)),
child: visible
? LinearProgressIndicator(
color: color,
backgroundColor: backgroundColor,
)
: 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: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 {
/// Creates a widget that shows the Nextcloud logo.
const NextcloudLogo({
this.size = 100,
super.key,
});
/// The size of the logo in logical pixels.
///
/// The logo will be fit into a square this size.
final double size;
@override
Widget build(final BuildContext context) => VectorGraphic(
width: 100,
height: 100,
width: size,
height: size,
loader: const AssetBytesLoader(
'assets/logo_nextcloud.svg.vec',
packageName: 'neon',

Loading…
Cancel
Save