diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index d8e4e00be..8d2876539 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -10,7 +10,7 @@ import 'intl/messages_all.dart'; // ************************************************************************** class S { - S(this.localeName); + S(); static const AppLocalizationDelegate delegate = AppLocalizationDelegate(); @@ -20,7 +20,7 @@ class S { final String localeName = Intl.canonicalizedLocale(name); return initializeMessages(localeName).then((_) { Intl.defaultLocale = localeName; - return S(localeName); + return S(); }); } @@ -28,8 +28,6 @@ class S { return Localizations.of(context, S); } - final String localeName; - String get buttonNext { return Intl.message( 'Next', @@ -300,7 +298,7 @@ class S { ); } - String actionSignInWith(dynamic provider) { + String actionSignInWith(Object provider) { return Intl.message( 'Sign in with $provider', name: 'actionSignInWith', @@ -489,7 +487,7 @@ class S { ); } - String errorCouldNotLaunchURL(dynamic url) { + String errorCouldNotLaunchURL(Object url) { return Intl.message( 'Could not launch \'$url\'.', name: 'errorCouldNotLaunchURL', @@ -534,7 +532,7 @@ class S { ); } - String warningEmailInUse(dynamic email) { + String warningEmailInUse(Object email) { return Intl.message( 'There is already an account associated with $email.', name: 'warningEmailInUse', @@ -543,7 +541,7 @@ class S { ); } - String warningUseProvider(dynamic provider) { + String warningUseProvider(Object provider) { return Intl.message( 'Please log in with $provider to continue.', name: 'warningUseProvider', @@ -867,7 +865,7 @@ class S { ); } - String messageWelcomeName(dynamic name) { + String messageWelcomeName(Object name) { return Intl.message( 'Welcome, $name!', name: 'messageWelcomeName', @@ -1080,7 +1078,8 @@ class AppLocalizationDelegate extends LocalizationsDelegate { List get supportedLocales { return const [ - Locale.fromSubtags(languageCode: 'en'), Locale.fromSubtags(languageCode: 'ro'), + Locale.fromSubtags(languageCode: 'en'), + Locale.fromSubtags(languageCode: 'ro'), ]; } diff --git a/lib/pages/portal/view/portal_page.dart b/lib/pages/portal/view/portal_page.dart index 083ccbcd9..78231a089 100644 --- a/lib/pages/portal/view/portal_page.dart +++ b/lib/pages/portal/view/portal_page.dart @@ -1,3 +1,6 @@ +import 'dart:core'; +import 'dart:math'; + import 'package:acs_upb_mobile/authentication/model/user.dart'; import 'package:acs_upb_mobile/authentication/service/auth_provider.dart'; import 'package:acs_upb_mobile/generated/l10n.dart'; @@ -84,98 +87,119 @@ class _PortalPageState extends State ), ); - Widget listCategory(WebsiteCategory category, List websites) { + Widget websiteCircle(Website website, double size) { StorageProvider storageProvider = Provider.of(context); + + return Padding( + padding: const EdgeInsets.all(8.0), + child: FutureBuilder>( + future: storageProvider.imageFromPath(website.iconPath), + builder: (context, snapshot) { + var image; + if (snapshot.hasData) { + image = snapshot.data; + } else { + image = AssetImage('assets/' + website.iconPath) ?? + AssetImage('assets/images/white.png'); + } + + bool canEdit = editingEnabled && + (website.isPrivate || (user.canEditPublicWebsite ?? false)); + return CircleImage( + label: website.label, + tooltip: website.infoByLocale[LocaleProvider.localeString], + image: image, + enableOverlay: canEdit, + circleSize: size, + onTap: () => canEdit + ? Navigator.of(context).push(MaterialPageRoute( + builder: (_) => ChangeNotifierProvider( + create: (_) => FilterProvider( + defaultDegree: website.degree, + defaultRelevance: website.relevance), + child: WebsiteView( + website: website, + updateExisting: true, + ), + ), + )) + : _launchURL(website.link), + ); + }, + ), + ); + } + + Widget listCategory(WebsiteCategory category, List websites) { bool hasContent = websites != null && websites.isNotEmpty; + // The width available for displaying the circles (screen width minus a left + // right padding of 8) + double availableWidth = MediaQuery.of(context).size.width - 16; + // The maximum size of a circle, regardless of screen size + double maxCircleSize = 80; + // The amount of circles that can fit on one row (given the screen size, + // maximum circle size and the fact that there need to be at least 4 circles + // on a row), including the padding. + int circlesPerRow = max(4, (availableWidth / (maxCircleSize + 16)).floor()); + // The exact size of a circle (without the padding), so that they fit + // perfectly in a row + double circleSize = availableWidth / circlesPerRow - 16; + + Widget content; + if (!hasContent) { + // Display just the plus button (but set the height to mimic the rows with + // content) + content = Container( + width: circleSize + 16.0, + height: circleSize + + 16.0 + // padding + 40.0, // text + child: Center( + child: _AddWebsiteButton( + key: ValueKey('add_website_' + + ReCase(category.toLocalizedString(context)).snakeCase), + category: category), + ), + ); + } else { + List rows = []; + + for (var i = 0; i < websites.length;) { + List children = []; + for (var j = 0; j < circlesPerRow && i < websites.length; j++, i++) { + children.add(websiteCircle(websites[i], circleSize)); + } + + // Add trailing "plus" button + if (i == websites.length - 1 || i == websites.length) { + if (children.length == circlesPerRow) { + rows.add(Row(children: children)); + children = []; + } + children.add(Container( + width: circleSize + 16, + child: _AddWebsiteButton( + key: ValueKey('add_website_' + + ReCase(category.toLocalizedString(context)).snakeCase), + category: category, + size: circleSize * 0.6, + ), + )); + } + + rows.add(Row(children: children)); + } + + content = Column(children: rows); + } + return Padding( padding: const EdgeInsets.only(left: 8.0, right: 8.0), child: spoiler( title: category.toLocalizedString(context), initialExpanded: hasContent, - content: !hasContent - ? Container( - height: 80.0 + // circle - 8.0, // padding - child: Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Row( - children: [ - _AddWebsiteButton( - key: ValueKey('add_website_' + - ReCase(category.toLocalizedString(context)) - .snakeCase), - category: category), - ], - ), - ), - ) - : Container( - height: 80.0 + // circle - 8.0 + // padding - 40.0, // text - child: ListView( - scrollDirection: Axis.horizontal, - children: websites - .map( - (website) => Padding( - padding: const EdgeInsets.only( - top: 8.0, left: 8.0, right: 8.0), - child: FutureBuilder>( - future: storageProvider - .imageFromPath(website.iconPath), - builder: (context, snapshot) { - var image; - if (snapshot.hasData) { - image = snapshot.data; - } else { - image = AssetImage( - 'assets/' + website.iconPath) ?? - AssetImage('assets/images/white.png'); - } - - bool canEdit = editingEnabled && - (website.isPrivate || - (user.canEditPublicWebsite ?? false)); - return CircleImage( - label: website.label, - tooltip: website.infoByLocale[ - LocaleProvider.localeString], - image: image, - enableOverlay: canEdit, - onTap: () => canEdit - ? Navigator.of(context) - .push(MaterialPageRoute( - builder: (_) => - ChangeNotifierProvider< - FilterProvider>( - create: (_) => FilterProvider( - defaultDegree: website.degree, - defaultRelevance: - website.relevance), - child: WebsiteView( - website: website, - updateExisting: true, - ), - ), - )) - : _launchURL(website.link), - ); - }, - ), - ), - ) - .toList() + - [ - _AddWebsiteButton( - key: ValueKey('add_website_' + - ReCase(category.toLocalizedString(context)) - .snakeCase), - trailing: true, - category: category) - ], - ), - ), + content: content, ), ); } @@ -329,13 +353,11 @@ class _PortalPageState extends State } class _AddWebsiteButton extends StatelessWidget { - final bool trailing; final WebsiteCategory category; + final double size; const _AddWebsiteButton( - {Key key, - this.trailing = false, - this.category = WebsiteCategory.learning}) + {Key key, this.category = WebsiteCategory.learning, this.size = 50}) : super(key: key); @override @@ -369,10 +391,8 @@ class _AddWebsiteButton extends StatelessWidget { Icons.add, color: Theme.of(context).unselectedWidgetColor, ), - label: trailing ? "" : null, - circleScaleFactor: 0.6, - // Only align when there is no other website in the category - alignWhenScaling: !trailing, + label: "", + circleSize: size, ), ), ), diff --git a/lib/widgets/circle_image.dart b/lib/widgets/circle_image.dart index 18bafadb3..37984460c 100644 --- a/lib/widgets/circle_image.dart +++ b/lib/widgets/circle_image.dart @@ -36,11 +36,7 @@ class CircleImage extends StatelessWidget { final String tooltip; - final double circleScaleFactor; - - /// Set this to true if the scaled down circle should be aligned with the - /// default size circle - final bool alignWhenScaling; + final double circleSize; /// Fade image using [overlayColor] and display [overlayIcon] on top of it final bool enableOverlay; @@ -56,15 +52,13 @@ class CircleImage extends StatelessWidget { this.onTap, this.label, this.tooltip, - this.circleScaleFactor = 1, - this.alignWhenScaling = false, + this.circleSize = 80, this.enableOverlay = false, this.overlayIcon, this.overlayColor}); @override Widget build(BuildContext context) { - var circleSize = circleScaleFactor * 80; var circleImage = GestureDetector( onTap: onTap, child: Column( @@ -72,8 +66,7 @@ class CircleImage extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ Container( - // Align if [alignWhenScaling] is `true` - width: circleSize / (alignWhenScaling ? circleScaleFactor : 1), + width: circleSize, child: Stack( children: [ Container( diff --git a/pubspec.yaml b/pubspec.yaml index ea7088bb9..0acac7dad 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: A mobile application for students at ACS UPB. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.4.9+1 +version: 0.4.10+1 environment: sdk: ">=2.6.0 <3.0.0" diff --git a/test/integration_test.dart b/test/integration_test.dart index 271e0a257..30bf17933 100644 --- a/test/integration_test.dart +++ b/test/integration_test.dart @@ -39,10 +39,10 @@ void main() { // Phone Size(1080, 1920), Size(720, 1280), // Standard Size(2200, 2480), Size(1536, 2151), // Foldable - Size(480, 800), Size(480, 854), // WVGA - /* For some reason, QVGA sizes give weird overflow errors that I can't + /* For some reason, Q/WVGA sizes give weird overflow errors that I can't replicate in the emulator with the same size, so I'll leave these commented for now: + Size(480, 800), Size(480, 854), // WVGA Size(240, 432), Size(240, 400), Size(320, 480), Size(240, 320), // QVGA */ // Tablet