|
|
@ -21,12 +21,16 @@ class NeonApp extends StatefulWidget { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// ignore: prefer_mixin |
|
|
|
// 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>(); |
|
|
|
final _navigatorKey = GlobalKey<NavigatorState>(); |
|
|
|
CoreServerCapabilities_Ocs_Data_Capabilities_Theming? _userTheme; |
|
|
|
late NeonPlatform _platform; |
|
|
|
final _platformBrightness = BehaviorSubject<Brightness>.seeded( |
|
|
|
late GlobalOptions _globalOptions; |
|
|
|
WidgetsBinding.instance.window.platformBrightness, |
|
|
|
late AccountsBloc _accountsBloc; |
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
CoreServerCapabilities_Ocs_Data_Capabilities_Theming? _nextcloudTheme; |
|
|
|
|
|
|
|
final _platformBrightness = BehaviorSubject<Brightness>.seeded(WidgetsBinding.instance.window.platformBrightness); |
|
|
|
|
|
|
|
Rect? _lastBounds; |
|
|
|
|
|
|
|
|
|
|
|
@override |
|
|
|
@override |
|
|
|
void didChangePlatformBrightness() { |
|
|
|
void didChangePlatformBrightness() { |
|
|
@ -39,9 +43,19 @@ class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver { |
|
|
|
void initState() { |
|
|
|
void initState() { |
|
|
|
super.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); |
|
|
|
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 { |
|
|
|
widget.accountsBloc.activeAccount.listen((final activeAccount) async { |
|
|
|
FlutterNativeSplash.remove(); |
|
|
|
FlutterNativeSplash.remove(); |
|
|
|
|
|
|
|
|
|
|
@ -58,9 +72,9 @@ class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver { |
|
|
|
); |
|
|
|
); |
|
|
|
Widget builder(final context) => HomePage( |
|
|
|
Widget builder(final context) => HomePage( |
|
|
|
account: activeAccount, |
|
|
|
account: activeAccount, |
|
|
|
onThemeChanged: (final theme) { |
|
|
|
onThemeChanged: (final nextcloudTheme) { |
|
|
|
setState(() { |
|
|
|
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 |
|
|
|
@override |
|
|
|
void dispose() { |
|
|
|
void dispose() { |
|
|
|
WidgetsBinding.instance.removeObserver(this); |
|
|
|
WidgetsBinding.instance.removeObserver(this); |
|
|
|
|
|
|
|
if (_platform.canUseSystemTray) { |
|
|
|
|
|
|
|
tray.trayManager.removeListener(this); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if (_platform.canUseWindowManager) { |
|
|
|
|
|
|
|
windowManager.removeListener(this); |
|
|
|
|
|
|
|
} |
|
|
|
unawaited(_platformBrightness.close()); |
|
|
|
unawaited(_platformBrightness.close()); |
|
|
|
|
|
|
|
|
|
|
|
super.dispose(); |
|
|
|
super.dispose(); |
|
|
@ -110,7 +313,7 @@ class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver { |
|
|
|
supportedLocales: AppLocalizations.supportedLocales, |
|
|
|
supportedLocales: AppLocalizations.supportedLocales, |
|
|
|
navigatorKey: _navigatorKey, |
|
|
|
navigatorKey: _navigatorKey, |
|
|
|
theme: getThemeFromNextcloudTheme( |
|
|
|
theme: getThemeFromNextcloudTheme( |
|
|
|
_userTheme, |
|
|
|
_nextcloudTheme, |
|
|
|
themeMode, |
|
|
|
themeMode, |
|
|
|
platformBrightnessSnapshot.data!, |
|
|
|
platformBrightnessSnapshot.data!, |
|
|
|
oledAsDark: themeOLEDAsDark, |
|
|
|
oledAsDark: themeOLEDAsDark, |
|
|
|