Browse Source

Merge pull request #100 from jld3103/refactor/tray-quickactions-pushnotifications

Refactor tray, quick actions and push notifications
pull/101/head
jld3103 2 years ago committed by GitHub
parent
commit
f4e9078aa5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 221
      packages/neon/lib/src/app.dart
  2. 2
      packages/neon/lib/src/apps/notifications/app.dart
  3. 7
      packages/neon/lib/src/apps/notifications/pages/main.dart
  4. 55
      packages/neon/lib/src/blocs/accounts.dart
  5. 48
      packages/neon/lib/src/blocs/apps.dart
  6. 12
      packages/neon/lib/src/models/account.dart
  7. 20
      packages/neon/lib/src/models/push_notification_with_account.dart
  8. 18
      packages/neon/lib/src/models/push_notification_with_account.g.dart
  9. 3
      packages/neon/lib/src/neon.dart
  10. 201
      packages/neon/lib/src/pages/home/home.dart
  11. 5
      packages/neon/lib/src/utils/global.dart
  12. 7
      packages/neon/lib/src/utils/localizations.dart
  13. 21
      packages/neon/lib/src/utils/push_utils.dart
  14. 20
      packages/nextcloud/lib/src/nextcloud.openapi.dart
  15. 10
      packages/nextcloud/lib/src/nextcloud.openapi.g.dart
  16. 7
      packages/nextcloud/lib/src/nextcloud.openapi.json
  17. 7
      specs/notifications.json
  18. 2
      tool/send-push-notification-test.sh

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

