A framework for building convergent cross-platform Nextcloud clients using Flutter.
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.
 
 

334 lines
11 KiB

part of 'neon.dart';
class NeonApp extends StatefulWidget {
const NeonApp({
required this.accountsBloc,
required this.sharedPreferences,
required this.env,
required this.platform,
required this.globalOptions,
super.key,
});
final AccountsBloc accountsBloc;
final SharedPreferences sharedPreferences;
final Env? env;
final NeonPlatform platform;
final GlobalOptions globalOptions;
@override
State<NeonApp> createState() => _NeonAppState();
}
// ignore: prefer_mixin
class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver, tray.TrayListener, WindowListener {
final _appRegex = RegExp(r'^app_([a-z]+)$', multiLine: true);
final _navigatorKey = GlobalKey<NavigatorState>();
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() {
_platformBrightness.add(WidgetsBinding.instance.window.platformBrightness);
super.didChangePlatformBrightness();
}
@override
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 _) async {
widget.accountsBloc.activeAccount.listen((final activeAccount) async {
FlutterNativeSplash.remove();
if (activeAccount == null) {
await _navigatorKey.currentState!.pushAndRemoveUntil(
MaterialPageRoute(
builder: (final context) => const LoginPage(),
),
(final _) => false,
);
} else {
const settings = RouteSettings(
name: 'home',
);
Widget builder(final context) => HomePage(
account: activeAccount,
onThemeChanged: (final nextcloudTheme) {
setState(() {
_nextcloudTheme = nextcloudTheme;
});
},
);
await _navigatorKey.currentState!.pushAndRemoveUntil(
widget.globalOptions.navigationMode.value == NavigationMode.drawer
? MaterialPageRoute(
settings: settings,
builder: builder,
)
: NoAnimationPageRoute(
settings: settings,
builder: builder,
),
(final _) => false,
);
}
});
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();
}
}
@override
Future onWindowMinimize() async {
await _saveAndMinimizeWindow();
}
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();
}
@override
Widget build(final BuildContext context) => StreamBuilder<Brightness>(
stream: _platformBrightness,
builder: (final context, final platformBrightnessSnapshot) => OptionBuilder(
option: widget.globalOptions.themeMode,
builder: (final context, final themeMode) => OptionBuilder(
option: widget.globalOptions.themeOLEDAsDark,
builder: (final context, final themeOLEDAsDark) => OptionBuilder(
option: widget.globalOptions.themeKeepOriginalAccentColor,
builder: (final context, final themeKeepOriginalAccentColor) {
if (!platformBrightnessSnapshot.hasData ||
themeMode == null ||
themeOLEDAsDark == null ||
themeKeepOriginalAccentColor == null) {
return Container();
}
return MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
navigatorKey: _navigatorKey,
theme: getThemeFromNextcloudTheme(
_nextcloudTheme,
themeMode,
platformBrightnessSnapshot.data!,
oledAsDark: themeOLEDAsDark,
keepOriginalAccentColor: themeKeepOriginalAccentColor,
),
home: Container(),
);
},
),
),
),
);
}