262 lines
8.4 KiB
Dart
262 lines
8.4 KiB
Dart
import 'dart:io';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
import 'package:protocol_handler/protocol_handler.dart';
|
|
import 'package:island/pods/activity/activity_rpc.dart';
|
|
import 'package:island/pods/config.dart';
|
|
import 'package:island/pods/network.dart';
|
|
import 'package:island/pods/websocket.dart';
|
|
import 'package:island/route.dart';
|
|
import 'package:island/screens/auth/login_content.dart';
|
|
import 'package:island/screens/tray_manager.dart';
|
|
import 'package:island/pods/web_auth/web_auth_providers.dart';
|
|
import 'package:island/services/notify.dart';
|
|
import 'package:island/services/sharing_intent.dart';
|
|
import 'package:island/services/update_service.dart';
|
|
import 'package:island/widgets/content/network_status_sheet.dart';
|
|
import 'package:island/widgets/tour/tour.dart';
|
|
import 'package:island/widgets/post/compose_sheet.dart';
|
|
import 'package:island/screens/notification.dart';
|
|
import 'package:island/screens/thought/think_sheet.dart';
|
|
import 'package:island/services/event_bus.dart';
|
|
import 'package:snow_fall_animation/snow_fall_animation.dart';
|
|
import 'package:tray_manager/tray_manager.dart';
|
|
import 'package:window_manager/window_manager.dart';
|
|
|
|
class AppWrapper extends HookConsumerWidget {
|
|
final Widget child;
|
|
const AppWrapper({super.key, required this.child});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final networkStateShowing = useState(false);
|
|
final wsNotifier = ref.watch(websocketStateProvider.notifier);
|
|
final websocketState = ref.watch(websocketStateProvider);
|
|
final showSnow = useState(false);
|
|
final isSnowGone = useState(false);
|
|
|
|
// Handle network status modal
|
|
if (websocketState == WebSocketState.duplicateDevice() &&
|
|
!networkStateShowing.value) {
|
|
networkStateShowing.value = true;
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
showModalBottomSheet(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
isDismissible: false,
|
|
builder: (context) =>
|
|
NetworkStatusSheet(onReconnect: () => wsNotifier.connect()),
|
|
).then((_) => networkStateShowing.value = false);
|
|
});
|
|
}
|
|
|
|
// Initialize services and listeners
|
|
useEffect(() {
|
|
final ntySubs = setupNotificationListener(context, ref);
|
|
final sharingService = SharingIntentService();
|
|
sharingService.initialize(context);
|
|
UpdateService().checkForUpdates(context);
|
|
|
|
final trayService = TrayService.instance;
|
|
trayService.initialize(
|
|
_TrayListenerImpl(
|
|
onTrayIconMouseDown: () => windowManager.show(),
|
|
onTrayIconRightMouseUp: () => trayManager.popUpContextMenu(),
|
|
onTrayMenuItemClick: (menuItem) => trayService.handleAction(menuItem),
|
|
),
|
|
);
|
|
|
|
ref.read(rpcServerStateProvider.notifier).start();
|
|
ref.read(webAuthServerStateProvider.notifier).start();
|
|
|
|
// Listen to special action events
|
|
final composeSheetSubs = eventBus.on<ShowComposeSheetEvent>().listen((
|
|
event,
|
|
) {
|
|
if (context.mounted) _showComposeSheet(context);
|
|
});
|
|
|
|
final notificationSheetSubs = eventBus
|
|
.on<ShowNotificationSheetEvent>()
|
|
.listen((event) {
|
|
if (context.mounted) _showNotificationSheet(context);
|
|
});
|
|
|
|
final thoughtSheetSubs = eventBus.on<ShowThoughtSheetEvent>().listen((
|
|
event,
|
|
) {
|
|
if (context.mounted) _showThoughtSheet(context, event);
|
|
});
|
|
|
|
// Protocol handler listener
|
|
final protocolListener = _ProtocolListenerImpl(
|
|
onProtocolUrlReceived: (url) =>
|
|
_handleDeepLink(Uri.parse(url), ref, context),
|
|
);
|
|
protocolHandler.addListener(protocolListener);
|
|
|
|
// Handle initial URL
|
|
protocolHandler.getInitialUrl().then((initialUrl) {
|
|
if (initialUrl != null) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_handleDeepLink(Uri.parse(initialUrl), ref, context);
|
|
});
|
|
}
|
|
});
|
|
|
|
return () {
|
|
protocolHandler.removeListener(protocolListener);
|
|
ref.read(rpcServerProvider).stop();
|
|
trayService.dispose(
|
|
_TrayListenerImpl(
|
|
onTrayIconMouseDown: () => {},
|
|
onTrayIconRightMouseUp: () => {},
|
|
onTrayMenuItemClick: (menuItem) => {},
|
|
),
|
|
);
|
|
ntySubs?.cancel();
|
|
composeSheetSubs.cancel();
|
|
notificationSheetSubs.cancel();
|
|
thoughtSheetSubs.cancel();
|
|
};
|
|
}, []);
|
|
|
|
final settings = ref.watch(appSettingsProvider);
|
|
|
|
final now = DateTime.now();
|
|
final doesShowSnow =
|
|
settings.festivalFeatures &&
|
|
now.month == 12 &&
|
|
(now.day >= 22 && now.day <= 28);
|
|
if (doesShowSnow && !isSnowGone.value) {
|
|
showSnow.value = true;
|
|
isSnowGone.value = true;
|
|
Future.delayed(const Duration(seconds: 10), () {
|
|
showSnow.value = false;
|
|
});
|
|
}
|
|
|
|
return TourTriggerWidget(
|
|
key: UniqueKey(),
|
|
child: Stack(
|
|
children: [
|
|
child,
|
|
if (showSnow.value)
|
|
IgnorePointer(
|
|
child: SnowFallAnimation(
|
|
key: const Key("app_snow_animation"),
|
|
config: SnowfallConfig(numberOfSnowflakes: 50, speed: 1.0),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showComposeSheet(BuildContext context) {
|
|
PostComposeSheet.show(context);
|
|
}
|
|
|
|
void _showNotificationSheet(BuildContext context) {
|
|
showModalBottomSheet(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
useRootNavigator: true,
|
|
builder: (context) => const NotificationSheet(),
|
|
);
|
|
}
|
|
|
|
void _showThoughtSheet(BuildContext context, ShowThoughtSheetEvent event) {
|
|
ThoughtSheet.show(
|
|
context,
|
|
initialMessage: event.initialMessage,
|
|
attachedMessages: event.attachedMessages,
|
|
attachedPosts: event.attachedPosts,
|
|
);
|
|
}
|
|
|
|
void _handleDeepLink(Uri uri, WidgetRef ref, BuildContext context) async {
|
|
String path = '/${uri.host}${uri.path}';
|
|
|
|
// Special handling for OIDC auth callback
|
|
if (path == '/auth/callback' && uri.queryParameters.containsKey('token')) {
|
|
final token = uri.queryParameters['token']!;
|
|
setToken(ref.read(sharedPreferencesProvider), token);
|
|
ref.invalidate(tokenProvider);
|
|
|
|
// Do post login tasks
|
|
await performPostLogin(context, ref);
|
|
|
|
if (!kIsWeb &&
|
|
(Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
|
|
windowManager.show();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Special handling for share intent deep links
|
|
// Share intents are handled by SharingIntentService showing a modal,
|
|
// not by routing to a page
|
|
if (path == '/share') {
|
|
if (!kIsWeb &&
|
|
(Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
|
|
windowManager.show();
|
|
}
|
|
return;
|
|
}
|
|
|
|
final router = ref.read(routerProvider);
|
|
if (uri.queryParameters.isNotEmpty) {
|
|
path = Uri.parse(
|
|
path,
|
|
).replace(queryParameters: uri.queryParameters).toString();
|
|
}
|
|
router.push(path);
|
|
if (!kIsWeb &&
|
|
(Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
|
|
windowManager.show();
|
|
}
|
|
}
|
|
}
|
|
|
|
class _TrayListenerImpl implements TrayListener {
|
|
final VoidCallback _primaryAction;
|
|
final VoidCallback _secondaryAction;
|
|
final void Function(MenuItem) _onTrayMenuItemClick;
|
|
|
|
_TrayListenerImpl({
|
|
required VoidCallback onTrayIconMouseDown,
|
|
required VoidCallback onTrayIconRightMouseUp,
|
|
required void Function(MenuItem) onTrayMenuItemClick,
|
|
}) : _primaryAction = onTrayIconMouseDown,
|
|
_secondaryAction = onTrayIconRightMouseUp,
|
|
_onTrayMenuItemClick = onTrayMenuItemClick;
|
|
|
|
@override
|
|
void onTrayIconMouseDown() => _primaryAction();
|
|
|
|
@override
|
|
void onTrayIconRightMouseUp() => _secondaryAction();
|
|
|
|
@override
|
|
void onTrayIconMouseUp() => _primaryAction();
|
|
|
|
@override
|
|
void onTrayIconRightMouseDown() => _secondaryAction();
|
|
|
|
@override
|
|
void onTrayMenuItemClick(MenuItem menuItem) => _onTrayMenuItemClick(menuItem);
|
|
}
|
|
|
|
class _ProtocolListenerImpl implements ProtocolListener {
|
|
final void Function(String) _onProtocolUrlReceived;
|
|
|
|
_ProtocolListenerImpl({required void Function(String) onProtocolUrlReceived})
|
|
: _onProtocolUrlReceived = onProtocolUrlReceived;
|
|
|
|
@override
|
|
void onProtocolUrlReceived(String url) => _onProtocolUrlReceived(url);
|
|
}
|