From 1a9599be2767ace6df26132219942e964e52cb7b Mon Sep 17 00:00:00 2001 From: Jaap Aarts Date: Thu, 9 May 2024 01:25:06 +0200 Subject: [PATCH] Fix --- integration_test/album_screen_test.dart | 3 - lib/blocs.dart | 5 - lib/blocs/food_cubit.dart | 150 ---------- lib/blocs/full_member_cubit.dart | 39 --- lib/blocs/theme_cubit.dart | 39 --- lib/blocs/welcome_cubit.dart | 117 -------- lib/main.dart | 260 ++++++----------- lib/models/album.dart | 13 +- lib/models/album.g.dart | 8 - lib/routes.dart | 60 +--- lib/ui/screens.dart | 3 - lib/ui/screens/album_screen.dart | 1 + lib/ui/screens/login_screen.dart | 177 ----------- lib/ui/screens/welcome_screen.dart | 275 ------------------ lib/ui/widgets.dart | 5 - lib/ui/widgets/album_tile.dart | 14 +- lib/ui/widgets/cached_image.dart | 50 ---- lib/ui/widgets/file_button.dart | 40 --- lib/ui/widgets/member_tile.dart | 112 ------- lib/ui/widgets/menu_drawer.dart | 139 --------- lib/ui/widgets/push_notification_dialog.dart | 48 --- lib/ui/widgets/push_notification_overlay.dart | 45 --- lib/ui/widgets/sortbutton.dart | 60 ---- lib/utilities/cache_manager.dart | 18 -- test/mocks.dart | 1 - test/mocks.mocks.dart | 111 ------- test/unit/routes_test.dart | 50 ---- test/widget/welcome_test.dart | 95 ------ 28 files changed, 94 insertions(+), 1844 deletions(-) delete mode 100644 lib/blocs/food_cubit.dart delete mode 100644 lib/blocs/full_member_cubit.dart delete mode 100644 lib/blocs/theme_cubit.dart delete mode 100644 lib/blocs/welcome_cubit.dart delete mode 100644 lib/ui/screens/login_screen.dart delete mode 100644 lib/ui/screens/welcome_screen.dart delete mode 100644 lib/ui/widgets/cached_image.dart delete mode 100644 lib/ui/widgets/file_button.dart delete mode 100644 lib/ui/widgets/member_tile.dart delete mode 100644 lib/ui/widgets/push_notification_dialog.dart delete mode 100644 lib/ui/widgets/push_notification_overlay.dart delete mode 100644 lib/ui/widgets/sortbutton.dart delete mode 100644 lib/utilities/cache_manager.dart delete mode 100644 test/unit/routes_test.dart delete mode 100644 test/widget/welcome_test.dart diff --git a/integration_test/album_screen_test.dart b/integration_test/album_screen_test.dart index 53b5acd37..00e480423 100644 --- a/integration_test/album_screen_test.dart +++ b/integration_test/album_screen_test.dart @@ -128,7 +128,6 @@ void testAlbum(IntegrationTestWidgetsFlutterBinding binding) { 'mock', false, false, - coverphoto1, [albumphoto1], ), ), @@ -143,7 +142,6 @@ void testAlbum(IntegrationTestWidgetsFlutterBinding binding) { 'MOcK2', false, false, - coverphoto1, [albumphoto1, albumphoto2], ), ), @@ -157,7 +155,6 @@ void testAlbum(IntegrationTestWidgetsFlutterBinding binding) { 'MOcK2', false, false, - coverphoto1, [albumphoto1, albumphoto2], ); diff --git a/lib/blocs.dart b/lib/blocs.dart index a74947119..87c10c827 100644 --- a/lib/blocs.dart +++ b/lib/blocs.dart @@ -1,9 +1,4 @@ -export 'blocs/album_cubit.dart'; export 'blocs/album_list_cubit.dart'; export 'blocs/auth_cubit.dart'; export 'blocs/detail_state.dart'; -export 'blocs/food_cubit.dart'; -export 'blocs/full_member_cubit.dart'; export 'blocs/list_state.dart'; -export 'blocs/theme_cubit.dart'; -export 'blocs/welcome_cubit.dart'; diff --git a/lib/blocs/food_cubit.dart b/lib/blocs/food_cubit.dart deleted file mode 100644 index 83e72c19c..000000000 --- a/lib/blocs/food_cubit.dart +++ /dev/null @@ -1,150 +0,0 @@ -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:reaxit/api/api_repository.dart'; -import 'package:equatable/equatable.dart'; -import 'package:meta/meta.dart'; -import 'package:reaxit/api/exceptions.dart'; -import 'package:reaxit/models.dart'; - -class FoodState extends Equatable { - /// This can only be null when [isLoading] or [hasException] is true. - final FoodEvent? foodEvent; - - /// This can only be null when [isLoading] or [hasException] is true. - final List? products; - - /// A message describing why there are no foodEvents. - final String? message; - - /// A foodEvent is being loaded. If there - /// already is a foodEvent, it is outdated. - final bool isLoading; - - bool get hasException => message != null; - - @protected - const FoodState({ - required this.foodEvent, - required this.products, - required this.isLoading, - required this.message, - }) : assert( - (foodEvent != null && products != null) || - isLoading || - message != null, - 'foodEvent and products can only be null ' - 'when isLoading or hasException is true.', - ); - - @override - List get props => [foodEvent, products, message, isLoading]; - - FoodState copyWith({ - FoodEvent? foodEvent, - List? products, - bool? isLoading, - String? message, - }) => - FoodState( - foodEvent: foodEvent ?? this.foodEvent, - products: products ?? this.products, - isLoading: isLoading ?? this.isLoading, - message: message ?? this.message, - ); - - const FoodState.result({ - required FoodEvent this.foodEvent, - required List this.products, - }) : message = null, - isLoading = false; - - const FoodState.loading({this.foodEvent, this.products}) - : message = null, - isLoading = true; - - const FoodState.failure({required String this.message}) - : foodEvent = null, - products = null, - isLoading = false; -} - -class FoodCubit extends Cubit { - final ApiRepository api; - - int? _foodEventPk; - - FoodCubit(this.api, {int? foodEventPk}) - : _foodEventPk = foodEventPk, - super(const FoodState.loading()); - - Future load() async { - emit(state.copyWith(isLoading: true)); - try { - late final FoodEvent event; - if (_foodEventPk == null) { - event = await api.getCurrentFoodEvent(); - _foodEventPk = event.pk; - } else { - event = await api.getFoodEvent(_foodEventPk!); - } - - final products = await api.getFoodEventProducts(_foodEventPk!); - emit(FoodState.result(foodEvent: event, products: products.results)); - } on ApiException catch (exception) { - emit(FoodState.failure( - message: exception.getMessage( - notFound: 'The food event does not exist.', - ), - )); - } - } - - Future placeOrder({ - required int productPk, - }) async { - if (_foodEventPk == null) { - final event = await api.getCurrentFoodEvent(); - _foodEventPk = event.pk; - } - - final order = await api.placeFoodOrder( - eventPk: _foodEventPk!, - productPk: productPk, - ); - await load(); - return order; - } - - Future changeOrder({ - required int productPk, - }) async { - if (_foodEventPk == null) { - final event = await api.getCurrentFoodEvent(); - _foodEventPk = event.pk; - } - - final order = await api.changeFoodOrder( - eventPk: _foodEventPk!, - productPk: productPk, - ); - await load(); - return order; - } - - /// Cancel you order. - Future cancelOrder() async { - if (_foodEventPk == null) { - final event = await api.getCurrentFoodEvent(); - _foodEventPk = event.pk; - } - await api.cancelFoodOrder(_foodEventPk!); - await load(); - } - - /// Pay your order `orderPk` using Thalia Pay. - Future thaliaPayOrder({ - required int orderPk, - }) async { - await api.thaliaPayFoodOrder(foodOrderPk: orderPk); - await load(); - } -} diff --git a/lib/blocs/full_member_cubit.dart b/lib/blocs/full_member_cubit.dart deleted file mode 100644 index 83fd19283..000000000 --- a/lib/blocs/full_member_cubit.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:image_cropper/image_cropper.dart'; -import 'package:reaxit/api/api_repository.dart'; -import 'package:reaxit/api/exceptions.dart'; -import 'package:reaxit/blocs.dart'; -import 'package:reaxit/models.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; - -typedef FullMemberState = DetailState; - -class FullMemberCubit extends Cubit { - final ApiRepository api; - - FullMemberCubit(this.api) : super(const LoadingState()); - - Future load() async { - emit(LoadingState.from(state)); - try { - final member = await api.getMe(); - // Set username for sentry. - Sentry.configureScope( - (scope) => scope.setUser(SentryUser(username: member.displayName)), - ); - emit(ResultState(member)); - } on ApiException catch (exception) { - emit(ErrorState(exception.message)); - } - } - - Future updateAvatar(CroppedFile file) async { - await api.updateAvatar(file.path); - await load(); - } - - Future updateDescription(String description) async { - await api.updateDescription(description); - await load(); - } -} diff --git a/lib/blocs/theme_cubit.dart b/lib/blocs/theme_cubit.dart deleted file mode 100644 index 7f8fbcd6c..000000000 --- a/lib/blocs/theme_cubit.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -const _themeModePreferenceKey = 'themeMode'; - -class ThemeCubit extends Cubit { - ThemeCubit() : super(ThemeMode.system); - - Future load() async { - var prefs = await SharedPreferences.getInstance(); - var modeString = prefs.getString(_themeModePreferenceKey); - if (modeString == null) { - emit(ThemeMode.system); - } else if (modeString == 'system') { - emit(ThemeMode.system); - } else if (modeString == 'light') { - emit(ThemeMode.light); - } else if (modeString == 'dark') { - emit(ThemeMode.dark); - } - } - - Future change(ThemeMode newMode) async { - var prefs = await SharedPreferences.getInstance(); - switch (newMode) { - case ThemeMode.system: - await prefs.setString(_themeModePreferenceKey, 'system'); - break; - case ThemeMode.light: - await prefs.setString(_themeModePreferenceKey, 'light'); - break; - case ThemeMode.dark: - await prefs.setString(_themeModePreferenceKey, 'dark'); - break; - } - emit(newMode); - } -} diff --git a/lib/blocs/welcome_cubit.dart b/lib/blocs/welcome_cubit.dart deleted file mode 100644 index 6f1802af5..000000000 --- a/lib/blocs/welcome_cubit.dart +++ /dev/null @@ -1,117 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:equatable/equatable.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:meta/meta.dart'; -import 'package:reaxit/api/api_repository.dart'; -import 'package:reaxit/api/exceptions.dart'; -import 'package:reaxit/models.dart'; - -class WelcomeState extends Equatable { - /// This can only be null when [isLoading] or [hasException] is true. - final List? slides; - - /// This can only be null when [isLoading] or [hasException] is true. - final List? articles; - - /// This can only be null when [isLoading] or [hasException] is true. - final List? upcomingEvents; - - /// A message describing why there are no results. - final String? message; - - /// The results are loading. Existing results may be outdated. - final bool isLoading; - - bool get hasException => message != null; - bool get hasResults => - slides != null && articles != null && upcomingEvents != null; - - @protected - const WelcomeState({ - required this.slides, - required this.articles, - required this.upcomingEvents, - required this.isLoading, - required this.message, - }); - - @override - List get props => - [slides, articles, upcomingEvents, message, isLoading]; - - WelcomeState copyWith({ - List? slides, - List? articles, - List? upcomingEvents, - bool? isLoading, - String? message, - }) => - WelcomeState( - slides: slides ?? this.slides, - articles: articles ?? this.articles, - upcomingEvents: upcomingEvents ?? this.upcomingEvents, - isLoading: isLoading ?? this.isLoading, - message: message ?? this.message, - ); - - const WelcomeState.result({ - required List this.slides, - required List this.articles, - required List this.upcomingEvents, - }) : message = null, - isLoading = false; - - const WelcomeState.loading({this.slides, this.articles, this.upcomingEvents}) - : message = null, - isLoading = true; - - const WelcomeState.failure({required String this.message}) - : slides = null, - articles = null, - upcomingEvents = null, - isLoading = false; -} - -class WelcomeCubit extends Cubit { - final ApiRepository api; - - WelcomeCubit(this.api) : super(const WelcomeState.loading()); - - Future load() async { - emit(state.copyWith(isLoading: true)); - try { - final slidesResponse = await api.getSlides(); - final articlesResponse = await api.getFrontpageArticles(); - final eventsResponse = await api.getEvents( - start: DateTime.now(), - ordering: 'start', - limit: 3, - ); - final partnerEventsResponse = await api.getPartnerEvents( - start: DateTime.now(), - ordering: 'start', - limit: 3, - ); - - List events = eventsResponse.results - .map((e) => e) - .followedBy(partnerEventsResponse.results.map((e) => e)) - .sortedBy((element) => element.start) - .take(3) - .toList(); - - // Filter out SVG slides, as long as concrexit does not offer an alternative. - final slides = slidesResponse.results - .where((slide) => !Uri.parse(slide.content.full).path.endsWith('svg')) - .toList(); - - emit(WelcomeState.result( - slides: slides, - articles: articlesResponse.results, - upcomingEvents: events, - )); - } on ApiException catch (exception) { - emit(WelcomeState.failure(message: exception.message)); - } - } -} diff --git a/lib/main.dart b/lib/main.dart index 7bf9db44a..db7381ea3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -14,10 +13,8 @@ import 'package:reaxit/config.dart'; import 'package:reaxit/firebase_options.dart'; import 'package:reaxit/routes.dart'; import 'package:reaxit/ui/theme.dart'; -import 'package:reaxit/ui/widgets.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:firebase_core/firebase_core.dart'; -import 'package:url_launcher/url_launcher.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -44,14 +41,12 @@ Future main() async { options.dsn = sentryDSN; }, appRunner: () async { - runApp(BlocProvider( - create: (_) => ThemeCubit()..load(), - lazy: false, - child: BlocProvider( + runApp( + BlocProvider( create: (context) => AuthCubit()..load(), child: const ThaliApp(), ), - )); + ); }, ); } @@ -77,10 +72,8 @@ Future testingMain(AuthCubit? authCubit, String? initialroute) async { await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); - runApp(BlocProvider( - create: (_) => ThemeCubit()..load(), - lazy: false, - child: authCubit == null + runApp( + authCubit == null ? BlocProvider( create: (context) => AuthCubit()..load(), child: ThaliApp( @@ -92,7 +85,7 @@ Future testingMain(AuthCubit? authCubit, String? initialroute) async { child: ThaliApp( initialRoute: initialroute, )), - )); + ); } class GoRouterRefreshStream extends ChangeNotifier { @@ -122,70 +115,6 @@ class _ThaliAppState extends State { late final GoRouter _router; late final AuthCubit _authCubit; - Future _setupPushNotificationHandlers() async { - // User got a push notification while the app is running. - // Display a notification inside the app. - FirebaseMessaging.onMessage.listen((RemoteMessage message) { - showOverlayNotification( - (context) => PushNotificationOverlay(message), - duration: const Duration(milliseconds: 4000), - ); - }); - - // User clicked on push notification outside of the app and the - // app was still in the background. Open the url or show a dialog. - FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) async { - final navigatorKey = _router.routerDelegate.navigatorKey; - if (message.data.containsKey('url') && message.data['url'] is String) { - Uri? uri = Uri.tryParse(message.data['url'] as String); - if (uri != null) { - if (uri.scheme.isEmpty) uri = uri.replace(scheme: 'https'); - if (isDeepLink(uri)) { - _router.go(Uri( - path: uri.path, - query: uri.query, - ).toString()); - } else { - await launchUrl(uri, mode: LaunchMode.externalApplication); - } - } - } else if (navigatorKey.currentContext != null) { - showDialog( - context: navigatorKey.currentContext!, - builder: (context) => PushNotificationDialog(message), - ); - } - }); - - final initialMessage = await FirebaseMessaging.instance.getInitialMessage(); - - // User got a push notification outside of the app while the app was not - // running in the background. Open the url or show a dialog. - if (initialMessage != null) { - final navigatorKey = _router.routerDelegate.navigatorKey; - final message = initialMessage; - if (message.data.containsKey('url') && message.data['url'] is String) { - Uri? uri = Uri.tryParse(message.data['url'] as String); - if (uri != null) { - if (uri.scheme.isEmpty) uri = uri.replace(scheme: 'https'); - if (isDeepLink(uri)) { - _router.go(Uri( - path: uri.path, - query: uri.query, - ).toString()); - } else { - await launchUrl(uri, mode: LaunchMode.externalApplication); - } - } - } else if (navigatorKey.currentContext != null) { - showDialog( - context: navigatorKey.currentContext!, - builder: (context) => PushNotificationDialog(message), - ); - } - } - } - @override void initState() { super.initState(); @@ -227,8 +156,6 @@ class _ThaliAppState extends State { initialLocation: widget.initialRoute, ); - - _setupPushNotificationHandlers(); } @override @@ -239,104 +166,89 @@ class _ThaliAppState extends State { @override Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, themeMode) { - return OverlaySupport.global( - child: MaterialApp.router( - title: 'ThaliApp', - theme: lightTheme, - darkTheme: darkTheme, - themeMode: themeMode, - routerDelegate: _router.routerDelegate, - routeInformationParser: _router.routeInformationParser, - routeInformationProvider: _router.routeInformationProvider, - - // This adds listeners for authentication status snackbars and setting up - // push notifications. This surrounds the navigator with providers when - // logged in, and replaces it with a [LoginScreen] when not logged in. - builder: (context, navigator) { - return BlocConsumer( - listenWhen: (previous, current) { - if (previous is LoggedInAuthState && - current is LoggedOutAuthState) { - return true; - } else if (current is FailureAuthState) { - return true; - } - return false; - }, - - // Listen to display login status snackbars and set up notifications. - listener: (context, state) async { - // Show a snackbar when the user logs out or logging in fails. - switch (state) { - case LoggedOutAuthState _: - ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - behavior: SnackBarBehavior.floating, - content: Text('Logged out.'), - )); - case FailureAuthState(message: var message): - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - behavior: SnackBarBehavior.floating, - content: Text(message ?? 'Logging in failed.'), - )); - case _: - } - }, - buildWhen: (previous, current) => current is! FailureAuthState, - builder: (context, authState) { - // Build with ApiRepository and cubits provided when an - // ApiRepository is available. This is the case when logged - // in, but also when just logged out (after having been logged - // in), with a closed ApiRepository. - // The latter allows us to keep the cubits alive - // while animating towards the login screen. - if (authState is LoggedInAuthState || - (authState is LoggedOutAuthState && - authState.apiRepository != null)) { - final ApiRepository apiRepository; - if (authState is LoggedInAuthState) { - apiRepository = authState.apiRepository; - } else { - apiRepository = - (authState as LoggedOutAuthState).apiRepository!; - } + return OverlaySupport.global( + child: MaterialApp.router( + title: 'ThaliApp', + theme: lightTheme, + darkTheme: darkTheme, + themeMode: ThemeMode.dark, + routerDelegate: _router.routerDelegate, + routeInformationParser: _router.routeInformationParser, + routeInformationProvider: _router.routeInformationProvider, + + // This adds listeners for authentication status snackbars and setting up + // push notifications. This surrounds the navigator with providers when + // logged in, and replaces it with a [LoginScreen] when not logged in. + builder: (context, navigator) { + return BlocConsumer( + listenWhen: (previous, current) { + if (previous is LoggedInAuthState && + current is LoggedOutAuthState) { + return true; + } else if (current is FailureAuthState) { + return true; + } + return false; + }, - return InheritedConfig( - config: apiRepository.config, - child: RepositoryProvider.value( - value: apiRepository, - child: MultiBlocProvider( - providers: [ - BlocProvider( - create: (_) => - FullMemberCubit(apiRepository)..load(), - lazy: false, - ), - BlocProvider( - create: (_) => - WelcomeCubit(apiRepository)..load(), - lazy: false, - ), - BlocProvider( - create: (_) => - AlbumListCubit(apiRepository)..load(), - lazy: false, - ), - ], - child: navigator!, + // Listen to display login status snackbars and set up notifications. + listener: (context, state) async { + // Show a snackbar when the user logs out or logging in fails. + switch (state) { + case LoggedOutAuthState _: + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + behavior: SnackBarBehavior.floating, + content: Text('Logged out.'), + )); + case FailureAuthState(message: var message): + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + behavior: SnackBarBehavior.floating, + content: Text(message ?? 'Logging in failed.'), + )); + case _: + } + }, + buildWhen: (previous, current) => current is! FailureAuthState, + builder: (context, authState) { + // Build with ApiRepository and cubits provided when an + // ApiRepository is available. This is the case when logged + // in, but also when just logged out (after having been logged + // in), with a closed ApiRepository. + // The latter allows us to keep the cubits alive + // while animating towards the login screen. + if (authState is LoggedInAuthState || + (authState is LoggedOutAuthState && + authState.apiRepository != null)) { + final ApiRepository apiRepository; + if (authState is LoggedInAuthState) { + apiRepository = authState.apiRepository; + } else { + apiRepository = + (authState as LoggedOutAuthState).apiRepository!; + } + + return InheritedConfig( + config: apiRepository.config, + child: RepositoryProvider.value( + value: apiRepository, + child: MultiBlocProvider( + providers: [ + BlocProvider( + create: (_) => AlbumListCubit(apiRepository)..load(), + lazy: false, ), - ), - ); - } else { - return navigator!; - } - }, - ); + ], + child: navigator!, + ), + ), + ); + } else { + return navigator!; + } }, - ), - ); - }, + ); + }, + ), ); } } diff --git a/lib/models/album.dart b/lib/models/album.dart index 1e6e35a11..06dd6c1a5 100644 --- a/lib/models/album.dart +++ b/lib/models/album.dart @@ -9,10 +9,8 @@ class ListAlbum { final String title; final bool accessible; final bool shareable; - final CoverPhoto? cover; - const ListAlbum( - this.slug, this.title, this.accessible, this.shareable, this.cover); + const ListAlbum(this.slug, this.title, this.accessible, this.shareable); factory ListAlbum.fromJson(Map json) => _$ListAlbumFromJson(json); @@ -35,15 +33,14 @@ class Album extends ListAlbum { title ?? this.title, accessible ?? this.accessible, shareable ?? this.shareable, - cover ?? this.cover, photos ?? this.photos, ); - const Album.fromlist(super.slug, super.title, super.accessible, - super.shareable, CoverPhoto super.cover, this.photos); + const Album.fromlist( + super.slug, super.title, super.accessible, super.shareable, this.photos); - const Album(super.slug, super.title, super.accessible, super.shareable, - super.cover, this.photos); + const Album( + super.slug, super.title, super.accessible, super.shareable, this.photos); factory Album.fromJson(Map json) => _$AlbumFromJson(json); } diff --git a/lib/models/album.g.dart b/lib/models/album.g.dart index a9fe6b5f8..b5f04be9b 100644 --- a/lib/models/album.g.dart +++ b/lib/models/album.g.dart @@ -11,9 +11,6 @@ ListAlbum _$ListAlbumFromJson(Map json) => ListAlbum( json['title'] as String, json['accessible'] as bool, json['shareable'] as bool, - json['cover'] == null - ? null - : CoverPhoto.fromJson(json['cover'] as Map), ); Map _$ListAlbumToJson(ListAlbum instance) => { @@ -21,7 +18,6 @@ Map _$ListAlbumToJson(ListAlbum instance) => { 'title': instance.title, 'accessible': instance.accessible, 'shareable': instance.shareable, - 'cover': instance.cover, }; Album _$AlbumFromJson(Map json) => Album( @@ -29,9 +25,6 @@ Album _$AlbumFromJson(Map json) => Album( json['title'] as String, json['accessible'] as bool, json['shareable'] as bool, - json['cover'] == null - ? null - : CoverPhoto.fromJson(json['cover'] as Map), (json['photos'] as List) .map((e) => AlbumPhoto.fromJson(e as Map)) .toList(), @@ -42,6 +35,5 @@ Map _$AlbumToJson(Album instance) => { 'title': instance.title, 'accessible': instance.accessible, 'shareable': instance.shareable, - 'cover': instance.cover, 'photos': instance.photos, }; diff --git a/lib/routes.dart b/lib/routes.dart index 0cbb9cfea..69c10f93a 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -2,59 +2,9 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:reaxit/models.dart'; import 'package:reaxit/ui/screens.dart'; -import 'package:reaxit/ui/widgets.dart'; - -/// Returns true if [uri] is a deep link that can be handled by the app. -bool isDeepLink(Uri uri) { - if (uri.host case 'thalia.nu' || 'staging.thalia.nu') { - return _deepLinkRegExps.any((re) => re.hasMatch(uri.path)); - } - return false; -} - -const _uuid = '([a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12})'; - -// Any route added here also needs to be added to -// android/app/src/main/AndroidManifest.xml and -// android/app/src/debug/AndroidManifest.xml - -/// The [RegExp]s that can used as deep links. This list should -/// contain all deep links that should be handled by the app. -final List _deepLinkRegExps = [ - RegExp('^/\$'), - RegExp('^/pizzas/?\$'), - RegExp('^/events/?\$'), - RegExp('^/events/([0-9]+)/?\$'), - RegExp('^/members/photos/?\$'), - RegExp('^/members/photos/liked/?\$'), - RegExp('^/members/photos/([a-z0-9-_]+)/?\$'), - RegExp('^/sales/order/$_uuid/pay/?\$'), - RegExp('^/events/([0-9]+)/mark-present/$_uuid/?\$'), - RegExp('^/association/societies(/[0-9]+)?/?\$'), - RegExp('^/association/committees(/[0-9]+)?/?\$'), - RegExp('^/association/boards/([0-9]{4}-[0-9]{4})/?\$'), -]; +import 'package:reaxit/ui/screens/album_screen.dart'; final List routes = [ - GoRoute( - path: '/', - name: 'welcome', - pageBuilder: (context, state) => CustomTransitionPage( - key: state.pageKey, - child: WelcomeScreen(), - transitionDuration: const Duration(milliseconds: 200), - transitionsBuilder: (context, animation, secondaryAnimation, child) { - return FadeTransition( - opacity: animation.drive(CurveTween(curve: Curves.easeIn)), - child: child, - ); - }, - ), - ), - GoRoute( - path: '/members/photos/liked', - redirect: (context, state) => '/albums/liked-photos', - ), GoRoute( // This redirect is above the members route because // the members path is a prefix of this albums path. @@ -96,12 +46,4 @@ final List routes = [ ), ], ), - GoRoute( - path: '/login', - name: 'login', - pageBuilder: (context, state) => MaterialPage( - key: state.pageKey, - child: const LoginScreen(), - ), - ), ]; diff --git a/lib/ui/screens.dart b/lib/ui/screens.dart index 29c2443a4..44aa61186 100644 --- a/lib/ui/screens.dart +++ b/lib/ui/screens.dart @@ -1,4 +1 @@ -export 'screens/album_screen.dart'; export 'screens/albums_screen.dart'; -export 'screens/login_screen.dart'; -export 'screens/welcome_screen.dart'; diff --git a/lib/ui/screens/album_screen.dart b/lib/ui/screens/album_screen.dart index bb34f05e3..2395b2b12 100644 --- a/lib/ui/screens/album_screen.dart +++ b/lib/ui/screens/album_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:reaxit/blocs.dart'; import 'package:reaxit/api/api_repository.dart'; +import 'package:reaxit/blocs/album_cubit.dart'; import 'package:reaxit/config.dart'; import 'package:reaxit/models.dart'; import 'package:reaxit/ui/widgets.dart'; diff --git a/lib/ui/screens/login_screen.dart b/lib/ui/screens/login_screen.dart deleted file mode 100644 index 12117a1f9..000000000 --- a/lib/ui/screens/login_screen.dart +++ /dev/null @@ -1,177 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:reaxit/blocs.dart'; -import 'package:reaxit/config.dart'; -import 'package:reaxit/ui/theme.dart'; - -class LoginScreen extends StatefulWidget { - const LoginScreen({super.key}); - - @override - State createState() => _LoginScreenState(); -} - -class _LoginScreenState extends State { - ImageProvider logo = const AssetImage('assets/img/logo.png'); - - @override - void didChangeDependencies() { - precacheImage(logo, context); - super.didChangeDependencies(); - } - - @override - Widget build(BuildContext context) { - return BlocBuilder( - buildWhen: (previous, current) => - current is LoggedOutAuthState || current is LoadingAuthState, - builder: (context, authState) { - if (authState is LoggedOutAuthState) { - return Scaffold( - backgroundColor: magenta, - body: SafeArea( - child: Stack( - children: [ - Align( - alignment: Alignment.topRight, - child: IconButton( - color: Colors.white54, - padding: const EdgeInsets.all(16), - icon: const Icon(Icons.build_rounded), - tooltip: - "Select environment, you probably don't need this", - onPressed: () { - showDialog( - context: context, - builder: (context) => const SelectEnvironmentDialog(), - ); - }, - ), - ), - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Center(child: Image(image: logo, width: 260)), - const SizedBox(height: 50), - SizedBox( - height: 50, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.black87, - foregroundColor: Colors.white, - ), - onPressed: () { - BlocProvider.of(context) - .logIn(authState.selectedEnvironment); - }, - child: Text(switch (authState.selectedEnvironment) { - Environment.production => 'LOG IN', - Environment.staging => 'LOG IN - STAGING', - Environment.local => 'LOG IN - LOCAL', - }), - ), - ), - ], - ), - ], - ), - ), - ); - } else { - return Scaffold( - backgroundColor: const Color(0xFFE62272), - body: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Center(child: Image(image: logo, width: 260)), - const SizedBox(height: 50), - const SizedBox( - height: 50, - child: Center( - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(Colors.white), - ), - ), - ), - ], - ), - ); - } - }, - ); - } -} - -class SelectEnvironmentDialog extends StatelessWidget { - const SelectEnvironmentDialog({super.key}); - - @override - Widget build(BuildContext context) { - return AlertDialog( - title: const Text('SELECT ENVIRONMENT'), - content: BlocBuilder( - buildWhen: (previous, current) => current is LoggedOutAuthState, - builder: (context, state) { - final selectedEnvironment = state is LoggedOutAuthState - ? state.selectedEnvironment - : Environment.defaultEnvironment; - return Column(mainAxisSize: MainAxisSize.min, children: [ - Text( - 'Select an alternative server to log in to. ' - 'If you are not sure you need this, just use production.', - style: Theme.of(context).textTheme.bodyMedium, - ), - const SizedBox(height: 12), - const Divider(height: 0), - if (Config.production != null) - RadioListTile( - title: const Text('PRODUCTION'), - subtitle: const Text('Default: thalia.nu'), - value: Environment.production, - groupValue: selectedEnvironment, - onChanged: (environment) { - BlocProvider.of(context).selectEnvironment( - Environment.production, - ); - }, - ), - RadioListTile( - title: const Text('STAGING'), - value: Environment.staging, - subtitle: const Text( - 'Used by the Technicie for testing: staging.thalia.nu', - ), - groupValue: selectedEnvironment, - onChanged: (environment) { - BlocProvider.of(context).selectEnvironment( - Environment.staging, - ); - }, - ), - if (Config.local != null) - RadioListTile( - title: const Text('LOCAL'), - subtitle: const Text('You should know what you are doing.'), - value: Environment.local, - groupValue: selectedEnvironment, - onChanged: (environment) { - BlocProvider.of(context).selectEnvironment( - Environment.local, - ); - }, - ), - ]); - }, - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text('CLOSE'), - ) - ], - ); - } -} diff --git a/lib/ui/screens/welcome_screen.dart b/lib/ui/screens/welcome_screen.dart deleted file mode 100644 index cc0e03322..000000000 --- a/lib/ui/screens/welcome_screen.dart +++ /dev/null @@ -1,275 +0,0 @@ -import 'package:carousel_slider/carousel_slider.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart'; -import 'package:go_router/go_router.dart'; -import 'package:intl/intl.dart'; -import 'package:reaxit/blocs.dart'; -import 'package:reaxit/models.dart'; -import 'package:reaxit/routes.dart'; -import 'package:reaxit/ui/widgets.dart'; -import 'package:collection/collection.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class WelcomeScreen extends StatefulWidget { - @override - State createState() => _WelcomeScreenState(); -} - -class _WelcomeScreenState extends State { - static final dateFormatter = DateFormat('EEEE d MMMM'); - - static Map> _groupByDay(List events) { - return groupBy( - events, - (event) => DateTime( - event.start.year, - event.start.month, - event.start.day, - ), - ); - } - - Widget _makeSlides(List slides) { - return AnimatedSize( - curve: Curves.ease, - duration: const Duration(milliseconds: 300), - child: - slides.isNotEmpty ? SlidesCarousel(slides) : const SizedBox.shrink(), - ); - } - - Widget _makeArticle(FrontpageArticle article) { - return Padding( - padding: const EdgeInsets.symmetric( - vertical: 12, - ), - child: Column( - children: [ - Text( - article.title.toUpperCase(), - style: Theme.of(context).textTheme.titleMedium, - ), - HtmlWidget( - article.content, - onTapUrl: (String url) async { - Uri uri = Uri(path: url); - if (uri.scheme.isEmpty) uri = uri.replace(scheme: 'https'); - if (isDeepLink(uri)) { - context.go(Uri( - path: uri.path, - query: uri.query, - ).toString()); - return true; - } else { - final messenger = ScaffoldMessenger.of(context); - try { - await launchUrl(uri, mode: LaunchMode.externalApplication); - } catch (_) { - messenger.showSnackBar(SnackBar( - behavior: SnackBarBehavior.floating, - content: Text('Could not open "$url".'), - )); - } - return true; - } - }, - ), - ], - ), - ); - } - - Widget _makeArticles(List articles) { - return AnimatedSize( - curve: Curves.ease, - duration: const Duration(milliseconds: 300), - child: articles.isNotEmpty - ? Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _makeArticle(articles.first), - for (final article in articles.skip(1)) ...[ - const Divider(height: 8), - _makeArticle(article), - ] - ], - ), - ) - : const SizedBox.shrink(), - ); - } - - Widget _makeUpcomingEvents(List events) { - final dayGroupedEvents = _groupByDay(events); - return AnimatedSize( - curve: Curves.ease, - duration: const Duration(milliseconds: 300), - child: Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - 'UPCOMING EVENTS', - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.titleMedium, - ), - ...dayGroupedEvents.entries.map((entry) { - final now = DateTime.now(); - final today = DateTime(now.year, now.month, now.day); - final day = entry.key; - final dayEvents = entry.value; - String dayText; - switch (day.difference(today).inDays) { - case 0: - dayText = 'TODAY'; - break; - case 1: - dayText = 'TOMORROW'; - break; - default: - dayText = dateFormatter.format(day).toUpperCase(); - } - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 8), - Text( - dayText, - textAlign: TextAlign.left, - style: Theme.of(context).textTheme.bodySmall, - ), - for (final event in dayEvents) - EventDetailCard(event: event), - ]); - }), - if (events.isEmpty) - const Padding( - padding: EdgeInsets.all(8), - child: Text( - 'There are no upcoming events.', - textAlign: TextAlign.center, - ), - ), - ], - ), - ), - ); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: ThaliaAppBar(title: const Text('WELCOME')), - drawer: MenuDrawer(), - body: RefreshIndicator( - onRefresh: () => BlocProvider.of(context).load(), - child: BlocBuilder( - builder: (context, state) { - if (state.hasException) { - return ErrorScrollView(state.message!); - } else if (!state.hasResults) { - return const Center(child: CircularProgressIndicator()); - } else { - return Scrollbar( - child: ListView( - key: const PageStorageKey('welcome'), - physics: const AlwaysScrollableScrollPhysics(), - children: [ - _makeSlides(state.slides!), - if (state.slides!.isNotEmpty) const Divider(height: 0), - _makeArticles(state.articles!), - if (state.articles!.isNotEmpty) - const Divider(indent: 16, endIndent: 16, height: 8), - _makeUpcomingEvents(state.upcomingEvents!), - TextButton( - onPressed: () => context.goNamed('calendar'), - child: const Text('SHOW THE ENTIRE AGENDA'), - ), - ], - )); - } - }, - ), - ), - ); - } -} - -class SlidesCarousel extends StatefulWidget { - final List slides; - - const SlidesCarousel( - this.slides, { - super.key, - }); - - @override - State createState() => _SlidesCarouselState(); -} - -class _SlidesCarouselState extends State { - int _current = 0; - - @override - Widget build(BuildContext context) { - return Stack( - alignment: Alignment.bottomCenter, - children: [ - CarouselSlider.builder( - options: CarouselOptions( - aspectRatio: 1075 / 430, - viewportFraction: 1, - autoPlay: true, - autoPlayInterval: const Duration(seconds: 6), - onPageChanged: (index, _) => setState(() { - _current = index; - }), - ), - itemCount: widget.slides.length, - itemBuilder: (context, index, _) { - final slide = widget.slides[index]; - return InkWell( - onTap: slide.url != null - ? () async { - await launchUrl( - slide.url!, - mode: LaunchMode.externalApplication, - ); - } - : null, - child: CachedImage( - imageUrl: slide.content.full, - placeholder: 'assets/img/slide_placeholder.png', - ), - ); - }, - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: List.generate( - widget.slides.length, - (index) => AnimatedContainer( - duration: const Duration(milliseconds: 300), - width: 6, - height: 6, - margin: const EdgeInsets.symmetric( - vertical: 6, - horizontal: 2, - ), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Theme.of(context).dividerColor.withOpacity( - _current == index ? 0.6 : 0.4, - ), - ), - ), - ), - ) - ], - ); - } -} diff --git a/lib/ui/widgets.dart b/lib/ui/widgets.dart index 78b94a813..855017189 100644 --- a/lib/ui/widgets.dart +++ b/lib/ui/widgets.dart @@ -1,12 +1,7 @@ export 'widgets/album_tile.dart'; export 'widgets/app_bar.dart'; -export 'widgets/cached_image.dart'; export 'widgets/error_center.dart'; export 'widgets/error_scroll_view.dart'; export 'widgets/event_detail_card.dart'; -export 'widgets/member_tile.dart'; export 'widgets/menu_drawer.dart'; -export 'widgets/push_notification_dialog.dart'; -export 'widgets/push_notification_overlay.dart'; -export 'widgets/sortbutton.dart'; export 'widgets/filter_popup.dart'; diff --git a/lib/ui/widgets/album_tile.dart b/lib/ui/widgets/album_tile.dart index 6896f1788..931f04e69 100644 --- a/lib/ui/widgets/album_tile.dart +++ b/lib/ui/widgets/album_tile.dart @@ -2,8 +2,6 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:reaxit/models.dart'; -import 'cached_image.dart'; - class AlbumTile extends StatelessWidget { final ListAlbum album; @@ -11,20 +9,10 @@ class AlbumTile extends StatelessWidget { @override Widget build(BuildContext context) { - final cover = album.cover; return Stack( fit: StackFit.expand, children: [ - if (cover != null) - RotatedBox( - quarterTurns: cover.rotation ~/ 90, - child: CachedImage( - placeholder: - 'assets/img/photo_placeholder_${(360 - cover.rotation) % 360}.png', - imageUrl: cover.small, - ), - ), - if (cover == null) Image.asset('assets/img/photo_placeholder_0.png'), + Image.asset('assets/img/photo_placeholder_0.png'), const _BlackGradient(), Align( alignment: Alignment.bottomLeft, diff --git a/lib/ui/widgets/cached_image.dart b/lib/ui/widgets/cached_image.dart deleted file mode 100644 index eaae7176c..000000000 --- a/lib/ui/widgets/cached_image.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter/widgets.dart'; -import 'package:reaxit/utilities/cache_manager.dart' as cache; - -/// Wrapper for [CachedNetworkImage] with sensible defaults. -class CachedImage extends CachedNetworkImage { - CachedImage({ - required super.imageUrl, - BoxFit super.fit = BoxFit.cover, - Duration super.fadeOutDuration = const Duration(milliseconds: 200), - super.fadeInDuration = const Duration(milliseconds: 200), - required String placeholder, - }) : super( - key: ValueKey(imageUrl), - cacheManager: cache.ThaliaCacheManager(), - cacheKey: _getCacheKey(imageUrl), - placeholder: (_, __) => Image.asset(placeholder, fit: fit), - ); -} - -/// Wrapper for [CachedNetworkImageProvider] with sensible defaults. -class CachedImageProvider extends CachedNetworkImageProvider { - CachedImageProvider(super.imageUrl) - : super( - cacheManager: cache.ThaliaCacheManager(), - cacheKey: _getCacheKey(imageUrl), - ); -} - -/// If the image is from thalia.nu, remove the query part of the url from its -/// key in the cache. Private images from concrexit have a signature in the url -/// that expires every few hours. Removing this signature makes sure that the -/// same cache object can be used regardless of the signature. -/// -/// This assumes that the query part is only used for authentication, -/// not to identify the image, so the remaining path is a unique key. -/// -/// If the url is not from thalia.nu, use the full url as the key. -String _getCacheKey(String url) { - final uri = Uri.parse(url); - if (uri.host - case 'thalia.nu' || - 'staging.thalia.nu' || - 'cdn.thalia.nu' || - 'cdn.staging.thalia.nu') { - return uri.replace(query: '').toString(); - } - return url; -} diff --git a/lib/ui/widgets/file_button.dart b/lib/ui/widgets/file_button.dart deleted file mode 100644 index 547115594..000000000 --- a/lib/ui/widgets/file_button.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:reaxit/utilities/cache_manager.dart'; -import 'package:open_file_plus/open_file_plus.dart'; - -class FileButton extends StatelessWidget { - final Uri url; - final String name; - final String extension; - - FileButton({ - required String url, - required this.name, - }) : extension = Uri.parse(url).path.split('.').last, - url = Uri.parse(url); - - @override - Widget build(BuildContext context) { - return ElevatedButton.icon( - onPressed: () async { - var file = (await ThaliaCacheManager() - .getFileFromCache('${url.origin}${url.path}')) - ?.file; - - if (file == null) { - var newFile = await ThaliaCacheManager() - .downloadFile(url.toString(), key: name); - file = await ThaliaCacheManager().putFile( - '${url.origin}${url.path}', await newFile.file.readAsBytes(), - fileExtension: extension); - - await ThaliaCacheManager().removeFile(name); - } - - OpenFile.open(file.path); - }, - icon: const Icon(Icons.description), - label: Text(name), - ); - } -} diff --git a/lib/ui/widgets/member_tile.dart b/lib/ui/widgets/member_tile.dart deleted file mode 100644 index 42403fbca..000000000 --- a/lib/ui/widgets/member_tile.dart +++ /dev/null @@ -1,112 +0,0 @@ -import 'package:animations/animations.dart'; -import 'package:flutter/material.dart'; -import 'package:reaxit/models.dart'; -import 'package:reaxit/ui/widgets.dart'; - -class MemberTile extends StatelessWidget { - final ListMember member; - - MemberTile({required this.member}) : super(key: ValueKey(member.pk)); - - @override - Widget build(BuildContext context) { - return OpenContainer( - tappable: false, - routeSettings: RouteSettings(name: 'Profile(${member.pk})'), - transitionType: ContainerTransitionType.fadeThrough, - closedShape: const RoundedRectangleBorder(), - closedBuilder: (context, openContainer) { - return Stack( - fit: StackFit.expand, - children: [ - CachedImage( - imageUrl: member.photo.small, - placeholder: 'assets/img/default-avatar.jpg', - ), - const _BlackGradient(), - Align( - alignment: Alignment.bottomLeft, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - member.displayName, - style: Theme.of(context).primaryTextTheme.bodyMedium, - ), - ), - ), - Positioned.fill( - child: Material( - color: Colors.transparent, - child: InkWell(onTap: openContainer), - ), - ), - ], - ); - }, - openBuilder: (_, __) => Container(), - ); - } -} - -class _BlackGradient extends StatelessWidget { - static const _black00 = Color(0x00000000); - static const _black50 = Color(0x80000000); - - const _BlackGradient(); - - @override - Widget build(BuildContext context) { - return const DecoratedBox( - decoration: BoxDecoration( - color: Colors.black, - gradient: LinearGradient( - begin: FractionalOffset.topCenter, - end: FractionalOffset.bottomCenter, - colors: [_black00, _black50], - stops: [0.4, 1.0], - ), - ), - ); - } -} - -/// A replacement for [MemberTile] for when the person is not actually a member. -class DefaultMemberTile extends StatelessWidget { - final String name; - - const DefaultMemberTile({super.key, required this.name}); - - @override - Widget build(BuildContext context) { - return Stack( - fit: StackFit.expand, - children: [ - Image.asset('assets/img/default-avatar.jpg'), - const _BlackGradient(), - Align( - alignment: Alignment.bottomLeft, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - name, - style: Theme.of(context).primaryTextTheme.bodyMedium, - ), - ), - ), - Positioned.fill( - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: () { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - behavior: SnackBarBehavior.floating, - content: Text('$name is not a member.'), - )); - }, - ), - ), - ), - ], - ); - } -} diff --git a/lib/ui/widgets/menu_drawer.dart b/lib/ui/widgets/menu_drawer.dart index 52dd0aae9..a4e4795ed 100644 --- a/lib/ui/widgets/menu_drawer.dart +++ b/lib/ui/widgets/menu_drawer.dart @@ -1,8 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; -import 'package:reaxit/blocs.dart'; -import 'package:reaxit/ui/widgets.dart'; import 'package:reaxit/config.dart' as config; class MenuDrawer extends StatelessWidget { @@ -19,142 +16,6 @@ class MenuDrawer extends StatelessWidget { primary: false, padding: EdgeInsets.zero, children: [ - BlocBuilder( - builder: (context, state) { - if (state.result != null) { - final me = state.result!; - return Stack( - children: [ - Container( - height: 180, - decoration: const BoxDecoration( - image: DecorationImage( - image: AssetImage('assets/img/huygens.jpg'), - fit: BoxFit.cover, - ), - ), - ), - Container( - height: 180, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: FractionalOffset.bottomCenter, - end: FractionalOffset.topCenter, - colors: [ - Colors.black.withOpacity(0.8), - Colors.transparent - ], - ), - ), - ), - Positioned( - left: 16, - bottom: 8, - child: Text( - me.displayName, - style: Theme.of(context).primaryTextTheme.headlineSmall, - ), - ), - SafeArea( - minimum: const EdgeInsets.all(16), - child: Container( - width: 80, - height: 80, - decoration: BoxDecoration( - image: DecorationImage( - image: CachedImageProvider( - me.photo.medium, - ), - fit: BoxFit.cover, - ), - borderRadius: BorderRadius.circular(40), - boxShadow: const [ - BoxShadow( - offset: Offset(1, 2), - blurRadius: 8, - ), - ], - ), - ), - ), - Positioned.fill( - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: () => context.pushNamed( - 'member', - pathParameters: {'memberPk': me.pk.toString()}, - extra: me, - ), - ), - ), - ), - ], - ); - } else { - return Stack( - children: [ - Container( - height: 180, - decoration: const BoxDecoration( - image: DecorationImage( - image: AssetImage('assets/img/huygens.jpg'), - fit: BoxFit.cover, - ), - ), - ), - Container( - height: 180, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: FractionalOffset.bottomCenter, - end: FractionalOffset.topCenter, - colors: [ - Colors.black.withOpacity(0.8), - Colors.transparent - ], - ), - ), - ), - Positioned( - left: 16, - bottom: 8, - child: Text( - 'Loading...', - style: Theme.of(context).primaryTextTheme.headlineSmall, - ), - ), - SafeArea( - minimum: const EdgeInsets.all(16), - child: Container( - width: 80, - height: 80, - decoration: BoxDecoration( - image: const DecorationImage( - image: AssetImage('assets/img/default-avatar.jpg'), - fit: BoxFit.cover, - ), - borderRadius: BorderRadius.circular(40), - boxShadow: const [ - BoxShadow( - offset: Offset(1, 2), - blurRadius: 8, - ), - ], - ), - ), - ), - Positioned.fill( - child: Material( - color: Colors.transparent, - child: InkWell(onTap: () {}), - ), - ), - ], - ); - } - }, - ), const Divider(height: 0, thickness: 1), ListTile( title: const Text('Welcome'), diff --git a/lib/ui/widgets/push_notification_dialog.dart b/lib/ui/widgets/push_notification_dialog.dart deleted file mode 100644 index d1cb1dc66..000000000 --- a/lib/ui/widgets/push_notification_dialog.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:reaxit/routes.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class PushNotificationDialog extends StatelessWidget { - final RemoteMessage message; - PushNotificationDialog(this.message) : super(key: ObjectKey(message)); - - @override - Widget build(BuildContext context) { - Uri? uri; - if (message.data.containsKey('url') && message.data['url'] is String) { - uri = Uri.tryParse(message.data['url'] as String); - if (uri?.scheme.isEmpty ?? false) uri = uri!.replace(scheme: 'https'); - } - - return AlertDialog( - title: Text(message.notification?.title ?? 'Notification'), - content: (message.notification?.body != null && - message.notification!.body!.isNotEmpty) - ? Text( - message.notification!.body!, - style: Theme.of(context).textTheme.bodyMedium, - ) - : null, - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('CLOSE'), - ), - if (uri != null) - OutlinedButton( - onPressed: () async { - Navigator.of(context).pop(); - if (isDeepLink(uri!)) { - context.go(Uri(path: uri.path, query: uri.query).toString()); - } else { - await launchUrl(uri, mode: LaunchMode.externalApplication); - } - }, - child: const Text('OPEN'), - ), - ], - ); - } -} diff --git a/lib/ui/widgets/push_notification_overlay.dart b/lib/ui/widgets/push_notification_overlay.dart deleted file mode 100644 index 308bcb49d..000000000 --- a/lib/ui/widgets/push_notification_overlay.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:overlay_support/overlay_support.dart'; -import 'package:reaxit/routes.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class PushNotificationOverlay extends StatelessWidget { - final RemoteMessage message; - PushNotificationOverlay(this.message) : super(key: ObjectKey(message)); - - @override - Widget build(BuildContext context) { - Uri? uri; - if (message.data.containsKey('url') && message.data['url'] is String) { - uri = Uri.tryParse(message.data['url'] as String); - if (uri?.scheme.isEmpty ?? false) uri = uri!.replace(scheme: 'https'); - } - - return SafeArea( - child: Card( - child: ListTile( - onTap: uri != null - ? () async { - if (isDeepLink(uri!)) { - context.go(Uri( - path: uri.path, - query: uri.query, - ).toString()); - } else { - await launchUrl(uri, mode: LaunchMode.externalApplication); - } - } - : null, - title: Text(message.notification!.title ?? '', maxLines: 1), - subtitle: Text(message.notification!.body ?? '', maxLines: 2), - trailing: IconButton( - icon: const Icon(Icons.close), - onPressed: () => OverlaySupportEntry.of(context)!.dismiss(), - ), - ), - ), - ); - } -} diff --git a/lib/ui/widgets/sortbutton.dart b/lib/ui/widgets/sortbutton.dart deleted file mode 100644 index ecc1948d6..000000000 --- a/lib/ui/widgets/sortbutton.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'app_bar.dart'; - -class SortItem { - final T value; - final String text; - final IconData? icon; - - const SortItem(this.value, this.text, this.icon); -} - -class SortButton extends StatelessWidget implements AppbarAction { - final void Function(T?) callback; - final List> items; - - const SortButton(this.items, this.callback); - - Widget _build(BuildContext context, bool issub) { - MenuController controller = MenuController(); - - // IconButton - return MenuAnchor( - alignmentOffset: const Offset(0, -1), - controller: controller, - menuChildren: items - .map((item) => MenuItemButton( - child: Row( - children: [ - if (item.icon != null) Icon(item.icon!), - Text(item.text.toUpperCase()), - ], - ), - onPressed: () => callback(item.value), - )) - .toList(), - child: issub - ? MenuItemButton( - closeOnActivate: false, - style: ButtonStyle( - textStyle: MaterialStateTextStyle.resolveWith( - (states) => Theme.of(context).textTheme.labelLarge!)), - onPressed: controller.open, - leadingIcon: const Icon(Icons.sort), - child: Text('Sort'.toUpperCase()), - ) - : IconButton( - onPressed: controller.open, icon: const Icon(Icons.sort)), - ); - } - - @override - Widget build(BuildContext context) => asIcon(context); - - @override - Widget asIcon(BuildContext context) => _build(context, false); - - @override - Widget asMenuItem(BuildContext context, _) => _build(context, true); -} diff --git a/lib/utilities/cache_manager.dart b/lib/utilities/cache_manager.dart deleted file mode 100644 index 27055da91..000000000 --- a/lib/utilities/cache_manager.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:flutter_cache_manager/flutter_cache_manager.dart' as cache; -import 'package:reaxit/config.dart'; - -/// A [BaseCacheManager] with customized configurations. -class ThaliaCacheManager extends cache.CacheManager - with cache.ImageCacheManager { - static const key = 'thaliaCachedData'; - - static final ThaliaCacheManager _instance = ThaliaCacheManager._(); - factory ThaliaCacheManager() => _instance; - - ThaliaCacheManager._() - : super(cache.Config( - key, - stalePeriod: Config.cacheStalePeriod, - maxNrOfCacheObjects: Config.cacheMaxObjects, - )); -} diff --git a/test/mocks.dart b/test/mocks.dart index 2ebe5af37..b9c25834c 100644 --- a/test/mocks.dart +++ b/test/mocks.dart @@ -5,6 +5,5 @@ import 'package:reaxit/blocs.dart'; @GenerateMocks([ AuthCubit, ApiRepository, - WelcomeCubit, ]) void main() {} diff --git a/test/mocks.mocks.dart b/test/mocks.mocks.dart index 40e492e91..68e1a77b2 100644 --- a/test/mocks.mocks.dart +++ b/test/mocks.mocks.dart @@ -9,7 +9,6 @@ import 'package:flutter_bloc/flutter_bloc.dart' as _i9; import 'package:mockito/mockito.dart' as _i1; import 'package:reaxit/api/api_repository.dart' as _i5; import 'package:reaxit/blocs/auth_cubit.dart' as _i2; -import 'package:reaxit/blocs/welcome_cubit.dart' as _i7; import 'package:reaxit/config.dart' as _i3; import 'package:reaxit/models.dart' as _i4; @@ -207,16 +206,6 @@ class _FakeApiRepository_17 extends _i1.SmartFake implements _i5.ApiRepository { ); } -class _FakeWelcomeState_19 extends _i1.SmartFake implements _i7.WelcomeState { - _FakeWelcomeState_19( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - /// A class which mocks [AuthCubit]. /// /// See the documentation for Mockito's code generation for more information. @@ -1572,103 +1561,3 @@ class MockApiRepository extends _i1.Mock implements _i5.ApiRepository { )), ) as _i8.Future<_i4.ListResponse<_i4.Payment>>); } - -/// A class which mocks [WelcomeCubit]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockWelcomeCubit extends _i1.Mock implements _i7.WelcomeCubit { - MockWelcomeCubit() { - _i1.throwOnMissingStub(this); - } - - @override - _i5.ApiRepository get api => (super.noSuchMethod( - Invocation.getter(#api), - returnValue: _FakeApiRepository_17( - this, - Invocation.getter(#api), - ), - ) as _i5.ApiRepository); - @override - _i7.WelcomeState get state => (super.noSuchMethod( - Invocation.getter(#state), - returnValue: _FakeWelcomeState_19( - this, - Invocation.getter(#state), - ), - ) as _i7.WelcomeState); - @override - _i8.Stream<_i7.WelcomeState> get stream => (super.noSuchMethod( - Invocation.getter(#stream), - returnValue: _i8.Stream<_i7.WelcomeState>.empty(), - ) as _i8.Stream<_i7.WelcomeState>); - @override - bool get isClosed => (super.noSuchMethod( - Invocation.getter(#isClosed), - returnValue: false, - ) as bool); - @override - _i8.Future load() => (super.noSuchMethod( - Invocation.method( - #load, - [], - ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); - @override - void emit(_i7.WelcomeState? state) => super.noSuchMethod( - Invocation.method( - #emit, - [state], - ), - returnValueForMissingStub: null, - ); - @override - void onChange(_i9.Change<_i7.WelcomeState>? change) => super.noSuchMethod( - Invocation.method( - #onChange, - [change], - ), - returnValueForMissingStub: null, - ); - @override - void addError( - Object? error, [ - StackTrace? stackTrace, - ]) => - super.noSuchMethod( - Invocation.method( - #addError, - [ - error, - stackTrace, - ], - ), - returnValueForMissingStub: null, - ); - @override - void onError( - Object? error, - StackTrace? stackTrace, - ) => - super.noSuchMethod( - Invocation.method( - #onError, - [ - error, - stackTrace, - ], - ), - returnValueForMissingStub: null, - ); - @override - _i8.Future close() => (super.noSuchMethod( - Invocation.method( - #close, - [], - ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); -} diff --git a/test/unit/routes_test.dart b/test/unit/routes_test.dart deleted file mode 100644 index 90ef2a250..000000000 --- a/test/unit/routes_test.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:reaxit/routes.dart'; - -void main() { - group('isDeepLink', () { - test('returns true if uri is a deep link', () { - const apiHost = 'thalia.nu'; - final validUris = [ - 'https://$apiHost/events/1/', - 'https://$apiHost/events/1', - 'http://$apiHost/events/1/', - 'https://$apiHost/', - 'https://$apiHost/pizzas/', - 'https://$apiHost/members/photos/', - 'https://$apiHost/members/photos/some-album-1/', - 'https://$apiHost/sales/order/11111111-aaaa-bbbb-cccc-222222222222/pay/', - 'https://$apiHost/events/1/mark-present/11111111-aaaa-bbbb-cccc-222222222222/', - ]; - - for (final uri in validUris) { - expect( - isDeepLink(Uri.parse(uri)), - true, - reason: '$uri is a valid deep link', - ); - } - }); - - test('returns false if uri is not a deep link', () { - const apiHost = 'thalia.nu'; - final invalidUris = [ - 'https://$apiHost/contact', - 'https://example.org/events/1/', - 'https://subdomain.$apiHost/events/1/', - 'http://$apiHost/events/xxx/', - 'https://$apiHost/sales/order/11111111-bbbb-cccc-222222222222/pay/', - 'https://$apiHost/events/1/mark-present/11111111-bbbb-cccc-222222222222/', - 'https://$apiHost/events/1/mark_present/11111111-aaaa-bbbb-cccc-222222222222/', - ]; - - for (final uri in invalidUris) { - expect( - isDeepLink(Uri.parse(uri)), - false, - reason: '$uri is not a valid deep link', - ); - } - }); - }); -} diff --git a/test/widget/welcome_test.dart b/test/widget/welcome_test.dart deleted file mode 100644 index fc71df9e1..000000000 --- a/test/widget/welcome_test.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/mockito.dart'; -import 'package:reaxit/blocs.dart'; -import 'package:reaxit/config.dart'; -import 'package:reaxit/models.dart'; -import 'package:reaxit/ui/screens.dart'; - -import '../mocks.mocks.dart'; - -void main() { - group('WelcomeScreen', () { - testWidgets('Displays normal and partner events', - (WidgetTester tester) async { - final normalEvent = Event( - 1, - 'Lorem 1', - 'Ipsum 1', - '', - '', - DateTime.parse('2022-03-04 13:37'), - DateTime.parse('2022-03-04 14:37'), - EventCategory.other, - null, - null, - null, - '', - '', - '', - 0, - null, - null, - false, - null, - '', - const EventPermissions(false, false, false, false), - null, - [], - '', - false, - [], - ); - - final partnerEvent = PartnerEvent( - 1, - 'Lorem 2', - 'Ipsum 1', - DateTime.parse('2022-03-04 13:37'), - DateTime.parse('2022-03-04 14:37'), - 'Dolor 1', - Uri(), - ); - - final state = WelcomeState.result( - slides: const [], - articles: const [], - upcomingEvents: [normalEvent, partnerEvent]); - - final cubit = MockWelcomeCubit(); - final streamController = StreamController.broadcast() - ..stream.listen((state) { - when(cubit.state).thenReturn(state); - }) - ..add(const WelcomeState.loading()) - ..add(state); - - when(cubit.load()).thenAnswer((_) => Future.value(null)); - when(cubit.stream).thenAnswer((_) => streamController.stream); - - await tester.pumpWidget( - MaterialApp( - home: InheritedConfig( - config: Config.defaultConfig, - child: Scaffold( - body: BlocProvider.value( - value: cubit, - child: WelcomeScreen(), - )), - ), - ), - ); - - await tester.pumpAndSettle(); - await tester.pumpAndSettle(); - await tester.pumpAndSettle(); - await tester.pumpAndSettle(); - - expect(find.text('LOREM 1'), findsOneWidget); - expect(find.text('LOREM 2'), findsOneWidget); - }); - }); -}