diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 72bdc3cf0..292d4e3ba 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -202,4 +202,5 @@ jobs: TOSTI_API_HOST: ${{ vars.TOSTI_API_HOST }} TOSTI_OAUTH_APP_ID: ${{ secrets.TOSTI_OAUTH_APP_ID }} TOSTI_OAUTH_APP_SECRET: ${{ secrets.TOSTI_OAUTH_APP_SECRET }} + SUPPLY_UPLOAD_MAX_RETRIES: ${{ vars.SUPPLY_UPLOAD_MAX_RETRIES }} run: bundle exec fastlane android deploy_internal \ No newline at end of file diff --git a/Gemfile b/Gemfile index 221dd7126..c2b96b0c1 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source "https://rubygems.org" -gem "fastlane", "~> 2.205" +gem "fastlane", "~> 2.215.1" gem "dotenv" gem "cocoapods", "~> 1.11" diff --git a/Gemfile.lock b/Gemfile.lock index 87e4b2960..66034adca 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -17,20 +17,20 @@ GEM artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) - aws-partitions (1.651.0) - aws-sdk-core (3.166.0) + aws-partitions (1.825.0) + aws-sdk-core (3.182.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.5) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.59.0) - aws-sdk-core (~> 3, >= 3.165.0) + aws-sdk-kms (1.71.0) + aws-sdk-core (~> 3, >= 3.177.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.117.1) - aws-sdk-core (~> 3, >= 3.165.0) + aws-sdk-s3 (1.134.0) + aws-sdk-core (~> 3, >= 3.181.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.4) - aws-sigv4 (1.5.2) + aws-sigv4 (~> 1.6) + aws-sigv4 (1.6.0) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) claide (1.1.0) @@ -77,7 +77,7 @@ GEM highline (~> 2.0.0) concurrent-ruby (1.2.2) declarative (0.0.20) - digest-crc (0.6.4) + digest-crc (0.6.5) rake (>= 12.0.0, < 14.0.0) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) @@ -86,8 +86,8 @@ GEM escape (0.0.4) ethon (0.15.0) ffi (>= 1.15.0) - excon (0.93.1) - faraday (1.10.2) + excon (0.103.0) + faraday (1.10.3) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) @@ -115,8 +115,8 @@ GEM faraday-retry (1.0.3) faraday_middleware (1.2.0) faraday (~> 1.0) - fastimage (2.2.6) - fastlane (2.210.1) + fastimage (2.2.7) + fastlane (2.215.1) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -137,10 +137,11 @@ GEM google-apis-playcustomapp_v1 (~> 0.1) google-cloud-storage (~> 1.31) highline (~> 2.0) + http-cookie (~> 1.0.5) json (< 3.0.0) jwt (>= 2.1.0, < 3) mini_magick (>= 4.9.4, < 5.0.0) - multipart-post (~> 2.0.0) + multipart-post (>= 2.0.0, < 3.0.0) naturally (~> 2.2) optparse (~> 0.1.1) plist (>= 3.1.0, < 4.0.0) @@ -148,7 +149,7 @@ GEM security (= 0.1.3) simctl (~> 1.6.3) terminal-notifier (>= 2.0.0, < 3.0.0) - terminal-table (>= 1.4.5, < 2.0.0) + terminal-table (~> 3) tty-screen (>= 0.6.3, < 1.0.0) tty-spinner (>= 0.8.0, < 1.0.0) word_wrap (~> 1.0.0) @@ -159,9 +160,9 @@ GEM fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.29.0) - google-apis-core (>= 0.9.0, < 2.a) - google-apis-core (0.9.1) + google-apis-androidpublisher_v3 (0.49.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-core (0.11.1) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) httpclient (>= 2.8.1, < 3.a) @@ -170,10 +171,10 @@ GEM retriable (>= 2.0, < 4.a) rexml webrick - google-apis-iamcredentials_v1 (0.15.0) - google-apis-core (>= 0.9.0, < 2.a) - google-apis-playcustomapp_v1 (0.12.0) - google-apis-core (>= 0.9.1, < 2.a) + google-apis-iamcredentials_v1 (0.17.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-playcustomapp_v1 (0.13.0) + google-apis-core (>= 0.11.0, < 2.a) google-apis-storage_v1 (0.19.0) google-apis-core (>= 0.9.0, < 2.a) google-cloud-core (1.6.0) @@ -181,8 +182,8 @@ GEM google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) faraday (>= 0.17.3, < 3.0) - google-cloud-errors (1.3.0) - google-cloud-storage (1.43.0) + google-cloud-errors (1.3.1) + google-cloud-storage (1.44.0) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) @@ -190,10 +191,9 @@ GEM google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - googleauth (1.3.0) + googleauth (1.8.1) faraday (>= 0.17.3, < 3.a) jwt (>= 1.4, < 3.0) - memoist (~> 0.16) multi_json (~> 1.11) os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) @@ -203,23 +203,22 @@ GEM httpclient (2.8.3) i18n (1.14.1) concurrent-ruby (~> 1.0) - jmespath (1.6.1) + jmespath (1.6.2) json (2.6.2) - jwt (2.5.0) - memoist (0.16.2) - mini_magick (4.11.0) - mini_mime (1.1.2) + jwt (2.7.1) + mini_magick (4.12.0) + mini_mime (1.1.5) minitest (5.19.0) molinillo (0.8.0) multi_json (1.15.0) - multipart-post (2.0.0) + multipart-post (2.3.0) nanaimo (0.3.0) nap (1.1.0) naturally (2.2.1) netrc (0.11.0) optparse (0.1.1) os (1.1.4) - plist (3.6.0) + plist (3.7.0) public_suffix (4.0.7) rake (13.0.6) representable (3.2.0) @@ -233,17 +232,17 @@ GEM ruby2_keywords (0.0.5) rubyzip (2.3.2) security (0.1.3) - signet (0.17.0) + signet (0.18.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 3.0) multi_json (~> 1.10) - simctl (1.6.8) + simctl (1.6.10) CFPropertyList naturally terminal-notifier (2.0.0) - terminal-table (1.8.0) - unicode-display_width (~> 1.1, >= 1.1.1) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) trailblazer-option (0.1.2) tty-cursor (0.7.1) tty-screen (0.8.1) @@ -257,8 +256,8 @@ GEM unf (0.1.4) unf_ext unf_ext (0.0.8.2) - unicode-display_width (1.8.0) - webrick (1.7.0) + unicode-display_width (2.4.2) + webrick (1.8.1) word_wrap (1.0.0) xcodeproj (1.22.0) CFPropertyList (>= 2.3.3, < 4.0) @@ -279,7 +278,7 @@ PLATFORMS DEPENDENCIES cocoapods (~> 1.11) dotenv - fastlane (~> 2.205) + fastlane (~> 2.215.1) BUNDLED WITH 2.3.21 diff --git a/fastlane/Fastfile-Android b/fastlane/Fastfile-Android index fe3301bc2..97f00fb51 100644 --- a/fastlane/Fastfile-Android +++ b/fastlane/Fastfile-Android @@ -47,6 +47,7 @@ platform :android do build changelog_from_git_commits upload_to_play_store( + root_url: "https://androidpublisher.googleapis.com/", track: options[:track], skip_upload_metadata: true, skip_upload_images: true, diff --git a/lib/tosti/tosti_screen.dart b/lib/tosti/tosti_screen.dart index 53c582f99..df66cdbde 100644 --- a/lib/tosti/tosti_screen.dart +++ b/lib/tosti/tosti_screen.dart @@ -14,11 +14,12 @@ class TostiScreen extends StatelessWidget { return Scaffold( appBar: ThaliaAppBar( title: const Text('T.O.S.T.I.'), - actions: [ - IconButton( - icon: const Icon(Icons.logout), - onPressed: () => BlocProvider.of(context).logOut(), - ), + collapsingActions: [ + IconAppbarAction( + 'logout', + Icons.logout, + () => BlocProvider.of(context).logOut(), + ) ], ), drawer: MenuDrawer(), diff --git a/lib/ui/screens/album_screen.dart b/lib/ui/screens/album_screen.dart index 7982b7149..4c936ab9e 100644 --- a/lib/ui/screens/album_screen.dart +++ b/lib/ui/screens/album_screen.dart @@ -49,13 +49,6 @@ class _AlbumScreenState extends State { } } - Widget _shareAlbumButton(BuildContext context) => IconButton( - padding: const EdgeInsets.all(16), - color: Theme.of(context).primaryIconTheme.color, - icon: Icon(Icons.adaptive.share), - onPressed: () => _shareAlbum(context), - ); - @override Widget build(BuildContext context) { return BlocProvider.value( @@ -74,7 +67,14 @@ class _AlbumScreenState extends State { return Scaffold( appBar: ThaliaAppBar( title: Text(state.result?.title.toUpperCase() ?? title), - actions: [_shareAlbumButton(context)], + collapsingActions: [ + IconAppbarAction( + 'SHARE', + Icons.adaptive.share, + () => _shareAlbum(context), + tooltip: 'share album', + ) + ], ), body: body, ); diff --git a/lib/ui/screens/albums_screen.dart b/lib/ui/screens/albums_screen.dart index 4af3dd698..8b5a76c51 100644 --- a/lib/ui/screens/albums_screen.dart +++ b/lib/ui/screens/albums_screen.dart @@ -43,16 +43,16 @@ class _AlbumsScreenState extends State { return Scaffold( appBar: ThaliaAppBar( title: const Text('ALBUMS'), - actions: [ - IconButton( - onPressed: () => context.pushNamed( - 'liked-photos', - ), - icon: const Icon(Icons.favorite_border)), - IconButton( - padding: const EdgeInsets.all(16), - icon: const Icon(Icons.search), - onPressed: () async { + collapsingActions: [ + IconAppbarAction( + 'LIKED PHOTOS', + Icons.favorite_border, + () => context.pushNamed('liked-photos'), + ), + IconAppbarAction( + 'SEARCH', + Icons.search, + () async { final searchCubit = AlbumListCubit( RepositoryProvider.of(context), ); diff --git a/lib/ui/screens/calendar_screen.dart b/lib/ui/screens/calendar_screen.dart index e81bdc492..a894f9fcf 100644 --- a/lib/ui/screens/calendar_screen.dart +++ b/lib/ui/screens/calendar_screen.dart @@ -88,12 +88,12 @@ class _CalendarScreenState extends State { return Scaffold( appBar: ThaliaAppBar( title: const Text('CALENDAR'), - actions: [ - IconButton( - padding: const EdgeInsets.all(16), - icon: const Icon(Icons.search), - onPressed: openSearch, - ), + collapsingActions: [ + IconAppbarAction( + 'SEARCH', + Icons.search, + openSearch, + ) ], ), drawer: MenuDrawer(), diff --git a/lib/ui/screens/event_admin_screen.dart b/lib/ui/screens/event_admin_screen.dart index b42cde528..d6b107e9c 100644 --- a/lib/ui/screens/event_admin_screen.dart +++ b/lib/ui/screens/event_admin_screen.dart @@ -19,6 +19,66 @@ class EventAdminScreen extends StatefulWidget { } class _EventAdminScreenState extends State { + void _showQRCode(EventAdminCubit cubit) async { + showModalBottomSheet( + isScrollControlled: true, + context: context, + builder: (context) { + return SafeArea( + child: BlocBuilder( + bloc: cubit, + builder: (context, state) { + final theme = Theme.of(context); + if (state.event != null) { + final host = Config.of(context).host; + final pk = state.event!.pk; + final token = state.event!.markPresentUrlToken; + final url = 'https://$host/events/$pk/mark-present/$token'; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Text( + 'Scan to mark yourself present.', + style: theme.textTheme.titleSmall, + ), + ), + QrImage( + data: url, + padding: const EdgeInsets.all(24), + backgroundColor: Colors.grey[50]!, + ), + Padding( + padding: const EdgeInsets.all(16), + child: RotatedBox( + quarterTurns: 2, + child: Text( + 'Scan to mark yourself present.', + style: theme.textTheme.titleSmall, + ), + ), + ) + ], + ); + } else if (state.isLoading) { + return const AspectRatio( + aspectRatio: 1, + child: Center(child: CircularProgressIndicator()), + ); + } else { + return AspectRatio( + aspectRatio: 1, + child: Center(child: Text(state.message!)), + ); + } + }, + ), + ); + }, + ); + } + @override Widget build(BuildContext context) { return BlocProvider( @@ -31,9 +91,38 @@ class _EventAdminScreenState extends State { return Scaffold( appBar: ThaliaAppBar( title: const Text('REGISTRATIONS'), - actions: [ - const _MarkPresentQrButton(), - _SearchButton(eventPk: widget.pk), + collapsingActions: [ + IconAppbarAction( + 'SEARCH', + Icons.search, + () async { + final adminCubit = + BlocProvider.of(context); + final searchCubit = EventAdminCubit( + RepositoryProvider.of(context), + eventPk: widget.pk, + ); + + await showSearch( + context: context, + delegate: EventAdminSearchDelegate(searchCubit), + ); + + searchCubit.close(); + + // After the search dialog closes, refresh the results, + // since the search screen may have changed stuff through + // its own EventAdminCubit, that do not show up in the cubit + // for the EventAdminScreen until a refresh. + adminCubit.loadRegistrations(); + }, + ), + IconAppbarAction( + 'QR Code', + Icons.qr_code, + () => _showQRCode(BlocProvider.of(context)), + tooltip: 'Show presence QR code', + ), ], ), body: RefreshIndicator( @@ -71,117 +160,6 @@ class _EventAdminScreenState extends State { } } -class _SearchButton extends StatelessWidget { - const _SearchButton({ - Key? key, - required this.eventPk, - }) : super(key: key); - - final int eventPk; - - @override - Widget build(BuildContext context) { - return IconButton( - padding: const EdgeInsets.all(16), - icon: const Icon(Icons.search), - onPressed: () async { - final adminCubit = BlocProvider.of(context); - final searchCubit = EventAdminCubit( - RepositoryProvider.of(context), - eventPk: eventPk, - ); - - await showSearch( - context: context, - delegate: EventAdminSearchDelegate(searchCubit), - ); - - searchCubit.close(); - - // After the search dialog closes, refresh the results, - // since the search screen may have changed stuff through - // its own EventAdminCubit, that do not show up in the cubit - // for the EventAdminScreen until a refresh. - adminCubit.loadRegistrations(); - }, - ); - } -} - -class _MarkPresentQrButton extends StatelessWidget { - const _MarkPresentQrButton({ - Key? key, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - final cubit = BlocProvider.of(context); - return IconButton( - icon: const Icon(Icons.qr_code), - tooltip: 'Show presence QR code', - onPressed: () async { - showModalBottomSheet( - isScrollControlled: true, - context: context, - builder: (context) { - return SafeArea( - child: BlocBuilder( - bloc: cubit, - builder: (context, state) { - final theme = Theme.of(context); - if (state.event != null) { - final host = Config.of(context).host; - final pk = state.event!.pk; - final token = state.event!.markPresentUrlToken; - final url = 'https://$host/events/$pk/mark-present/$token'; - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.all(16), - child: Text( - 'Scan to mark yourself present.', - style: theme.textTheme.titleSmall, - ), - ), - QrImage( - data: url, - padding: const EdgeInsets.all(24), - backgroundColor: Colors.grey[50]!, - ), - Padding( - padding: const EdgeInsets.all(16), - child: RotatedBox( - quarterTurns: 2, - child: Text( - 'Scan to mark yourself present.', - style: theme.textTheme.titleSmall, - ), - ), - ) - ], - ); - } else if (state.isLoading) { - return const AspectRatio( - aspectRatio: 1, - child: Center(child: CircularProgressIndicator()), - ); - } else { - return AspectRatio( - aspectRatio: 1, - child: Center(child: Text(state.message!)), - ); - } - }, - ), - ); - }, - ); - }, - ); - } -} - class _RegistrationTile extends StatefulWidget { final AdminEventRegistration registration; final bool requiresPayment; diff --git a/lib/ui/screens/event_screen.dart b/lib/ui/screens/event_screen.dart index 6255482c0..564965d6b 100644 --- a/lib/ui/screens/event_screen.dart +++ b/lib/ui/screens/event_screen.dart @@ -864,46 +864,6 @@ class _EventScreenState extends State { ); } - Widget _makeShareEventButton(Event event) { - return IconButton( - padding: const EdgeInsets.all(16), - color: Theme.of(context).primaryIconTheme.color, - icon: Icon( - Theme.of(context).platform == TargetPlatform.iOS - ? Icons.ios_share - : Icons.share, - ), - onPressed: () async { - final messenger = ScaffoldMessenger.of(context); - try { - await Share.share(event.url); - } catch (_) { - messenger.showSnackBar(const SnackBar( - behavior: SnackBarBehavior.floating, - content: Text('Could not share the event.'), - )); - } - }, - ); - } - - Widget _makeCalendarExportButton(Event event) { - return IconButton( - padding: const EdgeInsets.all(16), - color: Theme.of(context).primaryIconTheme.color, - icon: const Icon(Icons.edit_calendar_outlined), - onPressed: () async { - final exportableEvent = add2calendar.Event( - title: event.title, - location: event.location, - startDate: event.start, - endDate: event.end, - ); - await add2calendar.Add2Calendar.addEvent2Cal(exportableEvent); - }, - ); - } - @override Widget build(BuildContext context) { return BlocBuilder( @@ -933,14 +893,44 @@ class _EventScreenState extends State { return Scaffold( appBar: ThaliaAppBar( title: Text(event.title.toUpperCase()), - actions: [ - _makeCalendarExportButton(event), - _makeShareEventButton(event), + collapsingActions: [ + IconAppbarAction( + 'EXPORT', + Icons.edit_calendar_outlined, + () async { + final exportableEvent = add2calendar.Event( + title: event.title, + location: event.location, + startDate: event.start, + endDate: event.end, + ); + await add2calendar.Add2Calendar.addEvent2Cal( + exportableEvent); + }, + tooltip: 'add event to calendar', + ), + IconAppbarAction( + 'SHARE', + Theme.of(context).platform == TargetPlatform.iOS + ? Icons.ios_share + : Icons.share, + () async { + final messenger = ScaffoldMessenger.of(context); + try { + await Share.share(event.url); + } catch (_) { + messenger.showSnackBar(const SnackBar( + behavior: SnackBarBehavior.floating, + content: Text('Could not share the event.'), + )); + } + }, + ), if (event.userPermissions.manageEvent) - IconButton( - padding: const EdgeInsets.all(16), - icon: const Icon(Icons.settings), - onPressed: () => context.pushNamed( + IconAppbarAction( + 'EDIT', + Icons.settings, + () => context.pushNamed( 'event-admin', pathParameters: {'eventPk': event.pk.toString()}, ), diff --git a/lib/ui/screens/food_admin_screen.dart b/lib/ui/screens/food_admin_screen.dart index 47fa49693..2cbe7b653 100644 --- a/lib/ui/screens/food_admin_screen.dart +++ b/lib/ui/screens/food_admin_screen.dart @@ -29,11 +29,11 @@ class _FoodAdminScreenState extends State { return Scaffold( appBar: ThaliaAppBar( title: const Text('ORDERS'), - actions: [ - IconButton( - padding: const EdgeInsets.all(16), - icon: const Icon(Icons.search), - onPressed: () async { + collapsingActions: [ + IconAppbarAction( + 'SEACH', + Icons.search, + () async { final adminCubit = BlocProvider.of(context); final searchCubit = FoodAdminCubit( RepositoryProvider.of(context), @@ -53,7 +53,7 @@ class _FoodAdminScreenState extends State { // for the FoodAdminScreen until a refresh. adminCubit.load(); }, - ), + ) ], ), body: RefreshIndicator( diff --git a/lib/ui/screens/food_screen.dart b/lib/ui/screens/food_screen.dart index 1b53ca334..fe90d9196 100644 --- a/lib/ui/screens/food_screen.dart +++ b/lib/ui/screens/food_screen.dart @@ -339,16 +339,16 @@ class _FoodScreenState extends State { return Scaffold( appBar: ThaliaAppBar( title: const Text('ORDER FOOD'), - actions: [ - if (foodEvent.canManage) - IconButton( - padding: const EdgeInsets.all(16), - icon: const Icon(Icons.settings), - onPressed: () => context.pushNamed( - 'food-admin', - extra: foodEvent.pk, - ), + collapsingActions: [ + IconAppbarAction( + 'ADMIN', + Icons.settings, + () => context.pushNamed( + 'food-admin', + extra: foodEvent.pk, ), + tooltip: 'food admin', + ) ], ), body: RefreshIndicator( diff --git a/lib/ui/screens/groups_screen.dart b/lib/ui/screens/groups_screen.dart index 644267838..061dca413 100644 --- a/lib/ui/screens/groups_screen.dart +++ b/lib/ui/screens/groups_screen.dart @@ -70,11 +70,11 @@ class _GroupsScreenState extends State ], indicatorColor: Theme.of(context).colorScheme.primary, ), - actions: [ - IconButton( - padding: const EdgeInsets.all(16), - icon: const Icon(Icons.search), - onPressed: () async { + collapsingActions: [ + IconAppbarAction( + 'SEARCH', + Icons.search, + () async { final searchCubit = AllGroupsCubit(RepositoryProvider.of(context)); @@ -82,7 +82,6 @@ class _GroupsScreenState extends State context: context, delegate: GroupSearchDelegate(searchCubit), ); - searchCubit.close(); }, ) diff --git a/lib/ui/screens/members_screen.dart b/lib/ui/screens/members_screen.dart index f9ce22f16..5c4612d79 100644 --- a/lib/ui/screens/members_screen.dart +++ b/lib/ui/screens/members_screen.dart @@ -42,11 +42,11 @@ class _MembersScreenState extends State { return Scaffold( appBar: ThaliaAppBar( title: const Text('MEMBERS'), - actions: [ - IconButton( - padding: const EdgeInsets.all(16), - icon: const Icon(Icons.search), - onPressed: () async { + collapsingActions: [ + IconAppbarAction( + 'SEARCH', + Icons.search, + () async { final searchCubit = MemberListCubit( RepositoryProvider.of(context), ); @@ -58,7 +58,7 @@ class _MembersScreenState extends State { searchCubit.close(); }, - ), + ) ], ), drawer: MenuDrawer(), diff --git a/lib/ui/widgets/app_bar.dart b/lib/ui/widgets/app_bar.dart index 12800660a..df261b4c5 100644 --- a/lib/ui/widgets/app_bar.dart +++ b/lib/ui/widgets/app_bar.dart @@ -2,15 +2,109 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:reaxit/ui/theme.dart'; +abstract class AppbarAction { + Widget asIcon(BuildContext _); + Widget asMenuItem(BuildContext context, Function() callback); + void ontap(); + + const AppbarAction(); +} + +class IconAppbarAction extends AppbarAction { + final String text; + final String tooltip; + final IconData icon; + final void Function() onpressed; + + const IconAppbarAction(this.text, this.icon, this.onpressed, + {String? tooltip}) + : tooltip = tooltip ?? text; + + @override + Widget asIcon(BuildContext _) { + return IconButton( + padding: const EdgeInsets.all(16), + onPressed: onpressed, + icon: Icon(icon), + tooltip: tooltip, + ); + } + + @override + Widget asMenuItem(BuildContext context, Function() callback) { + return MenuItemButton( + style: ButtonStyle( + textStyle: MaterialStateTextStyle.resolveWith( + (states) => Theme.of(context).textTheme.labelLarge!), + foregroundColor: + MaterialStateColor.resolveWith((states) => Colors.white)), + onPressed: () { + onpressed(); + callback(); + }, + leadingIcon: Icon(icon), + child: Text(text), + ); + } + + @override + void ontap() => onpressed(); +} + +class _IconAction extends StatelessWidget { + final AppbarAction action; + + const _IconAction(this.action); + + @override + Widget build(BuildContext context) => action.asIcon(context); +} + +class _MenuAction extends StatelessWidget { + final AppbarAction action; + final Function() callback; + const _MenuAction(this.action, this.callback); + + @override + Widget build(BuildContext context) => action.asMenuItem(context, callback); +} + class ThaliaAppBar extends AppBar { + static const defaultIcons = 3; + + static List collapse(List widgets) { + if (widgets.length <= defaultIcons) { + return widgets.map((e) => _IconAction(e)).toList(); + } + + MenuController controller = MenuController(); + return [ + ...widgets.take(defaultIcons - 1).map((item) => _IconAction(item)), + MenuAnchor( + alignmentOffset: const Offset(0, -1), + controller: controller, + menuChildren: widgets + .skip(defaultIcons - 1) + .map( + (item) => _MenuAction(item, controller.close), + ) + .toList(), + child: IconButton( + onPressed: controller.open, + icon: const Icon(Icons.more_vert, color: Colors.white), + ), + ) + ]; + } + ThaliaAppBar({ Widget? title, - List? actions, + List collapsingActions = const [], Widget? leading, PreferredSizeWidget? bottom, }) : super( title: title, - actions: actions, + actions: collapse(collapsingActions), leading: leading, systemOverlayStyle: SystemUiOverlayStyle.light, // The bottom decoration only needs to be shown