@ -21,12 +21,16 @@ class NeonApp extends StatefulWidget {
}
// ignore: prefer_mixin
class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver {
class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver, tray.TrayListener, WindowListener {
final _appRegex = RegExp(r'^app_([a-z]+)$', multiLine: true);
final _navigatorKey = GlobalKey<NavigatorState>();
CoreServerCapabilities_Ocs_Data_Capabilities_Theming? _userTheme;
final _platformBrightness = BehaviorSubject<Brightness>.seeded(
WidgetsBinding.instance.window.platformBrightness,
);
late NeonPlatform _platform;
late GlobalOptions _globalOptions;
late AccountsBloc _accountsBloc;
CoreServerCapabilities_Ocs_Data_Capabilities_Theming? _nextcloudTheme;
final _platformBrightness = BehaviorSubject<Brightness>.seeded(WidgetsBinding.instance.window.platformBrightness);
Rect? _lastBounds;
@override
void didChangePlatformBrightness() {
@ -39,9 +43,19 @@ class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver {
void initState() {
super.initState();
_platform = Provider.of<NeonPlatform>(context, listen: false);
_globalOptions = Provider.of<GlobalOptions>(context, listen: false);
_accountsBloc = RxBlocProvider.of<AccountsBloc>(context);
WidgetsBinding.instance.addObserver(this);
if (_platform.canUseSystemTray) {
tray.trayManager.addListener(this);
}
if (_platform.canUseWindowManager) {
windowManager.addListener(this);
}
WidgetsBinding.instance.addPostFrameCallback((final _) {
WidgetsBinding.instance.addPostFrameCallback((final _) async {
widget.accountsBloc.activeAccount.listen((final activeAccount) async {
FlutterNativeSplash.remove();
@ -58,9 +72,9 @@ class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver {
);
Widget builder(final context) => HomePage(
account: activeAccount,
onThemeChanged: (final theme) {
onThemeChanged: (final nextcloudTheme) {
setState(() {
_userTheme = theme;
_nextcloudTheme = nextcloudTheme;
});
},
);
@ -78,12 +92,201 @@ class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver {
);
}
});
final localizations = await appLocalizationsFromSystem();
if (!mounted) {
return;
}
final appImplementations = Provider.of<List<AppImplementation>>(context, listen: false);
if (_platform.canUseQuickActions) {
const quickActions = QuickActions();
await quickActions.setShortcutItems(
appImplementations
.map(
(final app) => ShortcutItem(
type: 'app_${app.id}',
localizedTitle: app.nameFromLocalization(localizations),
icon: 'app_${app.id}',
),
)
.toList(),
);
await quickActions.initialize(_handleShortcut);
}
if (_platform.canUseWindowManager) {
await windowManager.setPreventClose(true);
if (_globalOptions.startupMinimized.value) {
await _saveAndMinimizeWindow();
}
}
if (_platform.canUseSystemTray) {
_globalOptions.systemTrayEnabled.stream.listen((final enabled) async {
if (enabled) {
// TODO: This works on Linux, but maybe not on macOS or Windows
await tray.trayManager.setIcon('assets/logo_neon.svg');
if (mounted) {
await tray.trayManager.setContextMenu(
tray.Menu(
items: [
for (final app in appImplementations) ...[
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',
label: localizations.showSlashHide,
),
tray.MenuItem(
key: 'exit',
label: localizations.exit,
),
],
),
);
}
} else {
await tray.trayManager.destroy();
}
});
}
if (_platform.canUsePushNotifications) {
final localNotificationsPlugin = await PushUtils.initLocalNotifications();
Global.onPushNotificationReceived = (final accountID) async {
final account = RxBlocProvider.of<AccountsBloc>(context).accounts.value.find(accountID);
if (account == null) {
return;
}
final appImplementation = Provider.of<List<AppImplementation>>(context, listen: false)
.singleWhere((final a) => a.id == 'notifications');
_accountsBloc.getAppsBloc(account).getAppBloc<NotificationsBloc>(appImplementation).refresh();
};
Global.onPushNotificationClicked = (final pushNotificationWithAccountID) async {
final allAppImplementations = Provider.of<List<AppImplementation>>(context, listen: false);
final matchingAppImplementations =
allAppImplementations.where((final a) => a.id == pushNotificationWithAccountID.notification.subject.app);
late AppImplementation appImplementation;
if (matchingAppImplementations.isNotEmpty) {
appImplementation = matchingAppImplementations.single;
} else {
appImplementation = allAppImplementations.singleWhere((final a) => a.id == 'notifications');
}
final account =
RxBlocProvider.of<AccountsBloc>(context).accounts.value.find(pushNotificationWithAccountID.accountID);
if (account == null) {
return;
}
_accountsBloc.setActiveAccount(account);
if (appImplementation.id != 'notifications') {
_accountsBloc
.getAppsBloc(account)
.getAppBloc<NotificationsBloc>(appImplementation)
.deleteNotification(pushNotificationWithAccountID.notification.subject.nid!);
}
await _openAppFromExternal(account, appImplementation.id);
};
final details = await localNotificationsPlugin.getNotificationAppLaunchDetails();
if (details != null && details.didNotificationLaunchApp && details.notificationResponse?.payload != null) {
await Global.onPushNotificationClicked!(
PushNotificationWithAccountID.fromJson(
json.decode(details.notificationResponse!.payload!) as Map<String, dynamic>,
),
);
}
}
});
}
@override
void onTrayMenuItemClick(final tray.MenuItem menuItem) {
if (menuItem.key != null) {
unawaited(_handleShortcut(menuItem.key!));
}
}
@override
Future onWindowClose() async {
if (_globalOptions.startupMinimizeInsteadOfExit.value) {
await _saveAndMinimizeWindow();
} else {
await windowManager.destroy();
}
}
Future _handleShortcut(final String shortcutType) async {
if (shortcutType == 'show_hide') {
if (_platform.canUseWindowManager) {
if (await windowManager.isVisible()) {
await _saveAndMinimizeWindow();
} else {
await _showAndRestoreWindow();
}
}
return;
}
if (shortcutType == 'exit') {
exit(0);
}
final matches = _appRegex.allMatches(shortcutType).toList();
if (matches.isNotEmpty) {
final activeAccount = _accountsBloc.activeAccount.valueOrNull;
if (activeAccount == null) {
return;
}
await _openAppFromExternal(activeAccount, matches[0].group(1)!);
}
}
Future _openAppFromExternal(final Account account, final String id) async {
_accountsBloc.getAppsBloc(account).setActiveApp(id);
_navigatorKey.currentState!.popUntil((final route) => route.settings.name == 'home');
await _showAndRestoreWindow();
}
Future _saveAndMinimizeWindow() async {
_lastBounds = await windowManager.getBounds();
if (_globalOptions.systemTrayEnabled.value && _globalOptions.systemTrayHideToTrayWhenMinimized.value) {
await windowManager.hide();
} else {
await windowManager.minimize();
}
}
Future _showAndRestoreWindow() async {
if (!_platform.canUseWindowManager) {
return;
}
final wasVisible = await windowManager.isVisible();
await windowManager.show();
await windowManager.focus();
if (_lastBounds != null && !wasVisible) {
await windowManager.setBounds(_lastBounds);
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
if (_platform.canUseSystemTray) {
tray.trayManager.removeListener(this);
}
if (_platform.canUseWindowManager) {
windowManager.removeListener(this);
}
unawaited(_platformBrightness.close());
super.dispose();
@ -110,7 +313,7 @@ class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver {
supportedLocales: AppLocalizations.supportedLocales,
navigatorKey: _navigatorKey,
theme: getThemeFromNextcloudTheme(
_userTheme,
_nextcloudTheme,
themeMode,
platformBrightnessSnapshot.data!,
oledAsDark: themeOLEDAsDark,

2
packages/neon/lib/src/apps/notifications/app.dart

@ -1,9 +1,11 @@
library notifications;
import 'package:flutter/material.dart';
import 'package:flutter_rx_bloc/flutter_rx_bloc.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:neon/l10n/localizations.dart';
import 'package:neon/src/apps/notifications/blocs/notifications.dart';
import 'package:neon/src/blocs/accounts.dart';
import 'package:neon/src/blocs/apps.dart';
import 'package:neon/src/neon.dart';
import 'package:nextcloud/nextcloud.dart';

7
packages/neon/lib/src/apps/notifications/pages/main.dart

@ -103,7 +103,12 @@ class _NotificationsMainPageState extends State<NotificationsMainPage> {
),
),
onTap: () async {
if (!(await Global.handleNotificationOpening!(notification))) {
final allAppImplementations = Provider.of<List<AppImplementation>>(context, listen: false);
final matchingAppImplementations = allAppImplementations.where((final a) => a.id == notification.app);
if (matchingAppImplementations.isNotEmpty) {
final accountsBloc = RxBlocProvider.of<AccountsBloc>(context);
accountsBloc.getAppsBloc(accountsBloc.activeAccount.value!).setActiveApp(notification.app);
} else {
await showDialog(
context: context,
builder: (final context) => AlertDialog(

55
packages/neon/lib/src/blocs/accounts.dart

@ -129,7 +129,8 @@ class AccountsBloc extends $AccountsBloc {
if (_accountsAppsBlocs[account.id] != null) {
return _accountsAppsBlocs[account.id]!;
}
return AppsBloc(
return _accountsAppsBlocs[account.id] = AppsBloc(
_requestManager,
this,
account,
@ -137,33 +138,15 @@ class AccountsBloc extends $AccountsBloc {
);
}
final RequestManager _requestManager;
final Storage _storage;
final SharedPreferences _sharedPreferences;
final GlobalOptions _globalOptions;
final List<AppImplementation> _allAppImplementations;
final PackageInfo _packageInfo;
final _keyAccounts = 'accounts';
final _keyLastUsedAccount = 'last-used-account';
final _accountsOptions = <String, AccountSpecificOptions>{};
final _accountsAppsBlocs = <String, AppsBloc>{};
late final _activeAccountSubject = BehaviorSubject<Account?>.seeded(null);
late final _accountsSubject = BehaviorSubject<List<Account>>.seeded([]);
String? pushNotificationApp;
final Map<Account, UserDetailsBloc> _userDetailsBlocs = {};
final Map<Account, UserStatusBloc> _userStatusBlocs = {};
UserDetailsBloc getUserDetailsBloc(final Account account) {
if (_userDetailsBlocs[account] != null) {
return _userDetailsBlocs[account]!;
}
final bloc = UserDetailsBloc(_requestManager, account.client);
_userDetailsBlocs[account] = bloc;
return bloc;
return _userDetailsBlocs[account.id] = UserDetailsBloc(
_requestManager,
account.client,
);
}
UserStatusBloc getUserStatusBloc(final Account account) {
@ -171,12 +154,30 @@ class AccountsBloc extends $AccountsBloc {
return _userStatusBlocs[account]!;
}
final bloc = UserStatusBloc(_requestManager, account, _activeAccountSubject);
_userStatusBlocs[account] = bloc;
return bloc;
return _userStatusBlocs[account.id] = UserStatusBloc(
_requestManager,
account,
_activeAccountSubject,
);
}
final RequestManager _requestManager;
final Storage _storage;
final SharedPreferences _sharedPreferences;
final GlobalOptions _globalOptions;
final List<AppImplementation> _allAppImplementations;
final PackageInfo _packageInfo;
final _keyAccounts = 'accounts';
final _keyLastUsedAccount = 'last-used-account';
final _accountsOptions = <String, AccountSpecificOptions>{};
late final _activeAccountSubject = BehaviorSubject<Account?>.seeded(null);
late final _accountsSubject = BehaviorSubject<List<Account>>.seeded([]);
final _accountsAppsBlocs = <String, AppsBloc>{};
final _userDetailsBlocs = <String, UserDetailsBloc>{};
final _userStatusBlocs = <String, UserStatusBloc>{};
@override
void dispose() {
unawaited(_activeAccountSubject.close());

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

@ -64,28 +64,23 @@ class AppsBloc extends $AppsBloc {
result.data != null ? _filteredAppImplementations(result.data!) : <AppImplementation>[];
if (result.data != null) {
if (_accountsBloc.pushNotificationApp != null) {
setActiveApp(_accountsBloc.pushNotificationApp);
_accountsBloc.pushNotificationApp = null;
} else {
final options = _accountsBloc.getOptions(_account);
unawaited(
options.initialApp.stream.first.then((var initialApp) {
if (initialApp == null) {
if (appImplementations.where((final a) => a.id == 'files').isNotEmpty) {
initialApp = 'files';
} else if (appImplementations.isNotEmpty) {
// This should never happen, because the files app is always installed and can not be removed, but just in
// case this changes at a later point.
initialApp = appImplementations[0].id;
}
final options = _accountsBloc.getOptions(_account);
unawaited(
options.initialApp.stream.first.then((var initialApp) async {
if (initialApp == null) {
if (appImplementations.where((final a) => a.id == 'files').isNotEmpty) {
initialApp = 'files';
} else if (appImplementations.isNotEmpty) {
// This should never happen, because the files app is always installed and can not be removed, but just in
// case this changes at a later point.
initialApp = appImplementations[0].id;
}
if (!_activeAppSubject.hasValue) {
setActiveApp(initialApp);
}
}),
);
}
}
if (!_activeAppSubject.hasValue) {
setActiveApp(initialApp);
}
}),
);
}
});
@ -120,17 +115,14 @@ class AppsBloc extends $AppsBloc {
final _appImplementationsSubject = BehaviorSubject<Result<List<AppImplementation>>>();
late final _activeAppSubject = BehaviorSubject<String?>();
final Map<AppImplementation, RxBlocBase> _blocs = {};
final Map<String, RxBlocBase> _blocs = {};
T getAppBloc<T extends RxBlocBase>(final AppImplementation appImplementation) {
if (_blocs[appImplementation] != null) {
return _blocs[appImplementation]! as T;
if (_blocs[appImplementation.id] != null) {
return _blocs[appImplementation.id]! as T;
}
final bloc = appImplementation.buildBloc(_account.client);
_blocs[appImplementation] = bloc;
return bloc as T;
return _blocs[appImplementation.id] = appImplementation.buildBloc(_account.client) as T;
}
@override

12
packages/neon/lib/src/models/account.dart

@ -81,3 +81,15 @@ extension NextcloudClientHelpers on NextcloudClient {
return '${username!}@${uri.port != 443 ? '${uri.host}:${uri.port}' : uri.host}';
}
}
extension AccountFind on List<Account> {
Account? find(final String accountID) {
for (final account in this) {
if (account.id == accountID) {
return account;
}
}
return null;
}
}

20
packages/neon/lib/src/models/push_notification_with_account.dart

@ -0,0 +1,20 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:nextcloud/nextcloud.dart';
part 'push_notification_with_account.g.dart';
@JsonSerializable()
class PushNotificationWithAccountID {
PushNotificationWithAccountID({
required this.notification,
required this.accountID,
});
factory PushNotificationWithAccountID.fromJson(final Map<String, dynamic> json) =>
_$PushNotificationWithAccountIDFromJson(json);
Map<String, dynamic> toJson() => _$PushNotificationWithAccountIDToJson(this);
final NotificationsPushNotification notification;
final String accountID;
}

18
packages/neon/lib/src/models/push_notification_with_account.g.dart

@ -0,0 +1,18 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'push_notification_with_account.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
PushNotificationWithAccountID _$PushNotificationWithAccountIDFromJson(Map<String, dynamic> json) =>
PushNotificationWithAccountID(
notification: NotificationsPushNotification.fromJson(json['notification'] as Map<String, dynamic>),
accountID: json['accountID'] as String,
);
Map<String, dynamic> _$PushNotificationWithAccountIDToJson(PushNotificationWithAccountID instance) => <String, dynamic>{
'notification': instance.notification,
'accountID': instance.accountID,
};

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

@ -9,6 +9,7 @@ import 'package:collection/collection.dart';
import 'package:crypto/crypto.dart';
import 'package:file_picker/file_picker.dart';
import 'package:filesize/filesize.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
@ -33,6 +34,7 @@ import 'package:neon/src/blocs/login.dart';
import 'package:neon/src/blocs/user_details.dart';
import 'package:neon/src/blocs/user_status.dart';
import 'package:neon/src/models/account.dart';
import 'package:neon/src/models/push_notification_with_account.dart';
import 'package:nextcloud/nextcloud.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path/path.dart' as p;
@ -73,6 +75,7 @@ part 'utils/env.dart';
part 'utils/global.dart';
part 'utils/global_options.dart';
part 'utils/hex_color.dart';
part 'utils/localizations.dart';
part 'utils/missing_permission_exception.dart';
part 'utils/nextcloud_app_specific_options.dart';
part 'utils/push_utils.dart';

201
packages/neon/lib/src/pages/home/home.dart

@ -17,33 +17,19 @@ class HomePage extends StatefulWidget {
}
// ignore: prefer_mixin
class _HomePageState extends State<HomePage> with tray.TrayListener, WindowListener {
final _appRegex = RegExp(r'^app_([a-z]+)$', multiLine: true);
class _HomePageState extends State<HomePage> {
final _scaffoldKey = GlobalKey<ScaffoldState>();
late NeonPlatform _platform;
late GlobalOptions _globalOptions;
late CapabilitiesBloc _capabilitiesBloc;
late AppsBloc _appsBloc;
Rect? _lastBounds;
@override
void initState() {
super.initState();
_platform = Provider.of<NeonPlatform>(context, listen: false);
_globalOptions = Provider.of<GlobalOptions>(context, listen: false);
final accountsBloc = RxBlocProvider.of<AccountsBloc>(context);
_appsBloc = accountsBloc.getAppsBloc(widget.account);
if (_platform.canUseSystemTray) {
tray.trayManager.addListener(this);
}
if (_platform.canUseWindowManager) {
windowManager.addListener(this);
}
_appsBloc = RxBlocProvider.of<AccountsBloc>(context).getAppsBloc(widget.account);
_capabilitiesBloc = CapabilitiesBloc(
Provider.of<RequestManager>(context, listen: false),
@ -95,180 +81,6 @@ class _HomePageState extends State<HomePage> with tray.TrayListener, WindowListe
}
}
});
WidgetsBinding.instance.addPostFrameCallback((final _) async {
final appImplementations = Provider.of<List<AppImplementation>>(context, listen: false);
if (_platform.canUseQuickActions) {
const quickActions = QuickActions();
await quickActions.setShortcutItems(
appImplementations
.map(
(final app) => ShortcutItem(
type: 'app_${app.id}',
localizedTitle: app.name(context),
icon: 'app_${app.id}',
),
)
.toList(),
);
await quickActions.initialize(_handleShortcut);
}
if (_platform.canUseWindowManager) {
await windowManager.setPreventClose(true);
if (_globalOptions.startupMinimized.value) {
await _saveAndMinimizeWindow();
}
}
if (_platform.canUseSystemTray) {
_globalOptions.systemTrayEnabled.stream.listen((final enabled) async {
if (enabled) {
// TODO: This works on Linux, but maybe not on macOS or Windows
await tray.trayManager.setIcon('assets/logo_neon.svg');
if (mounted) {
await tray.trayManager.setContextMenu(
tray.Menu(
items: [
for (final app in appImplementations) ...[
tray.MenuItem(
key: 'app_${app.id}',
label: app.name(context),
// TODO: Add icons which should work on macOS and Windows
),
],
tray.MenuItem.separator(),
tray.MenuItem(
key: 'show_hide',
label: AppLocalizations.of(context).showSlashHide,
),
tray.MenuItem(
key: 'exit',
label: AppLocalizations.of(context).exit,
),
],
),
);
}
} else {
await tray.trayManager.destroy();
}
});
}
Global.handleNotificationOpening = (final notification) async {
final allAppImplementations = Provider.of<List<AppImplementation>>(context, listen: false);
final matchingAppImplementations = allAppImplementations.where((final a) => a.id == notification.app);
if (matchingAppImplementations.isNotEmpty) {
_appsBloc.setActiveApp(notification.app);
return true;
}
return false;
};
if (_platform.canUsePushNotifications) {
final localNotificationsPlugin = await PushUtils.initLocalNotifications();
Global.onPushNotificationReceived = () async {
final appImplementation = Provider.of<List<AppImplementation>>(context, listen: false)
.singleWhere((final a) => a.id == 'notifications');
_appsBloc.getAppBloc<NotificationsBloc>(appImplementation).refresh();
};
Global.onPushNotificationClicked = (final payload) async {
if (payload != null) {
final notification = NotificationsPushNotification.fromJson(json.decode(payload) as Map<String, dynamic>);
debugPrint('onNotificationClicked: ${notification.subject}');
final allAppImplementations = Provider.of<List<AppImplementation>>(context, listen: false);
final matchingAppImplementations =
allAppImplementations.where((final a) => a.id == notification.subject.app);
late AppImplementation appImplementation;
if (matchingAppImplementations.isNotEmpty) {
appImplementation = matchingAppImplementations.single;
} else {
appImplementation = allAppImplementations.singleWhere((final a) => a.id == 'notifications');
}
if (appImplementation.id != 'notifications') {
_appsBloc.getAppBloc<NotificationsBloc>(appImplementation).deleteNotification(notification.subject.nid);
}
await _openAppFromExternal(appImplementation.id);
}
};
final details = await localNotificationsPlugin.getNotificationAppLaunchDetails();
if (details != null && details.didNotificationLaunchApp) {
await Global.onPushNotificationClicked!(details.notificationResponse?.payload);
}
}
});
}
@override
void onTrayMenuItemClick(final tray.MenuItem menuItem) {
if (menuItem.key != null) {
unawaited(_handleShortcut(menuItem.key!));
}
}
@override
Future onWindowClose() async {
if (_globalOptions.startupMinimizeInsteadOfExit.value) {
await _saveAndMinimizeWindow();
} else {
await windowManager.destroy();
}
}
Future _handleShortcut(final String shortcutType) async {
if (shortcutType == 'show_hide') {
if (_platform.canUseWindowManager) {
if (await windowManager.isVisible()) {
await _saveAndMinimizeWindow();
} else {
await _showAndRestoreWindow();
}
}
return;
}
if (shortcutType == 'exit') {
exit(0);
}
final matches = _appRegex.allMatches(shortcutType).toList();
if (matches.isNotEmpty) {
await _openAppFromExternal(matches[0].group(1)!);
return;
}
}
Future _openAppFromExternal(final String id) async {
_appsBloc.setActiveApp(id);
Navigator.of(context).popUntil((final route) => route.settings.name == 'home');
if (_platform.canUseWindowManager) {
await _showAndRestoreWindow();
}
}
Future _saveAndMinimizeWindow() async {
_lastBounds = await windowManager.getBounds();
if (_globalOptions.systemTrayEnabled.value && _globalOptions.systemTrayHideToTrayWhenMinimized.value) {
await windowManager.hide();
} else {
await windowManager.minimize();
}
}
Future _showAndRestoreWindow() async {
final wasVisible = await windowManager.isVisible();
await windowManager.show();
await windowManager.focus();
if (_lastBounds != null && !wasVisible) {
await windowManager.setBounds(_lastBounds);
}
}
Future _showUnsupportedVersion(final String appName) async {
@ -303,15 +115,6 @@ class _HomePageState extends State<HomePage> with tray.TrayListener, WindowListe
@override
void dispose() {
_capabilitiesBloc.dispose();
_appsBloc.dispose();
if (_platform.canUseSystemTray) {
tray.trayManager.removeListener(this);
}
if (_platform.canUseWindowManager) {
windowManager.removeListener(this);
}
super.dispose();
}

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

@ -1,7 +1,6 @@
part of '../neon.dart';
class Global {
static Function()? onPushNotificationReceived;
static Function(String? payload)? onPushNotificationClicked;
static Future<bool> Function(NotificationsNotification notification)? handleNotificationOpening;
static Function(String accountID)? onPushNotificationReceived;
static Function(PushNotificationWithAccountID notification)? onPushNotificationClicked;
}

7
packages/neon/lib/src/utils/localizations.dart

@ -0,0 +1,7 @@
part of '../neon.dart';
Future<AppLocalizations> appLocalizationsFromSystem() async {
final parts =
(await findSystemLocale()).split('_').map((final a) => a.split('.')).reduce((final a, final b) => [...a, ...b]);
return AppLocalizations.delegate.load(Locale(parts[0], parts.length > 1 ? parts[1] : null));
}

21
packages/neon/lib/src/utils/push_utils.dart

@ -40,8 +40,12 @@ class PushUtils {
final localNotificationsPlugin = await initLocalNotifications(
onDidReceiveNotificationResponse: (final notification) async {
if (Global.onPushNotificationClicked != null) {
await Global.onPushNotificationClicked!(notification.payload);
if (Global.onPushNotificationClicked != null && notification.payload != null) {
await Global.onPushNotificationClicked!(
PushNotificationWithAccountID.fromJson(
json.decode(notification.payload!) as Map<String, dynamic>,
),
);
}
},
);
@ -69,9 +73,7 @@ class PushUtils {
return;
}
final parts =
(await findSystemLocale()).split('_').map((final a) => a.split('.')).reduce((final a, final b) => [...a, ...b]);
final localizations = await AppLocalizations.delegate.load(Locale(parts[0], parts.length > 1 ? parts[1] : null));
final localizations = await appLocalizationsFromSystem();
final platform = await getNeonPlatform();
final cache = Cache(platform);
@ -112,10 +114,15 @@ class PushUtils {
urgency: notification.type == 'voip' ? LinuxNotificationUrgency.critical : LinuxNotificationUrgency.normal,
),
),
payload: json.encode(notification.toJson()),
payload: json.encode(
PushNotificationWithAccountID(
notification: notification,
accountID: instance,
).toJson(),
),
);
Global.onPushNotificationReceived?.call();
Global.onPushNotificationReceived?.call(instance);
}
static int _getNotificationID(

20
packages/nextcloud/lib/src/nextcloud.openapi.dart

@ -3840,11 +3840,11 @@ class UserStatusClient {
@JsonSerializable()
class NotificationsPushNotificationDecryptedSubject {
NotificationsPushNotificationDecryptedSubject({
required this.nid,
required this.app,
required this.subject,
required this.type,
required this.id,
this.nid,
this.app,
this.subject,
this.type,
this.id,
this.delete,
this.deleteAll,
});
@ -3852,15 +3852,15 @@ class NotificationsPushNotificationDecryptedSubject {
factory NotificationsPushNotificationDecryptedSubject.fromJson(Map<String, dynamic> json) =>
_$NotificationsPushNotificationDecryptedSubjectFromJson(json);
final int nid;
final int? nid;
final String app;
final String? app;
final String subject;
final String? subject;
final String type;
final String? type;
final String id;
final String? id;
final bool? delete;

10
packages/nextcloud/lib/src/nextcloud.openapi.g.dart

@ -1627,11 +1627,11 @@ Map<String, dynamic> _$UserStatusPredefinedStatusesToJson(UserStatusPredefinedSt
NotificationsPushNotificationDecryptedSubject _$NotificationsPushNotificationDecryptedSubjectFromJson(
Map<String, dynamic> json) =>
NotificationsPushNotificationDecryptedSubject(
nid: json['nid'] as int,
app: json['app'] as String,
subject: json['subject'] as String,
type: json['type'] as String,
id: json['id'] as String,
nid: json['nid'] as int?,
app: json['app'] as String?,
subject: json['subject'] as String?,
type: json['type'] as String?,
id: json['id'] as String?,
delete: json['delete'] as bool?,
deleteAll: json['delete-all'] as bool?,
);

7
packages/nextcloud/lib/src/nextcloud.openapi.json

@ -1599,13 +1599,6 @@
},
"NotificationsPushNotificationDecryptedSubject": {
"type": "object",
"required": [
"nid",
"app",
"subject",
"type",
"id"
],
"properties": {
"nid": {
"type": "integer"

7
specs/notifications.json

@ -307,13 +307,6 @@
},
"NotificationsPushNotificationDecryptedSubject": {
"type": "object",
"required": [
"nid",
"app",
"subject",
"type",
"id"
],
"properties": {
"nid": {
"type": "integer"

2
tool/send-push-notification-test.sh

@ -1,4 +1,4 @@
#!/bin/bash
set -euxo pipefail
docker exec -it "$(docker ps | grep nextcloud-neon-dev | cut -d " " -f 1)" /bin/bash -c "php -f occ notification:test-push test $*"
docker exec -it "$(docker ps | grep nextcloud-neon-dev | cut -d " " -f 1)" /bin/bash -c "php -f occ notification:test-push $*"

Loading…
Cancel
Save