Conversation
(cherry picked from commit 96d6aea)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
PR Compliance Guide 🔍Below is a summary of compliance checks for this PR:
Compliance status legend🟢 - Fully Compliant🟡 - Partial Compliant 🔴 - Not Compliant ⚪ - Requires Further Human Verification 🏷️ - Compliance label |
||||||||||||||||||||||||
Summary of ChangesHello @omeritzics, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly enhances the application's user experience by modernizing its UI with Material Design 3 principles and introducing expressive animations. It involves adding a comprehensive set of new animated UI components, extensively updating translation files for improved clarity and pluralization, and refactoring existing UI elements to seamlessly integrate these visual and interactive improvements. The changes aim to create a more dynamic, visually engaging, and globally accessible application. Highlights
Changelog
Ignored Files
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
PR Code Suggestions ✨Explore these optional code suggestions:
|
||||||||||||||||||||
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
There was a problem hiding this comment.
Code Review
This pull request introduces a suite of new 'Expressive' UI components, including AnimatedNavigationBar, CustomAppBar, ExpressiveButton, ExpressiveIconButton, ExpressiveFilledButton, ExpressiveCard, ExpressiveListTile, ExpressiveChip, and ExpressiveRefreshIndicator, aiming to enhance the application's visual appeal and interactivity with Material Design 3 animations. The ExpressiveMotion utility class was added to centralize animation durations and curves, but its usage is inconsistent as app_button.dart and cached_app_icon.dart were simultaneously refactored to use hardcoded animation values instead of this new utility. The AddAppPage and ImportExportPage now utilize the new CustomAppBar, and the ImportExportPage also replaces standard FilledButton widgets with the new ExpressiveFilledButton. The AppsPage was updated to use ExpressiveRefreshIndicator and ExpressiveCard for its list items, along with fixed sizes for icons and spacing instead of MediaQuery based dynamic sizing. Additionally, the ImportExportPage was moved from a FloatingActionButton on the AddAppPage to a dedicated tab in the main navigation. Numerous translation files were updated to include a 'two' plural form for various strings and to refine existing translations, such as changing 'Use Material You' to 'Use Material You colors' and softening a Google verification warning. A critical review comment highlights a potential security vulnerability in the AppsProvider.import function, which lacks validation for imported JSON payloads, allowing arbitrary SharedPreferences key overwrites. Another significant concern is the removal of categorized error handling in AppsProvider.import, leading to raw error messages being displayed, which can leak sensitive information and degrade user experience. Further feedback points out unused properties in ExpressiveSurface, inefficient animation handling in AnimatedNavigationBar, and code duplication in ExpressiveChip's build method.
| import 'package:flutter/material.dart'; | ||
|
|
||
| /// Material Design Expressive Refresh Indicator with smooth animations | ||
| class ExpressiveRefreshIndicator extends StatefulWidget { | ||
| final Widget child; | ||
| final Future<void> Function()? onRefresh; | ||
| final Color? color; | ||
| final double displacement; | ||
| final bool enabled; | ||
| final Duration duration; | ||
|
|
||
| const ExpressiveRefreshIndicator({ | ||
| super.key, | ||
| required this.child, | ||
| this.onRefresh, | ||
| this.color, | ||
| this.displacement = 40.0, | ||
| this.enabled = true, | ||
| this.duration = const Duration(milliseconds: 300), | ||
| }); | ||
|
|
||
| @override | ||
| State<ExpressiveRefreshIndicator> createState() => | ||
| _ExpressiveRefreshIndicatorState(); | ||
| } | ||
|
|
||
| class _ExpressiveRefreshIndicatorState extends State<ExpressiveRefreshIndicator> | ||
| with TickerProviderStateMixin { | ||
| late AnimationController _animationController; | ||
| late Animation<double> _rotationAnimation; | ||
| late Animation<double> _scaleAnimation; | ||
| late Animation<double> _opacityAnimation; | ||
| late bool _isRefreshing = false; | ||
|
|
||
| @override | ||
| void initState() { | ||
| super.initState(); | ||
| _animationController = AnimationController( | ||
| duration: widget.duration, | ||
| vsync: this, | ||
| ); | ||
|
|
||
| _rotationAnimation = Tween<double>(begin: 0.0, end: 0.5).animate( | ||
| CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), | ||
| ); | ||
|
|
||
| _scaleAnimation = Tween<double>(begin: 1.0, end: 0.8).animate( | ||
| CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), | ||
| ); | ||
|
|
||
| _opacityAnimation = Tween<double>(begin: 0.0, end: 1.0).animate( | ||
| CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), | ||
| ); | ||
| } | ||
|
|
||
| @override | ||
| void dispose() { | ||
| _animationController.dispose(); | ||
| super.dispose(); | ||
| } | ||
|
|
||
| Future<void> _handleRefresh() async { | ||
| if (widget.onRefresh != null && widget.enabled && !_isRefreshing) { | ||
| setState(() => _isRefreshing = true); | ||
|
|
||
| // Start animations | ||
| _animationController.repeat(); | ||
|
|
||
| try { | ||
| await widget.onRefresh!(); | ||
| } finally { | ||
| if (mounted) { | ||
| // Stop animations | ||
| _animationController.stop(); | ||
| _animationController.reset(); | ||
| setState(() => _isRefreshing = false); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @override | ||
| Widget build(BuildContext context) { | ||
| final theme = Theme.of(context); | ||
| final colorScheme = theme.colorScheme; | ||
|
|
||
| return RefreshIndicator( | ||
| displacement: widget.displacement, | ||
| color: widget.color ?? colorScheme.primary, | ||
| backgroundColor: colorScheme.surface, | ||
| strokeWidth: 2.5, | ||
| onRefresh: _handleRefresh, | ||
| child: widget.child, | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
This component is named ExpressiveRefreshIndicator and its state class defines several animations (_rotationAnimation, _scaleAnimation, _opacityAnimation). However, the build method only returns a standard RefreshIndicator and does not use these animations at all. This means the "expressive" part of the component is not implemented, and it behaves just like a regular RefreshIndicator. This should be fixed to implement the intended animations or the component should be removed to avoid confusion.
| tr('importExport'), | ||
| Icons.import_export, | ||
| const ImportExportPage(), | ||
| ), | ||
| NavigationPageItem(tr('settings'), Icons.settings, const SettingsPage()), |
There was a problem hiding this comment.
The addition of the ImportExportPage to the main navigation exposes a critical vulnerability in the application's import logic. The AppsProvider.import function, which is the core of the import feature, accepts an arbitrary JSON payload and uses it to overwrite SharedPreferences keys without any validation or allow-listing. An attacker can provide a malicious JSON file (or trigger a deep link) that, when imported, modifies sensitive application settings, disables security features, or redirects the application's source URL. It is strongly recommended to implement a strict allow-list of safe settings that can be imported and to sanitize all input data.
| return [e, userMessage]; | ||
| }).toList(); | ||
| List<List<String>> errors = errorsMap.keys | ||
| .map((e) => [e, errorsMap[e].toString()]) |
There was a problem hiding this comment.
The error handling logic for importing apps now displays raw error messages directly in the UI, removing previous categorization. This is a security regression as it can leak sensitive information such as internal file paths, network details, or implementation specifics to the user. It also significantly degrades user experience by showing technical details instead of user-friendly messages like "Network connection failed" or "App not found". It is recommended to restore the error categorization logic or ensure that only safe, localized messages are displayed. This may require re-adding the removed translation keys (networkError, invalidUrlFormat, accessDenied, importFailed) to assets/translations/en.json and other language files.
| import 'package:flutter/material.dart'; | ||
| import 'package:flutter/services.dart'; | ||
|
|
||
| /// Animated Navigation Bar with expressive interactions and animations | ||
| class AnimatedNavigationBar extends StatefulWidget { | ||
| final List<NavigationDestination> destinations; | ||
| final int selectedIndex; | ||
| final ValueChanged<int>? onDestinationSelected; | ||
|
|
||
| const AnimatedNavigationBar({ | ||
| super.key, | ||
| required this.destinations, | ||
| required this.selectedIndex, | ||
| this.onDestinationSelected, | ||
| }); | ||
|
|
||
| @override | ||
| State<AnimatedNavigationBar> createState() => _AnimatedNavigationBarState(); | ||
| } | ||
|
|
||
| class _AnimatedNavigationBarState extends State<AnimatedNavigationBar> | ||
| with TickerProviderStateMixin { | ||
| late AnimationController _animationController; | ||
| late Animation<double> _slideAnimation; | ||
| late Animation<double> _fadeAnimation; | ||
| int? _previousIndex; | ||
|
|
||
| @override | ||
| void initState() { | ||
| super.initState(); | ||
| _animationController = AnimationController( | ||
| duration: const Duration(milliseconds: 300), | ||
| vsync: this, | ||
| ); | ||
|
|
||
| _slideAnimation = Tween<double>(begin: 0.0, end: 1.0).animate( | ||
| CurvedAnimation(parent: _animationController, curve: Curves.easeOutCubic), | ||
| ); | ||
|
|
||
| _fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate( | ||
| CurvedAnimation(parent: _animationController, curve: Curves.easeOutCubic), | ||
| ); | ||
|
|
||
| _previousIndex = widget.selectedIndex; | ||
| _animationController.forward(); | ||
| } | ||
|
|
||
| @override | ||
| void didUpdateWidget(AnimatedNavigationBar oldWidget) { | ||
| super.didUpdateWidget(oldWidget); | ||
| if (oldWidget.selectedIndex != widget.selectedIndex) { | ||
| _previousIndex = oldWidget.selectedIndex; | ||
| _animationController.reset(); | ||
| _animationController.forward(); | ||
| } | ||
| } | ||
|
|
||
| @override | ||
| void dispose() { | ||
| _animationController.dispose(); | ||
| super.dispose(); | ||
| } | ||
|
|
||
| void _handleDestinationSelected(int index) { | ||
| HapticFeedback.selectionClick(); | ||
| widget.onDestinationSelected?.call(index); | ||
| } | ||
|
|
||
| @override | ||
| Widget build(BuildContext context) { | ||
| final theme = Theme.of(context); | ||
| final colorScheme = theme.colorScheme; | ||
|
|
||
| return Container( | ||
| height: 80, | ||
| decoration: BoxDecoration( | ||
| color: colorScheme.surface, | ||
| boxShadow: [ | ||
| BoxShadow( | ||
| color: colorScheme.shadow.withOpacity(0.12), | ||
| blurRadius: 12, | ||
| offset: const Offset(0, -2), | ||
| ), | ||
| ], | ||
| ), | ||
| child: SafeArea( | ||
| child: Padding( | ||
| padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), | ||
| child: Row( | ||
| mainAxisAlignment: MainAxisAlignment.spaceAround, | ||
| children: widget.destinations.asMap().entries.map((entry) { | ||
| final index = entry.key; | ||
| final destination = entry.value; | ||
| final isSelected = index == widget.selectedIndex; | ||
|
|
||
| return Expanded( | ||
| child: GestureDetector( | ||
| onTap: () => _handleDestinationSelected(index), | ||
| child: AnimatedBuilder( | ||
| animation: _animationController, | ||
| builder: (context, child) { | ||
| // Calculate animation progress | ||
| final bool isEntering = | ||
| isSelected && _previousIndex != index; | ||
| final bool isExiting = | ||
| !isSelected && _previousIndex == index; | ||
|
|
||
| double scale = 1.0; | ||
| double opacity = 1.0; | ||
| double verticalOffset = 0.0; | ||
|
|
||
| if (isEntering) { | ||
| scale = 0.8 + (0.2 * _slideAnimation.value); | ||
| opacity = _fadeAnimation.value; | ||
| verticalOffset = 4.0 * (1.0 - _slideAnimation.value); | ||
| } else if (isExiting) { | ||
| scale = 1.0 - (0.2 * _slideAnimation.value); | ||
| opacity = 1.0 - (0.3 * _fadeAnimation.value); | ||
| verticalOffset = -4.0 * _slideAnimation.value; | ||
| } else if (isSelected) { | ||
| scale = 1.0; | ||
| opacity = 1.0; | ||
| verticalOffset = 0.0; | ||
| } else { | ||
| scale = 1.0; | ||
| opacity = 0.7; | ||
| verticalOffset = 0.0; | ||
| } | ||
|
|
||
| return Transform.translate( | ||
| offset: Offset(0, verticalOffset), | ||
| child: Transform.scale( | ||
| scale: scale, | ||
| child: AnimatedOpacity( | ||
| opacity: opacity, | ||
| duration: const Duration(milliseconds: 200), | ||
| child: Container( | ||
| padding: const EdgeInsets.symmetric( | ||
| horizontal: 16, | ||
| vertical: 12, | ||
| ), | ||
| decoration: BoxDecoration( | ||
| borderRadius: BorderRadius.circular(12), | ||
| color: isSelected | ||
| ? colorScheme.secondaryContainer | ||
| : Colors.transparent, | ||
| border: Border.all( | ||
| color: isSelected | ||
| ? colorScheme.secondary.withOpacity(0.3) | ||
| : Colors.transparent, | ||
| width: 1, | ||
| ), | ||
| ), | ||
| child: Column( | ||
| mainAxisSize: MainAxisSize.min, | ||
| mainAxisAlignment: MainAxisAlignment.center, | ||
| children: [ | ||
| AnimatedContainer( | ||
| duration: const Duration(milliseconds: 300), | ||
| curve: Curves.easeOutCubic, | ||
| transform: Matrix4.identity() | ||
| ..scale(isSelected ? 1.1 : 1.0), | ||
| child: IconTheme( | ||
| data: IconThemeData( | ||
| color: isSelected | ||
| ? colorScheme.onSecondaryContainer | ||
| : colorScheme.onSurface.withOpacity( | ||
| 0.7, | ||
| ), | ||
| size: 24, | ||
| ), | ||
| child: destination.icon, | ||
| ), | ||
| ), | ||
| if (isSelected) | ||
| AnimatedContainer( | ||
| duration: const Duration( | ||
| milliseconds: 200, | ||
| ), | ||
| curve: Curves.easeOutCubic, | ||
| transform: Matrix4.identity() | ||
| ..scale(_slideAnimation.value), | ||
| child: Container( | ||
| width: 4, | ||
| height: 4, | ||
| decoration: BoxDecoration( | ||
| color: colorScheme.primary, | ||
| shape: BoxShape.circle, | ||
| ), | ||
| ), | ||
| ), | ||
| ], | ||
| ), | ||
| ), | ||
| ), | ||
| ), | ||
| ); | ||
| }, | ||
| ), | ||
| ), | ||
| ); | ||
| }).toList(), | ||
| ), | ||
| ), | ||
| ), | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
The animations for scale, opacity, and verticalOffset are calculated inside the AnimatedBuilder but then passed to Transform.scale, AnimatedOpacity, and Transform.translate respectively. While this works, it's slightly inefficient and less direct than it could be. AnimatedOpacity creates its own AnimationController. You can directly use the _fadeAnimation.value on an Opacity widget inside the AnimatedBuilder to avoid this. Similarly, the transforms can be combined into a single Transform widget with a Matrix4 for better performance.
| title: Text( | ||
| widget.title, | ||
| style: TextStyle( | ||
| color: Theme.of(context).textTheme.bodyMedium!.color, | ||
| ), | ||
| ), |
There was a problem hiding this comment.
The CustomAppBar title's TextStyle only sets the color, using Theme.of(context).textTheme.bodyMedium!.color. This is not ideal for a few reasons:
- It force-unwraps a nullable property (
!), which can lead to runtime errors ifbodyMediumor its color is null. bodyMediumis generally not the correct text style for an app bar title. It's better to use styles fromappBarThemeor more appropriatetextThemestyles liketitleLarge.- Only setting the color means other properties (like font size, weight) will fall back to the
FlexibleSpaceBar's default, which might not be what you intend.
| title: Text( | |
| widget.title, | |
| style: TextStyle( | |
| color: Theme.of(context).textTheme.bodyMedium!.color, | |
| ), | |
| ), | |
| style: Theme.of(context).appBarTheme.titleTextStyle ?? Theme.of(context).textTheme.titleLarge, |
| return widget.enableAnimation | ||
| ? AnimatedBuilder( | ||
| animation: _scaleAnimation, | ||
| builder: (context, child) { | ||
| return Transform.scale( | ||
| scale: _scaleAnimation.value, | ||
| child: Material( | ||
| color: | ||
| widget.backgroundColor ?? colorScheme.secondaryContainer, | ||
| borderRadius: BorderRadius.circular(8), | ||
| child: InkWell( | ||
| onTap: widget.onTap, | ||
| borderRadius: BorderRadius.circular(8), | ||
| child: Padding( | ||
| padding: | ||
| widget.padding ?? | ||
| const EdgeInsets.symmetric( | ||
| horizontal: 12, | ||
| vertical: 4, | ||
| ), | ||
| child: Row( | ||
| mainAxisSize: MainAxisSize.min, | ||
| children: [ | ||
| if (widget.avatar != null) ...[ | ||
| widget.avatar!, | ||
| const SizedBox(width: 8), | ||
| ], | ||
| DefaultTextStyle( | ||
| style: TextStyle( | ||
| color: | ||
| widget.foregroundColor ?? | ||
| colorScheme.onSecondaryContainer, | ||
| fontSize: 14, | ||
| fontWeight: FontWeight.w500, | ||
| ), | ||
| child: widget.label, | ||
| ), | ||
| if (widget.deleteIcon != null) ...[ | ||
| const SizedBox(width: 8), | ||
| GestureDetector( | ||
| onTap: widget.onDeleted, | ||
| child: widget.deleteIcon!, | ||
| ), | ||
| ], | ||
| ], | ||
| ), | ||
| ), | ||
| ), | ||
| ), | ||
| ); | ||
| }, | ||
| ) | ||
| : Material( | ||
| color: widget.backgroundColor ?? colorScheme.secondaryContainer, | ||
| borderRadius: BorderRadius.circular(8), | ||
| child: InkWell( | ||
| onTap: widget.onTap, | ||
| borderRadius: BorderRadius.circular(8), | ||
| child: Padding( | ||
| padding: | ||
| widget.padding ?? | ||
| const EdgeInsets.symmetric(horizontal: 12, vertical: 4), | ||
| child: Row( | ||
| mainAxisSize: MainAxisSize.min, | ||
| children: [ | ||
| if (widget.avatar != null) ...[ | ||
| widget.avatar!, | ||
| const SizedBox(width: 8), | ||
| ], | ||
| DefaultTextStyle( | ||
| style: TextStyle( | ||
| color: | ||
| widget.foregroundColor ?? | ||
| colorScheme.onSecondaryContainer, | ||
| fontSize: 14, | ||
| fontWeight: FontWeight.w500, | ||
| ), | ||
| child: widget.label, | ||
| ), | ||
| if (widget.deleteIcon != null) ...[ | ||
| const SizedBox(width: 8), | ||
| GestureDetector( | ||
| onTap: widget.onDeleted, | ||
| child: widget.deleteIcon!, | ||
| ), | ||
| ], | ||
| ], | ||
| ), | ||
| ), | ||
| ), | ||
| ); |
There was a problem hiding this comment.
The build method for ExpressiveChip has two large, nearly identical branches for when enableAnimation is true or false. This code duplication makes the component harder to maintain. Consider refactoring this to extract the common UI logic into a separate build method or widget, and then conditionally wrap it with the AnimatedBuilder for the animation.
Example Refactor Structure:
Widget _buildChipContent(BuildContext context) {
// ... build the Material, InkWell, Padding, Row, etc.
}
@override
Widget build(BuildContext context) {
final chipContent = _buildChipContent(context);
if (!widget.enableAnimation) {
return chipContent;
}
return AnimatedBuilder(
animation: _scaleAnimation,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: child,
);
},
child: chipContent,
);
}| import 'package:flutter/material.dart'; | ||
|
|
||
| /// Expressive Surface - Modern surface treatments with Material You effects | ||
| class ExpressiveSurface extends StatelessWidget { | ||
| final Widget child; | ||
| final Color? color; | ||
| final double? elevation; | ||
| final BorderRadius? borderRadius; | ||
| final EdgeInsetsGeometry? padding; | ||
| final bool enableGradient; | ||
| final bool enableNoise; | ||
| final BlendMode? blendMode; | ||
|
|
||
| const ExpressiveSurface({ | ||
| super.key, | ||
| required this.child, | ||
| this.color, | ||
| this.elevation, | ||
| this.borderRadius, | ||
| this.padding, | ||
| this.enableGradient = true, | ||
| this.enableNoise = false, | ||
| this.blendMode, | ||
| }); | ||
|
|
||
| @override | ||
| Widget build(BuildContext context) { | ||
| final theme = Theme.of(context); | ||
| final colorScheme = theme.colorScheme; | ||
| final surfaceColor = color ?? colorScheme.surface; | ||
|
|
||
| return Container( | ||
| decoration: BoxDecoration( | ||
| borderRadius: borderRadius ?? BorderRadius.circular(16), | ||
| gradient: enableGradient | ||
| ? LinearGradient( | ||
| begin: Alignment.topLeft, | ||
| end: Alignment.bottomRight, | ||
| colors: [ | ||
| surfaceColor, | ||
| surfaceColor.withOpacity(0.95), | ||
| surfaceColor.withOpacity(0.9), | ||
| ], | ||
| ) | ||
| : null, | ||
| boxShadow: [ | ||
| if (elevation != null && elevation! > 0) | ||
| BoxShadow( | ||
| color: colorScheme.shadow.withOpacity(0.1), | ||
| blurRadius: elevation! * 2, | ||
| offset: Offset(0, elevation!), | ||
| ), | ||
| ], | ||
| ), | ||
| child: Container( | ||
| padding: padding, | ||
| decoration: BoxDecoration( | ||
| borderRadius: borderRadius ?? BorderRadius.circular(16), | ||
| color: enableGradient ? null : surfaceColor.withOpacity(0.9), | ||
| ), | ||
| child: child, | ||
| ), | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
| import 'package:flutter/material.dart'; | ||
| import 'package:animations/animations.dart'; | ||
|
|
||
| /// Utility class for Material Design Expressive motion and transitions | ||
| class ExpressiveMotion { | ||
| // Standard Material 3 motion durations | ||
| static const Duration durationShort = Duration(milliseconds: 150); | ||
| static const Duration durationMedium = Duration(milliseconds: 250); | ||
| static const Duration durationLong = Duration(milliseconds: 350); | ||
| static const Duration durationExtraLong = Duration(milliseconds: 500); | ||
|
|
||
| // Standard Material 3 motion curves | ||
| static const Curve standardDecelerate = Curves.decelerate; | ||
| static const Curve standardAccelerate = Curves.easeIn; | ||
| static const Curve emphasizedDecelerate = Curves.easeOutCubic; | ||
| static const Curve emphasizedAccelerate = Curves.easeInCubic; | ||
| static const Curve standard = Curves.easeInOut; | ||
|
|
||
| /// Expressive fade transition with scale | ||
| static Widget fadeScaleTransition({ | ||
| required Widget child, | ||
| required Animation<double> animation, | ||
| double scaleStart = 0.9, | ||
| double scaleEnd = 1.0, | ||
| }) { | ||
| return ScaleTransition( | ||
| scale: Tween<double>(begin: scaleStart, end: scaleEnd).animate( | ||
| CurvedAnimation(parent: animation, curve: emphasizedDecelerate), | ||
| ), | ||
| child: FadeTransition(opacity: animation, child: child), | ||
| ); | ||
| } | ||
|
|
||
| /// Expressive slide transition with fade | ||
| static Widget slideFadeTransition({ | ||
| required Widget child, | ||
| required Animation<double> animation, | ||
| Offset slideStart = const Offset(0.0, 0.1), | ||
| }) { | ||
| return SlideTransition( | ||
| position: Tween<Offset>(begin: slideStart, end: Offset.zero).animate( | ||
| CurvedAnimation(parent: animation, curve: emphasizedDecelerate), | ||
| ), | ||
| child: FadeTransition(opacity: animation, child: child), | ||
| ); | ||
| } | ||
|
|
||
| /// Expressive shared axis transition | ||
| static Widget sharedAxisTransition({ | ||
| required Widget child, | ||
| required Animation<double> animation, | ||
| SharedAxisTransitionType type = SharedAxisTransitionType.horizontal, | ||
| }) { | ||
| return SharedAxisTransition( | ||
| animation: animation, | ||
| secondaryAnimation: const AlwaysStoppedAnimation(0.0), | ||
| transitionType: type, | ||
| child: child, | ||
| ); | ||
| } | ||
|
|
||
| /// Expressive container transform | ||
| static Widget containerTransform({ | ||
| required Widget child, | ||
| required Animation<double> animation, | ||
| }) { | ||
| return SharedAxisTransition( | ||
| animation: animation, | ||
| secondaryAnimation: const AlwaysStoppedAnimation(0.0), | ||
| transitionType: SharedAxisTransitionType.scaled, | ||
| child: child, | ||
| ); | ||
| } | ||
|
|
||
| /// Expressive button press animation | ||
| static Widget expressiveButton({ | ||
| required Widget child, | ||
| required VoidCallback onPressed, | ||
| bool enabled = true, | ||
| }) { | ||
| return TweenAnimationBuilder<double>( | ||
| duration: durationShort, | ||
| tween: Tween<double>(begin: 1.0, end: 1.0), | ||
| builder: (context, scale, child) { | ||
| return Transform.scale(scale: scale, child: child); | ||
| }, | ||
| child: GestureDetector( | ||
| onTapDown: enabled ? (_) => {} : null, | ||
| onTapUp: enabled ? (_) => onPressed() : null, | ||
| onTapCancel: enabled ? () {} : null, | ||
| child: AnimatedScale( | ||
| scale: enabled ? 1.0 : 0.95, | ||
| duration: durationShort, | ||
| curve: standardDecelerate, | ||
| child: child, | ||
| ), | ||
| ), | ||
| ); | ||
| } | ||
|
|
||
| /// Expressive card hover effect | ||
| static Widget expressiveCard({ | ||
| required Widget child, | ||
| bool isHovered = false, | ||
| VoidCallback? onTap, | ||
| }) { | ||
| return AnimatedContainer( | ||
| duration: durationMedium, | ||
| curve: standardDecelerate, | ||
| transform: Matrix4.identity() | ||
| ..translate(0.0, isHovered ? -4.0 : 0.0) | ||
| ..scale(isHovered ? 1.02 : 1.0), | ||
| decoration: BoxDecoration( | ||
| borderRadius: BorderRadius.circular(20), | ||
| boxShadow: isHovered | ||
| ? [ | ||
| BoxShadow( | ||
| color: Colors.black.withOpacity(0.2), | ||
| blurRadius: 20, | ||
| offset: const Offset(0, 8), | ||
| ), | ||
| ] | ||
| : [ | ||
| BoxShadow( | ||
| color: Colors.black.withOpacity(0.1), | ||
| blurRadius: 10, | ||
| offset: const Offset(0, 4), | ||
| ), | ||
| ], | ||
| ), | ||
| child: Material( | ||
| color: Colors.transparent, | ||
| child: InkWell( | ||
| onTap: onTap, | ||
| borderRadius: BorderRadius.circular(20), | ||
| splashFactory: InkRipple.splashFactory, | ||
| child: child, | ||
| ), | ||
| ), | ||
| ); | ||
| } | ||
|
|
||
| /// Expressive list item animation | ||
| static Widget animatedListItem({ | ||
| required Widget child, | ||
| required int index, | ||
| int totalItems = 1, | ||
| }) { | ||
| return TweenAnimationBuilder<double>( | ||
| duration: durationMedium, | ||
| tween: Tween<double>(begin: 0.0, end: 1.0), | ||
| curve: emphasizedDecelerate, | ||
| builder: (context, value, child) { | ||
| return Transform.translate( | ||
| offset: Offset(0.0, 50 * (1 - value)), | ||
| child: Opacity(opacity: value, child: child), | ||
| ); | ||
| }, | ||
| child: child, | ||
| ); | ||
| } | ||
|
|
||
| /// Expressive shimmer effect | ||
| static Widget shimmerEffect({required Widget child, bool isLoading = false}) { | ||
| if (!isLoading) return child; | ||
|
|
||
| return ShaderMask( | ||
| shaderCallback: (bounds) { | ||
| return LinearGradient( | ||
| colors: [ | ||
| Colors.transparent, | ||
| Colors.white.withOpacity(0.3), | ||
| Colors.transparent, | ||
| ], | ||
| stops: const [0.0, 0.5, 1.0], | ||
| begin: const Alignment(-1.0, 0.0), | ||
| end: const Alignment(1.0, 0.0), | ||
| ).createShader(bounds); | ||
| }, | ||
| child: child, | ||
| ); | ||
| } | ||
|
|
||
| /// Expressive page route with custom transition | ||
| static PageRouteBuilder<T> expressivePageRoute<T>({ | ||
| required WidgetBuilder builder, | ||
| Duration transitionDuration = durationMedium, | ||
| RouteSettings? settings, | ||
| }) { | ||
| return PageRouteBuilder<T>( | ||
| settings: settings, | ||
| transitionDuration: transitionDuration, | ||
| pageBuilder: (context, animation, secondaryAnimation) => builder(context), | ||
| transitionsBuilder: (context, animation, secondaryAnimation, child) { | ||
| return SlideTransition( | ||
| position: | ||
| Tween<Offset>( | ||
| begin: const Offset(1.0, 0.0), | ||
| end: Offset.zero, | ||
| ).animate( | ||
| CurvedAnimation(parent: animation, curve: emphasizedDecelerate), | ||
| ), | ||
| child: FadeTransition(opacity: animation, child: child), | ||
| ); | ||
| }, | ||
| ); | ||
| } | ||
|
|
||
| /// Expressive dialog transition | ||
| static Widget expressiveDialog({ | ||
| required Widget child, | ||
| required BuildContext context, | ||
| }) { | ||
| return Dialog( | ||
| backgroundColor: Colors.transparent, | ||
| child: TweenAnimationBuilder<double>( | ||
| duration: durationMedium, | ||
| tween: Tween<double>(begin: 0.0, end: 1.0), | ||
| curve: emphasizedDecelerate, | ||
| builder: (context, value, child) { | ||
| return Transform.scale( | ||
| scale: value, | ||
| child: FadeTransition( | ||
| opacity: AlwaysStoppedAnimation(value), | ||
| child: child, | ||
| ), | ||
| ); | ||
| }, | ||
| child: child, | ||
| ), | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| /// Custom expressive page route for better navigation | ||
| class ExpressivePageRoute<T> extends PageRoute<T> { | ||
| ExpressivePageRoute({ | ||
| required this.builder, | ||
| this.transitionDuration = ExpressiveMotion.durationMedium, | ||
| this.opaque = true, | ||
| this.barrierDismissible = false, | ||
| this.barrierColor, | ||
| this.barrierLabel, | ||
| this.maintainState = true, | ||
| super.settings, | ||
| }); | ||
|
|
||
| final WidgetBuilder builder; | ||
|
|
||
| @override | ||
| final Duration transitionDuration; | ||
|
|
||
| @override | ||
| final bool opaque; | ||
|
|
||
| @override | ||
| final bool barrierDismissible; | ||
|
|
||
| @override | ||
| final Color? barrierColor; | ||
|
|
||
| @override | ||
| final String? barrierLabel; | ||
|
|
||
| @override | ||
| final bool maintainState; | ||
|
|
||
| @override | ||
| Widget buildPage( | ||
| BuildContext context, | ||
| Animation<double> animation, | ||
| Animation<double> secondaryAnimation, | ||
| ) { | ||
| return builder(context); | ||
| } | ||
|
|
||
| @override | ||
| Widget buildTransitions( | ||
| BuildContext context, | ||
| Animation<double> animation, | ||
| Animation<double> secondaryAnimation, | ||
| Widget child, | ||
| ) { | ||
| return ExpressiveMotion.slideFadeTransition( | ||
| animation: animation, | ||
| child: child, | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
This pull request introduces the ExpressiveMotion utility class, which centralizes animation durations and curves. This is a great practice for consistency. However, the same PR also modifies app_button.dart and cached_app_icon.dart to remove dependencies on this utility class and use hardcoded Duration and Curve values instead. This is contradictory. To improve maintainability, please decide on a single approach: either consistently use the ExpressiveMotion utility across all new and refactored components, or remove it if the plan is to use inline values. Using both approaches creates confusion.
PR Type
Enhancement, Documentation
Description
Introduced comprehensive expressive design system with new animated components (
ExpressiveButton,ExpressiveCard,ExpressiveListTile,ExpressiveChip,ExpressiveSurface,ExpressiveContainer,ExpressiveBadge,ExpressiveRefreshIndicator)Created
AnimatedNavigationBarcomponent with smooth scale, slide, and fade animationsAdded
CustomAppBarreusable component to replace hardcodedSliverAppBarimplementations across multiple pagesImplemented
ExpressiveMotionutility class with Material Design 3 motion durations, curves, and transition helpersRefactored UI pages (
apps.dart,settings.dart,add_app.dart,import_export.dart) to use new expressive components and custom app barUpdated all 32 translation files to clarify
useMaterialYousetting as "Use Material You colors" and softengoogleVerificationWarningP1languageSimplified error message handling in
apps_provider.dartby removing categorization logicRemoved Flutter code analysis step from CI workflow
Removed unused
animationspackage import and replacedExpressiveMotionconstants with inline duration values in some componentsDiagram Walkthrough
File Walkthrough
12 files
apps.dart
Refactor UI components with expressive design systemlib/pages/apps.dart
better consistency
InkWellwrapper to nativeListTilewith
ExpressiveCardCard.outlinedgrid items with newExpressiveCardcomponentfeaturing animations
SliverAppBarwith reusableCustomAppBarcomponentRefreshIndicatorwithExpressiveRefreshIndicatorfor enhancedanimations
MaterialPageRouteinstead ofPageRouteBuilderwithSharedAxisTransitionexpressive_buttons.dart
New expressive button components with animationslib/components/expressive_buttons.dart
ExpressiveButtonwith smooth animations, ripple effects,and elevation changes
ExpressiveIconButtonwith scale and rotation animations oninteraction
ExpressiveFilledButtonwith gradient backgrounds and coloranimations
disabled states
expressive_components.dart
New expressive card, list tile, and chip componentslib/components/expressive_components.dart
ExpressiveCardwith scale and elevation animations ontap/hover
ExpressiveListTilewrappingListTilewith animationsupport
ExpressiveChipwith elastic scale animation on appearanceinteractions
expressive_surfaces.dart
New expressive surface and container componentslib/components/expressive_surfaces.dart
ExpressiveSurfacewith gradient backgrounds and shadow effectsExpressiveContainerwith scale and opacity animationsExpressiveBadgewith elastic scale and rotation animationsimport_export.dart
Migrate import/export page to expressive buttonslib/pages/import_export.dart
FilledButton.iconwithExpressiveFilledButtonthroughout thepage
SliverAppBarwithCustomAppBarcomponentTextButton.iconwithExpressiveButtonin dialogsexpressive_motion.dart
New expressive motion and transition utilitieslib/utils/expressive_motion.dart
curves
fadeScaleTransition,slideFadeTransition,sharedAxisTransitionExpressivePageRoutefor custom page transitions with slide andfade effects
animations
settings.dart
Refactor settings page with fixed sizing and custom app barlib/pages/settings.dart
picker
useMaterialThemeSwitchUI by removingCard.outlinedwrapperSliverAppBarwithCustomAppBarcomponentColorIndicatorwidgetanimated_navigation_bar.dart
New animated navigation bar componentlib/components/animated_navigation_bar.dart
AnimatedNavigationBarwith smooth scale, slide, and fadeanimations
expressive_refresh_indicator.dart
New expressive refresh indicator componentlib/components/expressive_refresh_indicator.dart
ExpressiveRefreshIndicatorwrapping Flutter'sRefreshIndicatoradd_app.dart
Simplify add app page with custom app barlib/pages/add_app.dart
animationspackageSliverAppBarwithCustomAppBarcomponentapp_buttonimporthome.dart
Add import/export page to navigationlib/pages/home.dart
AnimatedNavigationBarimportImportExportPagenavigation item to pages listNavigationBarwithAnimatedNavigationBarcomponentcustom_app_bar.dart
New custom app bar componentlib/components/custom_app_bar.dart
CustomAppBarstateful widget as reusable app bar componentSliverAppBarwith pinned behavior and flexible space3 files
cached_app_icon.dart
Remove expressive motion utility dependencylib/components/cached_app_icon.dart
ExpressiveMotionutility classExpressiveMotionconstants with inline duration and curvevalues
Durationdirectly instead ofutility constants
app_button.dart
Remove expressive motion utility dependencylib/components/app_button.dart
ExpressiveMotionutility classExpressiveMotionconstants with inline duration and curvevalues
Durationdirectlyapps_provider.dart
Simplify error message handling in importlib/providers/apps_provider.dart
31 files
he.json
Add and update Hebrew translationsassets/translations/he.json
removeOutdatedFilter,showWebInAppView,appWithIdOrNameNotFoundtranslationsmarkInstalled,markUpdated, and other UIstrings
ar.json
Update Arabic translationsassets/translations/ar.json
useMaterialYoutranslation to clarify "colors"googleVerificationWarningP1to soften language about Updatiumfunctionality
hu.json
Update Hungarian translationsassets/translations/hu.json
useMaterialYoutranslation to clarify "colors"googleVerificationWarningP1to soften language about Updatiumfunctionality
uk.json
Update Ukrainian translationsassets/translations/uk.json
useMaterialYoutranslation to clarify "colors"googleVerificationWarningP1to soften language about Updatiumfunctionality
cs.json
Update Czech translationsassets/translations/cs.json
useMaterialYoutranslation to clarify "colors"googleVerificationWarningP1to soften language about Updatiumfunctionality
ja.json
Update Japanese translationsassets/translations/ja.json
useMaterialYoutranslation to clarify "colors"googleVerificationWarningP1to soften language about Updatiumfunctionality
pl.json
Polish translation updates for Material You and Google verificationwarningassets/translations/pl.json
useMaterialYoutranslation from "Material You" to "Użyjkolorów Material You" (Use Material You colors)
googleVerificationWarningP1to soften language aboutUpdatium's future functionality on certified Android devices
et.json
Estonian translation updates for Material You and Google verificationassets/translations/et.json
useMaterialYoutranslation to include "värve" (colors)googleVerificationWarningP1to use softer language about appfunctionality
en.json
English translation updates and error message removalassets/translations/en.json
useMaterialYoufrom "Use Material You" to "Use Material Youcolors"
networkError,invalidUrlFormat,accessDenied,importFailedgoogleVerificationWarningP1to use softer language about appfunctionality
zh-Hant-TW.json
Traditional Chinese translation updates for Material Youassets/translations/zh-Hant-TW.json
useMaterialYouto include "顏色" (colors)googleVerificationWarningP1to soften language about appfunctionality
tr.json
Turkish translation updates for Material You and Google verificationassets/translations/tr.json
useMaterialYouto "Material You renklerini kullan" (UseMaterial You colors)
googleVerificationWarningP1to use softer language about appfunctionality
vi.json
Vietnamese translation updates for Material Youassets/translations/vi.json
useMaterialYouto "Sử dụng màu Material You" (Use Material Youcolors)
googleVerificationWarningP1with improved translation andsofter language
ml.json
Malayalam translation updates for Material Youassets/translations/ml.json
useMaterialYouto "Material You നിറങ്ങൾ ഉപയോഗിക്കുക" (UseMaterial You colors)
googleVerificationWarningP1to use softer language about appfunctionality
da.json
Danish translation updates for Material Youassets/translations/da.json
useMaterialYouto "Brug Material You-farver" (Use Material Youcolors)
googleVerificationWarningP1to use softer language about appfunctionality
ca.json
Catalan translation updates for Material Youassets/translations/ca.json
useMaterialYouto "Usar colors Material You" (Use Material Youcolors)
googleVerificationWarningP1with improved translation andsofter language
it.json
Italian translation updates for Material Youassets/translations/it.json
useMaterialYouto "Utilizzate i colori Material You" (UseMaterial You colors)
googleVerificationWarningP1to use softer language about appfunctionality
fa.json
Persian translation updates for Material Youassets/translations/fa.json
useMaterialYouto "استفاده از رنگهای Material You" (UseMaterial You colors)
googleVerificationWarningP1to use softer language about appfunctionality
sv.json
Swedish translation updates for Material Youassets/translations/sv.json
useMaterialYouto "Använd Material You-färger" (Use MaterialYou colors)
googleVerificationWarningP1to use softer language about appfunctionality
bs.json
Bosnian translation updates for Material Youassets/translations/bs.json
useMaterialYouto "Koristi Material You boje" (Use MaterialYou colors)
googleVerificationWarningP1to use softer language about appfunctionality
ru.json
Russian translation updates for Material Youassets/translations/ru.json
useMaterialYouto "Использовать цвета Material You" (UseMaterial You colors)
googleVerificationWarningP1to use softer language about appfunctionality
fr.json
French translation updates for Material Youassets/translations/fr.json
useMaterialYouto "Utiliser les couleurs Material You" (UseMaterial You colors)
googleVerificationWarningP1to use softer language about appfunctionality
gl.json
Galician translation updates for Material Youassets/translations/gl.json
useMaterialYouto "Usar cores Material You" (Use Material Youcolors)
googleVerificationWarningP1to use softer language about appfunctionality
de.json
German translation updates for Material Youassets/translations/de.json
useMaterialYouto "Material You Farben verwenden" (UseMaterial You colors)
googleVerificationWarningP1to use softer language about appfunctionality
id.json
Indonesian translation updates for Material Youassets/translations/id.json
useMaterialYouto "Gunakan warna Material You" (Use MaterialYou colors)
googleVerificationWarningP1to use softer language about appfunctionality
nl.json
Dutch translation updates for Material Youassets/translations/nl.json
useMaterialYouto "Material You kleuren gebruiken" (UseMaterial You colors)
googleVerificationWarningP1to use softer language about appfunctionality
pt-BR.json
Brazilian Portuguese translation updates for Material Youassets/translations/pt-BR.json
useMaterialYouto "Usar cores Material You" (Use Material Youcolors)
googleVerificationWarningP1to use softer language about appfunctionality
es.json
Spanish translation updates for Material Youassets/translations/es.json
useMaterialYouto "Aplicar colores Material You" (ApplyMaterial You colors)
googleVerificationWarningP1to use softer language about appfunctionality
pt.json
Portuguese translation updates for Material Youassets/translations/pt.json
useMaterialYouto "Utilizar cores Material You" (Use MaterialYou colors)
googleVerificationWarningP1to use softer language about appfunctionality
ko.json
Korean translation updates for Material Youassets/translations/ko.json
useMaterialYouto "Material You 색상 사용" (Use Material Youcolors)
googleVerificationWarningP1to use softer language about appfunctionality
en-EO.json
Esperanto translation updates for Material Youassets/translations/en-EO.json
useMaterialYouto "Uzi Material You kolorojn" (Use MaterialYou colors)
googleVerificationWarningP1to use softer language about appfunctionality
zh.json
Simplified Chinese translation updates for Material Youassets/translations/zh.json
useMaterialYouto "使用 Material You 配色" (Use Material You colorscheme)
googleVerificationWarningP1to use softer language about appfunctionality
1 files
ci.yml
Remove Flutter code analysis from CI workflow.github/workflows/ci.yml
flutter analyze