You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
572 lines
22 KiB
572 lines
22 KiB
part of '../../neon.dart'; |
|
|
|
class HomePage extends StatefulWidget { |
|
const HomePage({ |
|
required this.account, |
|
required this.onThemeChanged, |
|
super.key, |
|
}); |
|
|
|
final Account account; |
|
final Function(NextcloudTheme theme) onThemeChanged; |
|
|
|
@override |
|
State<HomePage> createState() => _HomePageState(); |
|
} |
|
|
|
// ignore: prefer_mixin |
|
class _HomePageState extends State<HomePage> with tray.TrayListener, WindowListener { |
|
final _appRegex = RegExp(r'^app_([a-z]+)$', multiLine: true); |
|
|
|
final _scaffoldKey = GlobalKey<ScaffoldState>(); |
|
|
|
late NeonPlatform _platform; |
|
late GlobalOptions _globalOptions; |
|
late RequestManager _requestManager; |
|
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); |
|
|
|
if (_platform.canUseSystemTray) { |
|
tray.trayManager.addListener(this); |
|
} |
|
if (_platform.canUseWindowManager) { |
|
windowManager.addListener(this); |
|
} |
|
|
|
_requestManager = Provider.of<RequestManager>(context, listen: false); |
|
_capabilitiesBloc = CapabilitiesBloc( |
|
_requestManager, |
|
widget.account.client, |
|
); |
|
_capabilitiesBloc.capabilities.listen((final result) { |
|
if (result.data != null) { |
|
widget.onThemeChanged(result.data!.capabilities!.theming!); |
|
|
|
// ignore cached version and prevent duplicate dialogs |
|
if (result is ResultSuccess) { |
|
const requiredMajorVersion = 24; |
|
if (result.data!.version!.major! < requiredMajorVersion) { |
|
showDialog( |
|
context: context, |
|
builder: (final context) => AlertDialog( |
|
title: Text(AppLocalizations.of(context).errorUnsupportedNextcloudVersion(requiredMajorVersion)), |
|
actions: [ |
|
ElevatedButton( |
|
style: ElevatedButton.styleFrom( |
|
primary: Colors.red, |
|
), |
|
onPressed: () { |
|
Navigator.of(context).pop(); |
|
}, |
|
child: Text(AppLocalizations.of(context).close), |
|
), |
|
], |
|
), |
|
); |
|
} |
|
} |
|
} |
|
}); |
|
_appsBloc = AppsBloc( |
|
_requestManager, |
|
accountsBloc, |
|
widget.account, |
|
Provider.of<List<AppImplementation>>(context, listen: false), |
|
); |
|
|
|
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 = NextcloudNotification.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( |
|
NotificationsNotification( |
|
notificationId: notification.subject.nid, |
|
), |
|
); |
|
} |
|
await _openAppFromExternal(appImplementation.id); |
|
} |
|
}; |
|
|
|
final details = await localNotificationsPlugin.getNotificationAppLaunchDetails(); |
|
if (details != null && details.didNotificationLaunchApp) { |
|
await Global.onPushNotificationClicked!(details.payload); |
|
} |
|
} |
|
}); |
|
} |
|
|
|
@override |
|
void onTrayMenuItemClick(tray.MenuItem menuItem) { |
|
if (menuItem.key != null) { |
|
_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); |
|
} |
|
} |
|
|
|
@override |
|
void dispose() { |
|
_capabilitiesBloc.dispose(); |
|
_appsBloc.dispose(); |
|
|
|
if (_platform.canUseSystemTray) { |
|
tray.trayManager.removeListener(this); |
|
} |
|
if (_platform.canUseWindowManager) { |
|
windowManager.removeListener(this); |
|
} |
|
|
|
super.dispose(); |
|
} |
|
|
|
@override |
|
Widget build(final BuildContext context) { |
|
final accountsBloc = RxBlocProvider.of<AccountsBloc>(context); |
|
return StandardRxResultBuilder<CapabilitiesBloc, Capabilities>( |
|
bloc: _capabilitiesBloc, |
|
state: (final bloc) => bloc.capabilities, |
|
builder: ( |
|
final context, |
|
final capabilitiesData, |
|
final capabilitiesError, |
|
final capabilitiesLoading, |
|
final _, |
|
) => |
|
StandardRxResultBuilder<AppsBloc, List<AppImplementation>>( |
|
bloc: _appsBloc, |
|
state: (final bloc) => bloc.appImplementations, |
|
builder: ( |
|
final context, |
|
final appsData, |
|
final appsError, |
|
final appsLoading, |
|
final _, |
|
) => |
|
RxBlocBuilder<AppsBloc, String?>( |
|
bloc: _appsBloc, |
|
state: (final bloc) => bloc.activeAppID, |
|
builder: ( |
|
final context, |
|
final activeAppIDSnapshot, |
|
final _, |
|
) => |
|
RxBlocBuilder<AccountsBloc, List<Account>>( |
|
bloc: accountsBloc, |
|
state: (final bloc) => bloc.accounts, |
|
builder: ( |
|
final context, |
|
final accountsSnapshot, |
|
final _, |
|
) => |
|
WillPopScope( |
|
onWillPop: () async { |
|
if (_scaffoldKey.currentState!.isDrawerOpen) { |
|
Navigator.pop(context); |
|
return true; |
|
} |
|
|
|
_scaffoldKey.currentState!.openDrawer(); |
|
return false; |
|
}, |
|
child: Scaffold( |
|
key: _scaffoldKey, |
|
resizeToAvoidBottomInset: false, |
|
appBar: AppBar( |
|
title: Row( |
|
children: [ |
|
Expanded( |
|
child: Row( |
|
children: [ |
|
if (appsData != null && activeAppIDSnapshot.hasData) ...[ |
|
Flexible( |
|
child: Text( |
|
appsData.singleWhere((final a) => a.id == activeAppIDSnapshot.data!).name(context), |
|
), |
|
), |
|
], |
|
if (appsError != null) ...[ |
|
const SizedBox( |
|
width: 8, |
|
), |
|
Icon( |
|
Icons.error_outline, |
|
size: 30, |
|
color: Theme.of(context).colorScheme.onPrimary, |
|
), |
|
], |
|
if (appsLoading) ...[ |
|
const SizedBox( |
|
width: 8, |
|
), |
|
SizedBox( |
|
height: 30, |
|
width: 30, |
|
child: CircularProgressIndicator( |
|
color: Theme.of(context).colorScheme.onPrimary, |
|
strokeWidth: 2, |
|
), |
|
), |
|
], |
|
const SizedBox( |
|
width: 8, |
|
), |
|
], |
|
), |
|
), |
|
Row( |
|
children: [ |
|
if (appsData != null && activeAppIDSnapshot.hasData) ...[ |
|
IconButton( |
|
icon: const Icon(Icons.settings), |
|
onPressed: () async { |
|
await Navigator.of(context).push( |
|
MaterialPageRoute( |
|
builder: (final context) => NextcloudAppSpecificSettingsPage( |
|
appImplementation: |
|
appsData.singleWhere((final a) => a.id == activeAppIDSnapshot.data!), |
|
), |
|
), |
|
); |
|
}, |
|
), |
|
Builder( |
|
builder: (final context) { |
|
if (accountsSnapshot.hasData) { |
|
final matches = accountsSnapshot.data! |
|
.where( |
|
(final account) => account.id == widget.account.id, |
|
) |
|
.toList(); |
|
if (matches.length == 1) { |
|
return AccountAvatar( |
|
account: matches[0], |
|
requestManager: _requestManager, |
|
); |
|
} |
|
} |
|
return Container(); |
|
}, |
|
), |
|
], |
|
], |
|
), |
|
], |
|
), |
|
), |
|
drawer: Drawer( |
|
child: Column( |
|
children: [ |
|
Expanded( |
|
child: Scrollbar( |
|
child: ListView( |
|
// Needed for the drawer header to also render in the statusbar |
|
padding: EdgeInsets.zero, |
|
children: [ |
|
Builder( |
|
builder: (final context) { |
|
if (accountsSnapshot.hasData) { |
|
return DrawerHeader( |
|
decoration: BoxDecoration( |
|
color: Theme.of(context).colorScheme.primary, |
|
), |
|
child: Column( |
|
crossAxisAlignment: CrossAxisAlignment.start, |
|
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
|
children: [ |
|
if (capabilitiesData != null) ...[ |
|
Text( |
|
capabilitiesData.capabilities!.theming!.name!, |
|
style: DefaultTextStyle.of(context).style.copyWith( |
|
color: Theme.of(context).colorScheme.onPrimary, |
|
), |
|
), |
|
if (capabilitiesData.capabilities!.theming!.logo != null) ...[ |
|
Flexible( |
|
child: CachedURLImage( |
|
url: capabilitiesData.capabilities!.theming!.logo!, |
|
requestManager: _requestManager, |
|
client: widget.account.client, |
|
), |
|
), |
|
], |
|
] else ...[ |
|
Container(), |
|
], |
|
if (accountsSnapshot.data!.length != 1) ...[ |
|
DropdownButtonHideUnderline( |
|
child: DropdownButton<String>( |
|
isExpanded: true, |
|
dropdownColor: Theme.of(context).colorScheme.primary, |
|
iconEnabledColor: Theme.of(context).colorScheme.onPrimary, |
|
value: widget.account.id, |
|
items: accountsSnapshot.data! |
|
.map<DropdownMenuItem<String>>( |
|
(final account) => DropdownMenuItem<String>( |
|
value: account.id, |
|
child: AccountTile( |
|
account: account, |
|
), |
|
), |
|
) |
|
.toList(), |
|
onChanged: (final id) { |
|
for (final account in accountsSnapshot.data!) { |
|
if (account.id == id) { |
|
accountsBloc.setActiveAccount(account); |
|
break; |
|
} |
|
} |
|
}, |
|
), |
|
), |
|
], |
|
], |
|
), |
|
); |
|
} |
|
return Container(); |
|
}, |
|
), |
|
ExceptionWidget( |
|
appsError, |
|
onRetry: () { |
|
_appsBloc.refresh(); |
|
}, |
|
), |
|
CustomLinearProgressIndicator( |
|
visible: appsLoading, |
|
), |
|
if (appsData != null) ...[ |
|
for (final appImplementation in appsData) ...[ |
|
if (appsData.map((final a) => a.id).contains(appImplementation.id)) ...[ |
|
ListTile( |
|
title: Text(appImplementation.name(context)), |
|
leading: appImplementation.buildIcon(context), |
|
minLeadingWidth: 0, |
|
onTap: () { |
|
_appsBloc.setActiveApp(appImplementation.id); |
|
Navigator.of(context).pop(); |
|
}, |
|
), |
|
], |
|
], |
|
], |
|
], |
|
), |
|
), |
|
), |
|
ListTile( |
|
title: Text(AppLocalizations.of(context).settings), |
|
leading: const Icon(Icons.settings), |
|
minLeadingWidth: 0, |
|
onTap: () async { |
|
await Navigator.of(context).push( |
|
MaterialPageRoute( |
|
builder: (final context) => const SettingsPage(), |
|
), |
|
); |
|
}, |
|
), |
|
], |
|
), |
|
), |
|
body: Column( |
|
children: [ |
|
ServerStatus( |
|
account: widget.account, |
|
), |
|
ExceptionWidget( |
|
appsError, |
|
onRetry: () { |
|
_appsBloc.refresh(); |
|
}, |
|
), |
|
if (appsData != null) ...[ |
|
if (appsData.isEmpty) ...[ |
|
Expanded( |
|
child: Center( |
|
child: Text( |
|
AppLocalizations.of(context).errorNoCompatibleNextcloudAppsFound, |
|
textAlign: TextAlign.center, |
|
), |
|
), |
|
), |
|
] else ...[ |
|
if (activeAppIDSnapshot.hasData) ...[ |
|
Expanded( |
|
child: appsData |
|
.singleWhere((final a) => a.id == activeAppIDSnapshot.data!) |
|
.buildPage(context, _appsBloc), |
|
), |
|
], |
|
], |
|
], |
|
], |
|
), |
|
), |
|
), |
|
), |
|
), |
|
), |
|
); |
|
} |
|
}
|
|
|