From 5bd4e7e472fb302659c7d376b2cef1dc9eae6510 Mon Sep 17 00:00:00 2001 From: SethCohen Date: Sun, 26 Mar 2023 01:56:01 -0400 Subject: [PATCH] feat: added popup instructions feat: added custom icon button feat: added custom colour palette theme extension --- src/lib/themes/comfy.dart | 179 ++++++++++++++++--------- src/lib/widgets/custom_iconbutton.dart | 41 ++++++ src/lib/widgets/flashcard.dart | 68 ++++++++-- 3 files changed, 219 insertions(+), 69 deletions(-) create mode 100644 src/lib/widgets/custom_iconbutton.dart diff --git a/src/lib/themes/comfy.dart b/src/lib/themes/comfy.dart index 7231ea3..8a8558b 100644 --- a/src/lib/themes/comfy.dart +++ b/src/lib/themes/comfy.dart @@ -1,66 +1,125 @@ import 'package:flutter/material.dart'; -// Palette: https://coolors.co/4a5b6e-425366-f8cdc6-9ec1cc-f5efee -// background: Color(0xFF4A5B6E) -// surface: Color(0xFF425366) -// selected: Color(0xFFF8CDC6) -// unselected: Color(0xFF9EC1CC) -// text/hover: Color(0xFFF5EFEE) -// error: Color(0xFFC9465E) +@immutable +class CustomPalette extends ThemeExtension { + const CustomPalette({ + required this.background, + required this.surface, + required this.selected, + required this.unselected, + required this.text, + required this.hover, + required this.error, + }); + + final Color? background; + final Color? surface; + final Color? selected; + final Color? unselected; + final Color? text; + final Color? hover; + final Color? error; + + @override + CustomPalette copyWith( + {Color? background, + Color? surface, + Color? selected, + Color? unselected, + Color? text, + Color? hover, + Color? error}) { + return CustomPalette( + background: background ?? this.background, + surface: surface ?? this.surface, + selected: selected ?? this.selected, + unselected: unselected ?? this.unselected, + text: text ?? this.text, + hover: hover ?? this.hover, + error: error ?? this.error, + ); + } + + @override + CustomPalette lerp(CustomPalette? other, double t) { + if (other is! CustomPalette) { + return this; + } + return CustomPalette( + background: Color.lerp(background, other.background, t), + surface: Color.lerp(surface, other.surface, t), + selected: Color.lerp(selected, other.selected, t), + unselected: Color.lerp(unselected, other.unselected, t), + text: Color.lerp(text, other.text, t), + hover: Color.lerp(hover, other.hover, t), + error: Color.lerp(error, other.error, t), + ); + } +} final comfyTheme = ThemeData( - useMaterial3: true, - colorScheme: ColorScheme.fromSeed( - seedColor: const Color(0xFFF8CDC6), - background: const Color(0xFF4A5B6E), - ), - appBarTheme: const AppBarTheme( - elevation: 0, - backgroundColor: Colors.transparent, - foregroundColor: Color(0xFF9EC1CC), - titleTextStyle: TextStyle( - color: Color(0xFFF5EFEE), - fontSize: 22, - fontWeight: FontWeight.w500, + useMaterial3: true, + colorScheme: ColorScheme.fromSeed( + seedColor: const Color(0xFFF8CDC6), + background: const Color(0xFF4A5B6E), ), - ), - tabBarTheme: TabBarTheme( - labelColor: const Color(0xFFF8CDC6), - unselectedLabelColor: const Color(0xFF9EC1CC), - indicator: const BoxDecoration(), - splashFactory: NoSplash.splashFactory, - overlayColor: MaterialStateProperty.all(Colors.transparent), - ), - textButtonTheme: TextButtonThemeData( - style: ButtonStyle( - foregroundColor: - MaterialStateProperty.all(const Color(0xFFF8CDC6)), - overlayColor: MaterialStateProperty.all(const Color(0xFF425366)), - ), - ), - cardTheme: const CardTheme( + appBarTheme: const AppBarTheme( elevation: 0, - color: Color(0xFF425366), - surfaceTintColor: Colors.transparent), - progressIndicatorTheme: const ProgressIndicatorThemeData( - color: Color(0xFFF8CDC6), - linearTrackColor: Color(0xFF4A5B6E), - ), - textTheme: const TextTheme( - displayLarge: TextStyle(color: Color(0xFFF5EFEE)), - displayMedium: TextStyle(color: Color(0xFFF5EFEE)), - displaySmall: TextStyle(color: Color(0xFFF5EFEE)), - headlineLarge: TextStyle(color: Color(0xFFF5EFEE)), - headlineMedium: TextStyle(color: Color(0xFFF5EFEE)), - headlineSmall: TextStyle(color: Color(0xFFF5EFEE)), - titleLarge: TextStyle(color: Color(0xFFF5EFEE)), - titleMedium: TextStyle(color: Color(0xFFF5EFEE)), - titleSmall: TextStyle(color: Color(0xFFF5EFEE)), - labelLarge: TextStyle(color: Color(0xFFF5EFEE)), - labelMedium: TextStyle(color: Color(0xFFF5EFEE)), - labelSmall: TextStyle(color: Color(0xFFF5EFEE)), - bodyLarge: TextStyle(color: Color(0xFFF5EFEE)), - bodyMedium: TextStyle(color: Color(0xFFF5EFEE)), - bodySmall: TextStyle(color: Color(0xFFF5EFEE)), - ), -); + backgroundColor: Colors.transparent, + foregroundColor: Color(0xFF9EC1CC), + titleTextStyle: TextStyle( + color: Color(0xFFF5EFEE), + fontSize: 22, + fontWeight: FontWeight.w500, + ), + ), + tabBarTheme: TabBarTheme( + labelColor: const Color(0xFFF8CDC6), + unselectedLabelColor: const Color(0xFF9EC1CC), + indicator: const BoxDecoration(), + splashFactory: NoSplash.splashFactory, + overlayColor: MaterialStateProperty.all(Colors.transparent), + ), + textButtonTheme: TextButtonThemeData( + style: ButtonStyle( + foregroundColor: + MaterialStateProperty.all(const Color(0xFFF8CDC6)), + overlayColor: MaterialStateProperty.all(const Color(0xFF425366)), + ), + ), + cardTheme: const CardTheme( + elevation: 0, + color: Color(0xFF425366), + surfaceTintColor: Colors.transparent), + progressIndicatorTheme: const ProgressIndicatorThemeData( + color: Color(0xFFF8CDC6), + linearTrackColor: Color(0xFF4A5B6E), + ), + textTheme: const TextTheme( + displayLarge: TextStyle(color: Color(0xFFF5EFEE)), + displayMedium: TextStyle(color: Color(0xFFF5EFEE)), + displaySmall: TextStyle(color: Color(0xFFF5EFEE)), + headlineLarge: TextStyle(color: Color(0xFFF5EFEE)), + headlineMedium: TextStyle(color: Color(0xFFF5EFEE)), + headlineSmall: TextStyle(color: Color(0xFFF5EFEE)), + titleLarge: TextStyle(color: Color(0xFFF5EFEE)), + titleMedium: TextStyle(color: Color(0xFFF5EFEE)), + titleSmall: TextStyle(color: Color(0xFFF5EFEE)), + labelLarge: TextStyle(color: Color(0xFFF5EFEE)), + labelMedium: TextStyle(color: Color(0xFFF5EFEE)), + labelSmall: TextStyle(color: Color(0xFFF5EFEE)), + bodyLarge: TextStyle(color: Color(0xFFF5EFEE)), + bodyMedium: TextStyle(color: Color(0xFFF5EFEE)), + bodySmall: TextStyle(color: Color(0xFFF5EFEE)), + ), + extensions: const >[ + CustomPalette( + background: Color(0xFF4A5B6E), + surface: Color(0xFF425366), + selected: Color(0xFFF8CDC6), + unselected: Color(0xFF9EC1CC), + text: Color(0xFFF5EFEE), + hover: Color(0xFFF5EFEE), + error: Color(0xFFC9465E), + ), + ]); diff --git a/src/lib/widgets/custom_iconbutton.dart b/src/lib/widgets/custom_iconbutton.dart new file mode 100644 index 0000000..138ef57 --- /dev/null +++ b/src/lib/widgets/custom_iconbutton.dart @@ -0,0 +1,41 @@ +import 'package:asl/themes/comfy.dart'; +import 'package:flutter/material.dart'; + +class CustomIconButton extends StatefulWidget { + final Icon icon; + final bool? isSelected; + final VoidCallback onPressed; + + const CustomIconButton({ + super.key, + this.isSelected, + required this.icon, + required this.onPressed, + }); + + @override + State createState() => _CustomIconButtonState(); +} + +class _CustomIconButtonState extends State { + bool _isHovering = false; + + @override + Widget build(BuildContext context) { + final ThemeData themeData = Theme.of(context); + + return MouseRegion( + onEnter: (_) => setState(() => _isHovering = true), + onExit: (_) => setState(() => _isHovering = false), + child: IconButton( + icon: widget.icon, + color: _isHovering + ? themeData.extension()!.hover + : widget.isSelected! + ? themeData.extension()!.selected + : themeData.extension()!.unselected, + onPressed: widget.onPressed, + ), + ); + } +} diff --git a/src/lib/widgets/flashcard.dart b/src/lib/widgets/flashcard.dart index cea5c49..45ae2ce 100644 --- a/src/lib/widgets/flashcard.dart +++ b/src/lib/widgets/flashcard.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../models/flashcard_model.dart'; import '../providers/data_provider.dart'; +import 'custom_iconbutton.dart'; class Flashcard extends StatefulWidget { const Flashcard({ @@ -22,6 +23,7 @@ class Flashcard extends StatefulWidget { class _FlashcardState extends State { bool _isImageBlurred = true; + OverlayEntry? _popupOverlayEntry; @override Widget build(BuildContext context) { @@ -37,18 +39,33 @@ class _FlashcardState extends State { children: [ _buildTitle(), // TODO replace network images with controllable apng||video player/frame controller - _buildImage(), + Stack( + children: [ + _buildImage(), + if (!_isImageBlurred && !isEmptyInstructions) + _buildInstructionsPopup(context), + ], + ), // TODO media controls implementation _buildMediaControls(), _buildFlashcardButtons(), - // TODO replace instructions with FAB over and on bottom right of image - if (!_isImageBlurred && !isEmptyInstructions) _buildInstructions() ], ), ), ); } + Positioned _buildInstructionsPopup(BuildContext context) => Positioned( + bottom: 8, + right: 8, + child: CustomIconButton( + isSelected: _popupOverlayEntry != null, + onPressed: () => + _popupOverlayEntry == null ? _showPopup(context) : _hidePopup(), + icon: const Icon(Icons.info_outline), + ), + ); + Widget _buildDifficultyButton(String text, int quality, Color color) => TextButton( style: TextButton.styleFrom(foregroundColor: color), @@ -124,13 +141,46 @@ class _FlashcardState extends State { ); } - Widget _buildInstructions() => Padding( - padding: const EdgeInsets.all(8.0), - child: SizedBox( - height: 100, - child: SingleChildScrollView( - child: Text(widget.card.instructions), + Widget _buildInstructions(height, width) => Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SizedBox( + height: height, + width: width, + child: SingleChildScrollView( + child: Text(widget.card.instructions), + ), ), ), ); + + OverlayEntry _createPopup(BuildContext context) { + final RenderBox renderBox = context.findRenderObject() as RenderBox; + final Size size = renderBox.size; + final Offset offset = renderBox.localToGlobal(Offset.zero); + + return OverlayEntry( + builder: (BuildContext context) => Positioned( + left: offset.dx + size.width, + top: offset.dy + size.height / 2, + child: _buildInstructions(size.height, size.width), + ), + ); + } + + void _showPopup(BuildContext context) { + _popupOverlayEntry = _createPopup(context); + Overlay.of(context).insert(_popupOverlayEntry!); + } + + void _hidePopup() { + _popupOverlayEntry?.remove(); + _popupOverlayEntry = null; + } + + @override + void dispose() { + _hidePopup(); + super.dispose(); + } }