Browse Source

neon: Refactor tray, quick actions and push notifications to app level

pull/100/head
jld3103 2 years ago
parent
commit
5596b58589
No known key found for this signature in database
GPG Key ID: 9062417B9E8EB7B3
  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. 12
      packages/neon/lib/src/models/account.dart
  5. 20
      packages/neon/lib/src/models/push_notification_with_account.dart
  6. 18
      packages/neon/lib/src/models/push_notification_with_account.g.dart
  7. 3
      packages/neon/lib/src/neon.dart
  8. 201
      packages/neon/lib/src/pages/home/home.dart
  9. 5
      packages/neon/lib/src/utils/global.dart
  10. 7
      packages/neon/lib/src/utils/localizations.dart
  11. 21
      packages/neon/lib/src/utils/push_utils.dart

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(

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(

Loading…
Cancel
Save