From ea2b6400116e1d1f9ef78cd57b219ad19ee47705 Mon Sep 17 00:00:00 2001 From: Efrain Date: Fri, 16 Jul 2021 20:26:53 -0500 Subject: [PATCH] [Presentation] Added a tabview to the characters page on landscape --- .../character/character_page.dart | 34 +- .../character/widgets/character_detail.dart | 7 + .../widgets/character_detail_bottom.dart | 304 +++++++++++++++--- .../character_detail_constellations_card.dart | 8 +- .../character_detail_general_card.dart | 14 +- .../character_detail_passive_card.dart | 14 +- .../widgets/character_detail_top.dart | 90 ++++-- 7 files changed, 386 insertions(+), 85 deletions(-) diff --git a/lib/presentation/character/character_page.dart b/lib/presentation/character/character_page.dart index ccac10218..b7005d52b 100644 --- a/lib/presentation/character/character_page.dart +++ b/lib/presentation/character/character_page.dart @@ -7,13 +7,45 @@ import 'widgets/character_detail_top.dart'; class CharacterPage extends StatelessWidget { const CharacterPage({Key? key}) : super(key: key); + @override + Widget build(BuildContext context) { + final isPortrait = MediaQuery.of(context).orientation == Orientation.portrait; + return isPortrait ? const _PortraitLayout() : const _LandscapeLayout(); + } +} + +class _PortraitLayout extends StatelessWidget { + const _PortraitLayout({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { return ScaffoldWithFab( child: Stack( fit: StackFit.passthrough, clipBehavior: Clip.none, - children: const [CharacterDetailTop(), CharacterDetailBottom()], + alignment: Alignment.topCenter, + children: const [ + CharacterDetailTop(), + CharacterDetailBottom(), + ], + ), + ); + } +} + +class _LandscapeLayout extends StatelessWidget { + const _LandscapeLayout({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: Row( + children: const [ + Expanded(child: CharacterDetailTop()), + Expanded(child: CharacterDetailBottom()), + ], + ), ), ); } diff --git a/lib/presentation/character/widgets/character_detail.dart b/lib/presentation/character/widgets/character_detail.dart index 01047c64a..4060eab05 100644 --- a/lib/presentation/character/widgets/character_detail.dart +++ b/lib/presentation/character/widgets/character_detail.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + export 'character_detail_ascension_materials_card.dart'; export 'character_detail_bottom.dart'; export 'character_detail_build_card.dart'; @@ -11,3 +13,8 @@ export 'character_detail_top.dart'; const double imgSize = 28; const double imgHeight = 550; +const double charDescriptionHeight = 240; + +double getTopHeightForPortrait(BuildContext context) { + return MediaQuery.of(context).size.height * 0.7; +} diff --git a/lib/presentation/character/widgets/character_detail_bottom.dart b/lib/presentation/character/widgets/character_detail_bottom.dart index 265b29562..f173fe663 100644 --- a/lib/presentation/character/widgets/character_detail_bottom.dart +++ b/lib/presentation/character/widgets/character_detail_bottom.dart @@ -1,83 +1,297 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:genshindb/application/bloc.dart'; +import 'package:genshindb/domain/enums/element_type.dart'; +import 'package:genshindb/domain/enums/enums.dart'; +import 'package:genshindb/domain/models/models.dart'; import 'package:genshindb/generated/l10n.dart'; +import 'package:genshindb/presentation/character/widgets/character_detail.dart'; +import 'package:genshindb/presentation/character/widgets/character_detail_skills_card.dart'; import 'package:genshindb/presentation/shared/extensions/element_type_extensions.dart'; import 'package:genshindb/presentation/shared/item_description_detail.dart'; import 'package:genshindb/presentation/shared/loading.dart'; import 'package:genshindb/presentation/shared/styles.dart'; +import 'package:responsive_builder/responsive_builder.dart'; import 'character_detail.dart'; +// final height = MediaQuery.of(context).size.height; +// +// // Height (without SafeArea) +// var padding = MediaQuery.of(context).padding; +// double height1 = height - padding.top - padding.bottom; +// // Height (without status bar) +// double height2 = height - padding.top; +// // Height (without status and toolbar) +// double height3 = height - padding.top - kToolbarHeight; + class CharacterDetailBottom extends StatelessWidget { const CharacterDetailBottom({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - final s = S.of(context); + final isPortrait = MediaQuery.of(context).orientation == Orientation.portrait; + + return isPortrait ? const _PortraitLayout() : const _LandscapeLayout(); + } +} +class _PortraitLayout extends StatelessWidget { + const _PortraitLayout({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final s = S.of(context); + final size = MediaQuery.of(context).size; + final maxTopHeight = (getTopHeightForPortrait(context) / 2) + (charDescriptionHeight / 1.8); + final device = getDeviceType(size); + final width = size.width * (device == DeviceScreenType.mobile ? 0.9 : 0.8); return BlocBuilder( builder: (ctx, state) => state.map( loading: (_) => const Loading(useScaffold: false), - loaded: (state) => Card( - margin: const EdgeInsets.only(top: 400, right: 10, left: 10), - shape: Styles.cardItemDetailShape, - child: Padding( - padding: Styles.edgeInsetAll10, + loaded: (state) => SizedBox( + width: width, + child: Card( + margin: EdgeInsets.only(top: maxTopHeight), + shape: Styles.cardItemDetailShape, + child: Padding( + padding: Styles.edgeInsetAll10, + child: Column( + children: [ + Container( + margin: const EdgeInsets.only(bottom: 10), + child: ItemDescriptionDetail( + title: s.description, + body: Container(margin: const EdgeInsets.symmetric(horizontal: 5), child: Text(state.description)), + textColor: state.elementType.getElementColorFromContext(context), + ), + ), + CharacterDetailSkillsCard(elementType: state.elementType, skills: state.skills), + if (state.builds.isNotEmpty) + ItemDescriptionDetail( + title: s.builds, + body: Column( + children: state.builds + .map((build) => CharacterDetailBuildCard( + isForSupport: build.isForSupport, + elementType: state.elementType, + weapons: build.weapons, + artifacts: build.artifacts, + subStatsToFocus: build.subStatsToFocus, + )) + .toList(), + ), + textColor: state.elementType.getElementColorFromContext(context), + ), + CharacterDetailAscensionMaterialsCard( + ascensionMaterials: state.ascensionMaterials, + elementType: state.elementType, + ), + if (state.talentAscensionsMaterials.isNotEmpty) + CharacterDetailTalentAscensionMaterialsCard.withTalents( + talentAscensionMaterials: state.talentAscensionsMaterials, + elementType: state.elementType, + ), + if (state.multiTalentAscensionMaterials != null && state.multiTalentAscensionMaterials!.isNotEmpty) + CharacterDetailTalentAscensionMaterialsCard.withMultiTalents( + multiTalentAscensionMaterials: state.multiTalentAscensionMaterials!, + elementType: state.elementType, + ), + CharacterDetailPassiveCard(elementType: state.elementType, passives: state.passives), + CharacterDetailConstellationsCard(elementType: state.elementType, constellations: state.constellations), + if (state.stats.isNotEmpty) + CharacterDetailStatsCard( + elementType: state.elementType, + stats: state.stats, + subStatType: state.subStatType, + ), + ], + ), + ), + ), + ), + ), + ); + } +} + +class _LandscapeLayout extends StatelessWidget { + const _LandscapeLayout({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final s = S.of(context); + final tabColor = Theme.of(context).brightness == Brightness.dark ? Colors.white : Colors.black; + return BlocBuilder( + builder: (ctx, state) => state.map( + loading: (_) => const Loading(), + loaded: (state) { + return DefaultTabController( + length: 6, + //had to use a container to keep the background color on the system bar + child: Container( + color: state.elementType.getElementColorFromContext(context), + padding: const EdgeInsets.only(right: 20), + child: SafeArea( + child: Scaffold( + appBar: AppBar( + toolbarHeight: 50, + backgroundColor: Colors.transparent, + foregroundColor: Colors.pink, + shadowColor: Colors.transparent, + automaticallyImplyLeading: false, + flexibleSpace: TabBar( + physics: const BouncingScrollPhysics(), + indicatorColor: state.elementType.getElementColorFromContext(context), + isScrollable: true, + labelPadding: const EdgeInsets.symmetric(horizontal: 30), + labelColor: tabColor, + tabs: [ + Tab(text: s.skills), + Tab(text: s.passives), + Tab(text: s.constellations), + Tab(text: s.materials), + Tab(text: s.builds), + Tab(text: s.stats), + ], + ), + ), + body: _LandscapeTabView( + description: state.description, + elementType: state.elementType, + subStatType: state.subStatType, + ascensionMaterials: state.ascensionMaterials, + talentAscensionsMaterials: state.talentAscensionsMaterials, + multiTalentAscensionMaterials: state.multiTalentAscensionMaterials ?? [], + constellations: state.constellations, + passives: state.passives, + skills: state.skills, + builds: state.builds, + stats: state.stats, + ), + ), + ), + ), + ); + }, + ), + ); + } +} + +class _LandscapeTabView extends StatelessWidget { + final ElementType elementType; + final StatType subStatType; + final String description; + final List skills; + final List passives; + final List constellations; + final List ascensionMaterials; + final List talentAscensionsMaterials; + final List multiTalentAscensionMaterials; + final List builds; + final List stats; + final EdgeInsets padding; + + const _LandscapeTabView({ + Key? key, + required this.elementType, + required this.subStatType, + required this.description, + required this.skills, + required this.passives, + required this.constellations, + required this.ascensionMaterials, + required this.talentAscensionsMaterials, + this.multiTalentAscensionMaterials = const [], + required this.builds, + required this.stats, + this.padding = const EdgeInsets.symmetric(horizontal: 25), + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final s = S.of(context); + return Padding( + padding: padding, + child: TabBarView( + physics: const BouncingScrollPhysics(), + children: [ + SingleChildScrollView( + physics: const BouncingScrollPhysics(), child: Column( - mainAxisSize: MainAxisSize.min, children: [ Container( margin: const EdgeInsets.only(bottom: 10), child: ItemDescriptionDetail( title: s.description, - body: Container(margin: const EdgeInsets.symmetric(horizontal: 5), child: Text(state.description)), - textColor: state.elementType.getElementColorFromContext(context), + body: Container(margin: const EdgeInsets.symmetric(horizontal: 5), child: Text(description)), + textColor: elementType.getElementColorFromContext(context), ), ), - CharacterDetailSkillsCard(elementType: state.elementType, skills: state.skills), - if (state.builds.isNotEmpty) - ItemDescriptionDetail( - title: s.builds, - body: Column( - children: state.builds - .map((build) => CharacterDetailBuildCard( - isForSupport: build.isForSupport, - elementType: state.elementType, - weapons: build.weapons, - artifacts: build.artifacts, - subStatsToFocus: build.subStatsToFocus, - )) - .toList(), - ), - textColor: state.elementType.getElementColorFromContext(context), - ), + CharacterDetailSkillsCard(elementType: elementType, skills: skills), + ], + ), + ), + SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: CharacterDetailPassiveCard(elementType: elementType, passives: passives), + ), + SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: CharacterDetailConstellationsCard( + elementType: elementType, + constellations: constellations, + ), + ), + SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ CharacterDetailAscensionMaterialsCard( - ascensionMaterials: state.ascensionMaterials, - elementType: state.elementType, + ascensionMaterials: ascensionMaterials, + elementType: elementType, ), - if (state.talentAscensionsMaterials.isNotEmpty) - CharacterDetailTalentAscensionMaterialsCard.withTalents( - talentAscensionMaterials: state.talentAscensionsMaterials, - elementType: state.elementType, - ), - if (state.multiTalentAscensionMaterials != null && state.multiTalentAscensionMaterials!.isNotEmpty) + CharacterDetailTalentAscensionMaterialsCard.withTalents( + talentAscensionMaterials: talentAscensionsMaterials, + elementType: elementType, + ), + if (multiTalentAscensionMaterials.isNotEmpty) CharacterDetailTalentAscensionMaterialsCard.withMultiTalents( - multiTalentAscensionMaterials: state.multiTalentAscensionMaterials!, - elementType: state.elementType, - ), - CharacterDetailPassiveCard(elementType: state.elementType, passives: state.passives), - CharacterDetailConstellationsCard(elementType: state.elementType, constellations: state.constellations), - if (state.stats.isNotEmpty) - CharacterDetailStatsCard( - elementType: state.elementType, - stats: state.stats, - subStatType: state.subStatType, + multiTalentAscensionMaterials: multiTalentAscensionMaterials, + elementType: elementType, ), ], ), ), - ), + SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: ItemDescriptionDetail( + title: s.builds, + body: Column( + children: builds + .map((build) => CharacterDetailBuildCard( + isForSupport: build.isForSupport, + elementType: elementType, + weapons: build.weapons, + artifacts: build.artifacts, + subStatsToFocus: build.subStatsToFocus, + )) + .toList(), + ), + textColor: elementType.getElementColorFromContext(context), + ), + ), + SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: CharacterDetailStatsCard( + elementType: elementType, + stats: stats, + subStatType: subStatType, + ), + ), + ], ), ); } diff --git a/lib/presentation/character/widgets/character_detail_constellations_card.dart b/lib/presentation/character/widgets/character_detail_constellations_card.dart index 4065096d6..34e1f3c3b 100644 --- a/lib/presentation/character/widgets/character_detail_constellations_card.dart +++ b/lib/presentation/character/widgets/character_detail_constellations_card.dart @@ -6,6 +6,7 @@ import 'package:genshindb/presentation/shared/bullet_list.dart'; import 'package:genshindb/presentation/shared/extensions/element_type_extensions.dart'; import 'package:genshindb/presentation/shared/item_description_detail.dart'; import 'package:genshindb/presentation/shared/styles.dart'; +import 'package:responsive_grid/responsive_grid.dart'; class CharacterDetailConstellationsCard extends StatelessWidget { final ElementType elementType; @@ -20,11 +21,12 @@ class CharacterDetailConstellationsCard extends StatelessWidget { @override Widget build(BuildContext context) { final s = S.of(context); - final items = constellations.map((e) => _buildConstellationCard(e, context)).toList(); - final body = Wrap(alignment: WrapAlignment.center, children: items); return ItemDescriptionDetail( title: s.constellations, - body: body, + body: ResponsiveGridRow( + crossAxisAlignment: CrossAxisAlignment.center, + children: constellations.map((e) => ResponsiveGridCol(md: 6, lg: 6, xl: 6, child: _buildConstellationCard(e, context))).toList(), + ), textColor: elementType.getElementColorFromContext(context), ); } diff --git a/lib/presentation/character/widgets/character_detail_general_card.dart b/lib/presentation/character/widgets/character_detail_general_card.dart index 497aa1ddc..bd386b59c 100644 --- a/lib/presentation/character/widgets/character_detail_general_card.dart +++ b/lib/presentation/character/widgets/character_detail_general_card.dart @@ -50,7 +50,8 @@ class CharacterDetailGeneralCard extends StatelessWidget { child: Padding( padding: Styles.edgeInsetAll10, child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.center, children: [ Text(name, style: theme.textTheme.headline5!.copyWith(fontWeight: FontWeight.bold, color: Colors.white)), Rarity(stars: rarity, starSize: 25, alignment: MainAxisAlignment.start), @@ -85,12 +86,11 @@ class CharacterDetailGeneralCard extends StatelessWidget { widget: Icon(isFemale ? GenshinDb.female : GenshinDb.male, color: isFemale ? Colors.pink : Colors.blue), useColumn: false, ), - if (birthday.isNotNullEmptyOrWhitespace) - ItemDescription( - title: s.birthday, - widget: Text(birthday!, style: const TextStyle(color: Colors.white)), - useColumn: false, - ), + ItemDescription( + title: s.birthday, + widget: Text(birthday.isNotNullEmptyOrWhitespace ? birthday! : s.na, style: const TextStyle(color: Colors.white)), + useColumn: false, + ), ], ), ), diff --git a/lib/presentation/character/widgets/character_detail_passive_card.dart b/lib/presentation/character/widgets/character_detail_passive_card.dart index 31276d05a..eea3474fe 100644 --- a/lib/presentation/character/widgets/character_detail_passive_card.dart +++ b/lib/presentation/character/widgets/character_detail_passive_card.dart @@ -6,6 +6,7 @@ import 'package:genshindb/presentation/shared/bullet_list.dart'; import 'package:genshindb/presentation/shared/extensions/element_type_extensions.dart'; import 'package:genshindb/presentation/shared/item_description_detail.dart'; import 'package:genshindb/presentation/shared/styles.dart'; +import 'package:responsive_grid/responsive_grid.dart'; class CharacterDetailPassiveCard extends StatelessWidget { final ElementType elementType; @@ -20,11 +21,18 @@ class CharacterDetailPassiveCard extends StatelessWidget { @override Widget build(BuildContext context) { final s = S.of(context); - final items = passives.map((e) => _buildPassiveCard(e, context)).toList(); - final body = Wrap(alignment: WrapAlignment.center, children: items); return ItemDescriptionDetail( title: s.passives, - body: body, + body: ResponsiveGridRow( + children: passives + .map((e) => ResponsiveGridCol( + md: 6, + lg: 6, + xl: 6, + child: _buildPassiveCard(e, context), + )) + .toList(), + ), textColor: elementType.getElementColorFromContext(context), ); } diff --git a/lib/presentation/character/widgets/character_detail_top.dart b/lib/presentation/character/widgets/character_detail_top.dart index 2505f0838..3136a9a32 100644 --- a/lib/presentation/character/widgets/character_detail_top.dart +++ b/lib/presentation/character/widgets/character_detail_top.dart @@ -2,11 +2,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:genshindb/application/bloc.dart'; import 'package:genshindb/generated/l10n.dart'; +import 'package:genshindb/presentation/character/widgets/character_detail.dart'; +import 'package:genshindb/presentation/character/widgets/character_detail_general_card.dart'; import 'package:genshindb/presentation/shared/extensions/element_type_extensions.dart'; import 'package:genshindb/presentation/shared/extensions/i18n_extensions.dart'; import 'package:genshindb/presentation/shared/loading.dart'; - -import '../../character/widgets/character_detail.dart'; +import 'package:responsive_builder/responsive_builder.dart'; class CharacterDetailTop extends StatelessWidget { const CharacterDetailTop({ @@ -18,47 +19,35 @@ class CharacterDetailTop extends StatelessWidget { final s = S.of(context); final mediaQuery = MediaQuery.of(context); final isPortrait = mediaQuery.orientation == Orientation.portrait; - final descriptionWidth = mediaQuery.size.width / (isPortrait ? 1.2 : 2); - //TODO: IM NOT SURE HOW THIS WILL LOOK LIKE IN BIGGER DEVICES - // final padding = mediaQuery.padding; - // final screenHeight = mediaQuery.size.height - padding.top - padding.bottom; + final imgHeight = mediaQuery.size.height; + final device = getDeviceType(mediaQuery.size); + final descriptionWidth = (mediaQuery.size.width / (isPortrait ? 1 : 2)) / (device == DeviceScreenType.mobile ? 1.2 : 2); return BlocBuilder( builder: (ctx, state) => state.map( loading: (_) => const Loading(useScaffold: false), loaded: (state) => Container( + height: isPortrait ? getTopHeightForPortrait(context) : null, color: state.elementType.getElementColorFromContext(context), child: Stack( fit: StackFit.passthrough, alignment: Alignment.center, children: [ + ShadowImage(fullImage: state.fullImage, secondFullImage: state.secondFullImage), Align( - alignment: Alignment.topRight, - child: Container( - transform: Matrix4.translationValues(60, -30, 0.0), - child: Opacity( - opacity: 0.5, - child: Image.asset( - state.secondFullImage ?? state.fullImage, - width: 350, - height: imgHeight, - ), - ), - ), - ), - Align( - alignment: Alignment.topLeft, + alignment: Alignment.bottomLeft, child: Image.asset( state.fullImage, - width: 340, - height: imgHeight, + fit: BoxFit.fill, + width: isPortrait ? 340 : null, + height: isPortrait ? imgHeight : null, ), ), Align( - alignment: Alignment.bottomCenter, - child: Container( + alignment: isPortrait ? Alignment.center : Alignment.bottomCenter, + child: SizedBox( + height: charDescriptionHeight, width: descriptionWidth, - margin: const EdgeInsets.symmetric(horizontal: 30), child: CharacterDetailGeneralCard( elementType: state.elementType, isFemale: state.isFemale, @@ -104,3 +93,52 @@ class CharacterDetailTop extends StatelessWidget { context.read().add(event); } } + +class ShadowImage extends StatelessWidget { + final String fullImage; + final String? secondFullImage; + + const ShadowImage({ + Key? key, + required this.fullImage, + this.secondFullImage, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final mediaQuery = MediaQuery.of(context); + final isPortrait = mediaQuery.orientation == Orientation.portrait; + final imgHeight = mediaQuery.size.height; + if (!isPortrait) { + return Positioned( + top: 0, + right: -40, + bottom: 30, + child: Opacity( + opacity: 0.5, + child: Image.asset( + secondFullImage ?? fullImage, + fit: BoxFit.fill, + width: isPortrait ? 350 : null, + height: isPortrait ? imgHeight : null, + ), + ), + ); + } + return Align( + alignment: Alignment.bottomRight, + child: Container( + transform: Matrix4.translationValues(60, -30, 0.0), + child: Opacity( + opacity: 0.5, + child: Image.asset( + secondFullImage ?? fullImage, + fit: BoxFit.fill, + width: isPortrait ? 350 : null, + height: isPortrait ? imgHeight : null, + ), + ), + ), + ); + } +}