diff --git a/assets/translations/en.json b/assets/translations/en.json index 797dc7f7..70639641 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -136,10 +136,6 @@ "close": "Close", "share": "Share", "appNotFound": "App not found", - "networkError": "Network connection failed", - "invalidUrlFormat": "Invalid URL format", - "accessDenied": "Access denied", - "importFailed": "Import failed", "updatiumExportHyphenatedLowercase": "updatium-export", "pickAnAPK": "Pick an APK", "appHasMoreThanOnePackage": "{} has more than one package:", diff --git a/lib/components/animated_navigation_bar.dart b/lib/components/animated_navigation_bar.dart new file mode 100644 index 00000000..bb435bad --- /dev/null +++ b/lib/components/animated_navigation_bar.dart @@ -0,0 +1,208 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +/// Animated Navigation Bar with expressive interactions and animations +class AnimatedNavigationBar extends StatefulWidget { + final List destinations; + final int selectedIndex; + final ValueChanged? onDestinationSelected; + + const AnimatedNavigationBar({ + super.key, + required this.destinations, + required this.selectedIndex, + this.onDestinationSelected, + }); + + @override + State createState() => _AnimatedNavigationBarState(); +} + +class _AnimatedNavigationBarState extends State + with TickerProviderStateMixin { + late AnimationController _animationController; + late Animation _slideAnimation; + late Animation _fadeAnimation; + int? _previousIndex; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + + _slideAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation(parent: _animationController, curve: Curves.easeOutCubic), + ); + + _fadeAnimation = Tween(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(), + ), + ), + ), + ); + } +} diff --git a/lib/components/app_button.dart b/lib/components/app_button.dart index b92c8992..236e81d3 100644 --- a/lib/components/app_button.dart +++ b/lib/components/app_button.dart @@ -61,11 +61,17 @@ class _AppTextButtonState extends State ); _scaleAnimation = Tween(begin: 1.0, end: 0.95).animate( - CurvedAnimation(parent: _animationController, curve: Curves.easeOutCubic), + CurvedAnimation( + parent: _animationController, + curve: Curves.easeOutCubic, + ), ); _opacityAnimation = Tween(begin: 1.0, end: 0.8).animate( - CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), + CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + ), ); } diff --git a/lib/components/cached_app_icon.dart b/lib/components/cached_app_icon.dart index dc2b9edc..50c9eab1 100644 --- a/lib/components/cached_app_icon.dart +++ b/lib/components/cached_app_icon.dart @@ -58,7 +58,10 @@ class _CachedAppIconState extends State vsync: this, ); _shimmerAnimation = Tween(begin: -2.0, end: 2.0).animate( - CurvedAnimation(parent: _shimmerController, curve: Curves.easeInOut), + CurvedAnimation( + parent: _shimmerController, + curve: Curves.easeInOut, + ), ); // Scale animation for interactions @@ -67,7 +70,10 @@ class _CachedAppIconState extends State vsync: this, ); _scaleAnimation = Tween(begin: 1.0, end: 0.9).animate( - CurvedAnimation(parent: _scaleController, curve: Curves.easeOutCubic), + CurvedAnimation( + parent: _scaleController, + curve: Curves.easeOutCubic, + ), ); // Rotation animation for loading/error states @@ -76,7 +82,10 @@ class _CachedAppIconState extends State vsync: this, ); _rotationAnimation = Tween(begin: 0.0, end: 0.1).animate( - CurvedAnimation(parent: _rotationController, curve: Curves.easeInOut), + CurvedAnimation( + parent: _rotationController, + curve: Curves.easeInOut, + ), ); // Start loading the icon diff --git a/lib/components/custom_app_bar.dart b/lib/components/custom_app_bar.dart new file mode 100644 index 00000000..6609e3a4 --- /dev/null +++ b/lib/components/custom_app_bar.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +class CustomAppBar extends StatefulWidget { + const CustomAppBar({super.key, required this.title}); + + final String title; + + @override + State createState() => _CustomAppBarState(); +} + +class _CustomAppBarState extends State { + @override + Widget build(BuildContext context) { + return SliverAppBar( + pinned: true, + automaticallyImplyLeading: false, + expandedHeight: 120, + flexibleSpace: FlexibleSpaceBar( + titlePadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20), + title: Text( + widget.title, + style: TextStyle( + color: Theme.of(context).textTheme.bodyMedium!.color, + ), + ), + ), + ); + } +} diff --git a/lib/components/expressive_buttons.dart b/lib/components/expressive_buttons.dart new file mode 100644 index 00000000..2ad53ad5 --- /dev/null +++ b/lib/components/expressive_buttons.dart @@ -0,0 +1,520 @@ +import 'package:flutter/material.dart'; + +/// Expressive Button - Modern Material Design 3 Expressive button +/// Features: smooth animations, ripple effects, and enhanced interactions +class ExpressiveButton extends StatefulWidget { + final Widget child; + final VoidCallback? onPressed; + final VoidCallback? onLongPress; + final Color? backgroundColor; + final Color? foregroundColor; + final Color? overlayColor; + final double? elevation; + final BorderRadius? borderRadius; + final EdgeInsetsGeometry? padding; + final bool enableAnimation; + final bool enableRipple; + final Duration animationDuration; + final ButtonStyle? style; + + const ExpressiveButton({ + super.key, + required this.child, + this.onPressed, + this.onLongPress, + this.backgroundColor, + this.foregroundColor, + this.overlayColor, + this.elevation, + this.borderRadius, + this.padding, + this.enableAnimation = true, + this.enableRipple = true, + this.animationDuration = const Duration(milliseconds: 200), + this.style, + }); + + @override + State createState() => _ExpressiveButtonState(); +} + +class _ExpressiveButtonState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _scaleAnimation; + late Animation _elevationAnimation; + late Animation _colorAnimation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: widget.animationDuration, + vsync: this, + ); + + _scaleAnimation = Tween( + begin: 1.0, + end: 0.95, + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _updateAnimations(); + } + + @override + void didUpdateWidget(ExpressiveButton oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.elevation != widget.elevation || + oldWidget.backgroundColor != widget.backgroundColor || + oldWidget.animationDuration != widget.animationDuration || + oldWidget.style != widget.style) { + _updateAnimations(); + } + } + + void _updateAnimations() { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + + // Use effective background color from style or individual property + final effectiveStyle = widget.style; + final isDisabled = widget.onPressed == null; + final effectiveBackgroundColor = + widget.backgroundColor ?? + effectiveStyle?.backgroundColor?.resolve({}) ?? + (isDisabled ? colorScheme.surface : colorScheme.primary); + + _elevationAnimation = Tween( + begin: widget.elevation ?? effectiveStyle?.elevation?.resolve({}) ?? 2.0, + end: + (widget.elevation ?? effectiveStyle?.elevation?.resolve({}) ?? 2.0) + + 4.0, + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); + + _colorAnimation = ColorTween( + begin: effectiveBackgroundColor, + end: isDisabled + ? effectiveBackgroundColor + : effectiveBackgroundColor.withOpacity(0.8), + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _handleTapDown(TapDownDetails details) { + if (widget.enableAnimation && widget.onPressed != null) { + _controller.forward(); + } + } + + void _handleTapUp(TapUpDetails details) { + if (widget.enableAnimation) { + _controller.reverse(); + } + widget.onPressed?.call(); + } + + void _handleTapCancel() { + if (widget.enableAnimation) { + _controller.reverse(); + } + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + + // Apply style if provided, otherwise use individual properties + final effectiveStyle = widget.style; + final isDisabled = widget.onPressed == null; + final effectiveBackgroundColor = + widget.backgroundColor ?? + effectiveStyle?.backgroundColor?.resolve({}) ?? + (isDisabled ? colorScheme.surface : colorScheme.primary); + final effectiveForegroundColor = + widget.foregroundColor ?? + effectiveStyle?.foregroundColor?.resolve({}) ?? + (isDisabled + ? colorScheme.onSurface.withOpacity(0.38) + : colorScheme.onPrimary); + final effectiveElevation = + widget.elevation ?? effectiveStyle?.elevation?.resolve({}) ?? 2.0; + // Extract border radius from shape if it's a RoundedRectangleBorder, otherwise use default + BorderRadius? shapeBorderRadius; + final shape = effectiveStyle?.shape?.resolve({}); + if (shape is RoundedRectangleBorder) { + shapeBorderRadius = shape.borderRadius.resolve( + Directionality.of(context), + ); + } + + final effectiveBorderRadius = + widget.borderRadius ?? shapeBorderRadius ?? BorderRadius.circular(12); + final effectivePadding = + widget.padding ?? + effectiveStyle?.padding?.resolve({}) ?? + const EdgeInsets.symmetric(horizontal: 24, vertical: 12); + final effectiveOverlayColor = + widget.overlayColor ?? effectiveStyle?.overlayColor?.resolve({}); + + return AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return Material( + elevation: _elevationAnimation.value, + borderRadius: effectiveBorderRadius, + color: _colorAnimation.value, + shadowColor: colorScheme.shadow.withOpacity(0.2), + child: InkWell( + onTapDown: widget.onPressed != null ? _handleTapDown : null, + onTapUp: widget.onPressed != null ? _handleTapUp : null, + onTapCancel: _handleTapCancel, + onLongPress: widget.onLongPress, + borderRadius: effectiveBorderRadius, + splashFactory: widget.enableRipple + ? InkRipple.splashFactory + : NoSplash.splashFactory, + highlightColor: + effectiveOverlayColor?.withOpacity(0.1) ?? + colorScheme.primary.withOpacity(0.1), + child: Container( + padding: effectivePadding, + decoration: BoxDecoration( + borderRadius: effectiveBorderRadius, + border: Border.all( + color: colorScheme.outline.withOpacity(0.2), + width: 1, + ), + ), + child: DefaultTextStyle( + style: TextStyle( + color: effectiveForegroundColor, + fontSize: 16, + fontWeight: FontWeight.w600, + letterSpacing: 0.5, + ), + child: widget.child, + ), + ), + ), + ); + }, + ); + } +} + +/// Expressive Icon Button - Modern icon button with animations +class ExpressiveIconButton extends StatefulWidget { + final Widget icon; + final VoidCallback? onPressed; + final VoidCallback? onLongPress; + final Color? backgroundColor; + final Color? foregroundColor; + final double? iconSize; + final double? size; + final bool enableAnimation; + final bool enableRipple; + final Duration animationDuration; + + const ExpressiveIconButton({ + super.key, + required this.icon, + this.onPressed, + this.onLongPress, + this.backgroundColor, + this.foregroundColor, + this.iconSize, + this.size, + this.enableAnimation = true, + this.enableRipple = true, + this.animationDuration = const Duration(milliseconds: 200), + }); + + @override + State createState() => _ExpressiveIconButtonState(); +} + +class _ExpressiveIconButtonState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _scaleAnimation; + late Animation _rotationAnimation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: widget.animationDuration, + vsync: this, + ); + + _scaleAnimation = Tween( + begin: 1.0, + end: 0.95, + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); + + _rotationAnimation = Tween( + begin: 0.0, + end: 0.1, + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _handleTapDown(TapDownDetails details) { + if (widget.enableAnimation && widget.onPressed != null) { + _controller.forward(); + } + } + + void _handleTapUp(TapUpDetails details) { + if (widget.enableAnimation) { + _controller.reverse(); + } + widget.onPressed?.call(); + } + + void _handleTapCancel() { + if (widget.enableAnimation) { + _controller.reverse(); + } + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + final buttonSize = widget.size ?? 40.0; + final isDisabled = widget.onPressed == null; + + return AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return Transform.scale( + scale: _scaleAnimation.value, + child: Transform.rotate( + angle: _rotationAnimation.value, + child: Material( + elevation: widget.onPressed != null ? 4.0 : 2.0, + borderRadius: BorderRadius.circular(12), + color: isDisabled + ? colorScheme.surface + : (widget.backgroundColor ?? colorScheme.surface), + shadowColor: colorScheme.shadow.withOpacity(0.2), + child: InkWell( + onTapDown: widget.onPressed != null ? _handleTapDown : null, + onTapUp: widget.onPressed != null ? _handleTapUp : null, + onTapCancel: _handleTapCancel, + onLongPress: widget.onLongPress, + borderRadius: BorderRadius.circular(12), + splashFactory: widget.enableRipple + ? InkRipple.splashFactory + : NoSplash.splashFactory, + child: SizedBox( + width: buttonSize, + height: buttonSize, + child: IconTheme( + data: IconThemeData( + size: widget.iconSize ?? 24.0, + color: isDisabled + ? colorScheme.onSurface.withOpacity(0.38) + : (widget.foregroundColor ?? + (widget.onPressed != null + ? colorScheme.primary + : colorScheme.onSurface.withOpacity(0.6))), + ), + child: Center(child: widget.icon), + ), + ), + ), + ), + ), + ); + }, + ); + } +} + +/// Expressive Filled Button - Tonal style button with enhanced animations +class ExpressiveFilledButton extends StatefulWidget { + final Widget child; + final VoidCallback? onPressed; + final VoidCallback? onLongPress; + final Color? backgroundColor; + final Color? foregroundColor; + final EdgeInsetsGeometry? padding; + final ButtonStyle? style; + final bool enableAnimation; + final Duration animationDuration; + + const ExpressiveFilledButton({ + super.key, + required this.child, + this.onPressed, + this.onLongPress, + this.backgroundColor, + this.foregroundColor, + this.padding, + this.style, + this.enableAnimation = true, + this.animationDuration = const Duration(milliseconds: 200), + }); + + @override + State createState() => _ExpressiveFilledButtonState(); +} + +class _ExpressiveFilledButtonState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _scaleAnimation; + late Animation _colorAnimation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: widget.animationDuration, + vsync: this, + ); + + _scaleAnimation = Tween( + begin: 1.0, + end: 0.95, + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _updateAnimations(); + } + + @override + void didUpdateWidget(ExpressiveFilledButton oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.backgroundColor != widget.backgroundColor || + oldWidget.animationDuration != widget.animationDuration) { + _updateAnimations(); + } + } + + void _updateAnimations() { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + + final effectiveStyle = widget.style; + final isDisabled = widget.onPressed == null; + final effectiveBackgroundColor = + widget.backgroundColor ?? + effectiveStyle?.backgroundColor?.resolve({}) ?? + (isDisabled ? colorScheme.surface : colorScheme.primary); + + _colorAnimation = ColorTween( + begin: effectiveBackgroundColor, + end: isDisabled + ? effectiveBackgroundColor + : effectiveBackgroundColor.withOpacity(0.8), + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _handleTapDown(TapDownDetails details) { + if (widget.enableAnimation && widget.onPressed != null) { + _controller.forward(); + } + } + + void _handleTapUp(TapUpDetails details) { + if (widget.enableAnimation) { + _controller.reverse(); + } + widget.onPressed?.call(); + } + + void _handleTapCancel() { + if (widget.enableAnimation) { + _controller.reverse(); + } + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + final isDisabled = widget.onPressed == null; + final effectiveForegroundColor = + widget.foregroundColor ?? colorScheme.onPrimary; + + return AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return Transform.scale( + scale: _scaleAnimation.value, + child: Material( + elevation: 2.0, + borderRadius: BorderRadius.circular(12), + color: _colorAnimation.value, + shadowColor: colorScheme.shadow.withOpacity(0.2), + child: InkWell( + onTapDown: widget.onPressed != null ? _handleTapDown : null, + onTapUp: widget.onPressed != null ? _handleTapUp : null, + onTapCancel: _handleTapCancel, + onLongPress: widget.onLongPress, + borderRadius: BorderRadius.circular(12), + splashFactory: InkRipple.splashFactory, + child: Container( + padding: + widget.padding ?? + const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + _colorAnimation.value ?? + (widget.backgroundColor ?? colorScheme.primary), + (_colorAnimation.value ?? + (widget.backgroundColor ?? colorScheme.primary)) + .withOpacity(0.8), + ], + ), + ), + child: DefaultTextStyle( + style: TextStyle( + color: isDisabled + ? colorScheme.onSurface.withOpacity(0.38) + : effectiveForegroundColor, + fontSize: 16, + fontWeight: FontWeight.w600, + letterSpacing: 0.5, + ), + child: widget.child, + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/components/expressive_components.dart b/lib/components/expressive_components.dart new file mode 100644 index 00000000..d782ca9f --- /dev/null +++ b/lib/components/expressive_components.dart @@ -0,0 +1,386 @@ +import 'package:flutter/material.dart'; + +/// Expressive Card - A modern Material Design 3 Expressive card component +/// Features: subtle animations, modern styling, and enhanced interactions +class ExpressiveCard extends StatefulWidget { + final Widget child; + final VoidCallback? onTap; + final VoidCallback? onLongPress; + final EdgeInsetsGeometry? margin; + final EdgeInsetsGeometry? padding; + final Color? color; + final double? elevation; + final BorderRadius? borderRadius; + final bool enableAnimation; + final Duration animationDuration; + + const ExpressiveCard({ + super.key, + required this.child, + this.onTap, + this.onLongPress, + this.margin, + this.padding, + this.color, + this.elevation, + this.borderRadius, + this.enableAnimation = true, + this.animationDuration = const Duration(milliseconds: 200), + }); + + @override + State createState() => _ExpressiveCardState(); +} + +class _ExpressiveCardState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _scaleAnimation; + late Animation _elevationAnimation; + bool _isHovered = false; + bool _isPressed = false; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: widget.animationDuration, + vsync: this, + ); + + _scaleAnimation = Tween(begin: 1.0, end: 0.98).animate( + CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), + ); + + _elevationAnimation = + Tween( + begin: widget.elevation ?? 2.0, + end: (widget.elevation ?? 2.0) + 4.0, + ).animate( + CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + ), + ); + } + + @override + void didUpdateWidget(ExpressiveCard oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.elevation != widget.elevation || + oldWidget.animationDuration != widget.animationDuration) { + _updateAnimations(); + } + } + + void _updateAnimations() { + _elevationAnimation = + Tween( + begin: widget.elevation ?? 2.0, + end: (widget.elevation ?? 2.0) + 4.0, + ).animate( + CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + ), + ); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + void _handleTapDown(TapDownDetails details) { + if (widget.enableAnimation) { + setState(() => _isPressed = true); + _animationController.forward(); + } + } + + void _handleTapUp(TapUpDetails details) { + if (widget.enableAnimation) { + setState(() => _isPressed = false); + _animationController.reverse(); + } + widget.onTap?.call(); + } + + void _handleTapCancel() { + if (widget.enableAnimation) { + setState(() => _isPressed = false); + _animationController.reverse(); + } + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + + return AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return MouseRegion( + onEnter: (_) => setState(() => _isHovered = true), + onExit: (_) => setState(() => _isHovered = false), + child: GestureDetector( + onTapDown: widget.onTap != null ? _handleTapDown : null, + onTapUp: widget.onTap != null ? _handleTapUp : null, + onTapCancel: widget.onTap != null ? _handleTapCancel : null, + onLongPress: widget.onLongPress, + child: AnimatedContainer( + duration: widget.animationDuration, + margin: + widget.margin ?? + const EdgeInsets.symmetric(horizontal: 4, vertical: 2), + transform: Matrix4.identity()..scale(_scaleAnimation.value), + child: Material( + elevation: _elevationAnimation.value, + borderRadius: widget.borderRadius ?? BorderRadius.circular(16), + color: widget.color ?? colorScheme.surface, + shadowColor: colorScheme.shadow.withOpacity(0.1), + child: AnimatedContainer( + duration: widget.animationDuration, + decoration: BoxDecoration( + borderRadius: + widget.borderRadius ?? BorderRadius.circular(16), + border: Border.all( + color: _isHovered + ? colorScheme.outline.withOpacity(0.3) + : colorScheme.outline.withOpacity(0.1), + width: 1, + ), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + widget.color ?? colorScheme.surface, + (widget.color ?? colorScheme.surface).withOpacity(0.95), + ], + ), + ), + child: Container( + padding: widget.padding ?? const EdgeInsets.all(16), + child: widget.child, + ), + ), + ), + ), + ), + ); + }, + ); + } +} + +/// Expressive ListTile - A modern, animated list tile component +class ExpressiveListTile extends StatelessWidget { + final Widget? leading; + final Widget? title; + final Widget? subtitle; + final Widget? trailing; + final VoidCallback? onTap; + final VoidCallback? onLongPress; + final Color? tileColor; + final EdgeInsetsGeometry? contentPadding; + final bool enableAnimation; + + const ExpressiveListTile({ + super.key, + this.leading, + required this.title, + this.subtitle, + this.trailing, + this.onTap, + this.onLongPress, + this.tileColor, + this.contentPadding, + this.enableAnimation = true, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + + return ExpressiveCard( + onTap: onTap, + onLongPress: onLongPress, + margin: EdgeInsets.zero, + padding: EdgeInsets.zero, + color: tileColor ?? Colors.transparent, + elevation: 0, + borderRadius: BorderRadius.circular(12), + enableAnimation: enableAnimation, + child: ListTile( + leading: leading, + title: title, + subtitle: subtitle, + trailing: trailing, + contentPadding: + contentPadding ?? + const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + tileColor: Colors.transparent, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + ); + } +} + +/// Expressive Chip - A modern, animated chip component +class ExpressiveChip extends StatefulWidget { + final Widget label; + final Widget? avatar; + final Widget? deleteIcon; + final VoidCallback? onTap; + final VoidCallback? onDeleted; + final Color? backgroundColor; + final Color? foregroundColor; + final EdgeInsetsGeometry? padding; + final bool enableAnimation; + + const ExpressiveChip({ + super.key, + required this.label, + this.avatar, + this.deleteIcon, + this.onTap, + this.onDeleted, + this.backgroundColor, + this.foregroundColor, + this.padding, + this.enableAnimation = true, + }); + + @override + State createState() => _ExpressiveChipState(); +} + +class _ExpressiveChipState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _scaleAnimation; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 200), + vsync: this, + ); + + _scaleAnimation = Tween(begin: 0.8, end: 1.0).animate( + CurvedAnimation(parent: _animationController, curve: Curves.elasticOut), + ); + + if (widget.enableAnimation) { + _animationController.forward(); + } + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + + 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!, + ), + ], + ], + ), + ), + ), + ); + } +} diff --git a/lib/components/expressive_refresh_indicator.dart b/lib/components/expressive_refresh_indicator.dart new file mode 100644 index 00000000..29305bd3 --- /dev/null +++ b/lib/components/expressive_refresh_indicator.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; + +/// Material Design Expressive Refresh Indicator with smooth animations +class ExpressiveRefreshIndicator extends StatefulWidget { + final Widget child; + final Future 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 createState() => + _ExpressiveRefreshIndicatorState(); +} + +class _ExpressiveRefreshIndicatorState extends State + with TickerProviderStateMixin { + late AnimationController _animationController; + late Animation _rotationAnimation; + late Animation _scaleAnimation; + late Animation _opacityAnimation; + late bool _isRefreshing = false; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: widget.duration, + vsync: this, + ); + + _rotationAnimation = Tween(begin: 0.0, end: 0.5).animate( + CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), + ); + + _scaleAnimation = Tween(begin: 1.0, end: 0.8).animate( + CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), + ); + + _opacityAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), + ); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + Future _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, + ); + } +} diff --git a/lib/components/expressive_surfaces.dart b/lib/components/expressive_surfaces.dart new file mode 100644 index 00000000..df4b874b --- /dev/null +++ b/lib/components/expressive_surfaces.dart @@ -0,0 +1,348 @@ +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, + ), + ); + } +} + +/// Expressive Container - Enhanced container with modern styling +class ExpressiveContainer extends StatefulWidget { + final Widget child; + final VoidCallback? onTap; + final Color? backgroundColor; + final Color? foregroundColor; + final double? width; + final double? height; + final EdgeInsetsGeometry? margin; + final EdgeInsetsGeometry? padding; + final BorderRadius? borderRadius; + final BoxBorder? border; + final List? boxShadow; + final Gradient? gradient; + final bool enableAnimation; + final Duration animationDuration; + + const ExpressiveContainer({ + super.key, + required this.child, + this.onTap, + this.backgroundColor, + this.foregroundColor, + this.width, + this.height, + this.margin, + this.padding, + this.borderRadius, + this.border, + this.boxShadow, + this.gradient, + this.enableAnimation = true, + this.animationDuration = const Duration(milliseconds: 200), + }); + + @override + State createState() => _ExpressiveContainerState(); +} + +class _ExpressiveContainerState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _scaleAnimation; + late Animation _opacityAnimation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: widget.animationDuration, + vsync: this, + ); + + _scaleAnimation = Tween( + begin: 1.0, + end: 0.95, + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); + + _opacityAnimation = Tween( + begin: 1.0, + end: 0.8, + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); + } + + @override + void didUpdateWidget(ExpressiveContainer oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.animationDuration != widget.animationDuration) { + _controller.duration = widget.animationDuration; + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _handleTapDown(TapDownDetails details) { + if (widget.enableAnimation) { + _controller.forward(); + } + } + + void _handleTapUp(TapUpDetails details) { + if (widget.enableAnimation) { + _controller.reverse(); + } + widget.onTap?.call(); + } + + void _handleTapCancel() { + if (widget.enableAnimation) { + _controller.reverse(); + } + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + + return AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return GestureDetector( + onTapDown: _handleTapDown, + onTapUp: _handleTapUp, + onTapCancel: _handleTapCancel, + child: AnimatedContainer( + duration: widget.animationDuration, + width: widget.width, + height: widget.height, + margin: widget.margin, + padding: widget.padding, + transform: Matrix4.identity()..scale(_scaleAnimation.value), + decoration: BoxDecoration( + color: widget.backgroundColor ?? colorScheme.surface, + borderRadius: widget.borderRadius ?? BorderRadius.circular(12), + border: widget.border, + gradient: + widget.gradient ?? + LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + (widget.backgroundColor ?? colorScheme.surface), + (widget.backgroundColor ?? colorScheme.surface) + .withOpacity(0.8), + ], + ), + boxShadow: + widget.boxShadow ?? + [ + BoxShadow( + color: colorScheme.shadow.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Opacity( + opacity: _opacityAnimation.value, + child: widget.foregroundColor != null + ? DefaultTextStyle( + style: TextStyle(color: widget.foregroundColor), + child: widget.child, + ) + : IconTheme( + data: IconThemeData(color: widget.foregroundColor), + child: widget.child, + ), + ), + ), + ); + }, + ); + } +} + +/// Expressive Badge - Modern badge component with animations +class ExpressiveBadge extends StatefulWidget { + final Widget child; + final String? label; + final Color? color; + final Color? textColor; + final bool showLabel; + final EdgeInsetsGeometry? padding; + final bool enableAnimation; + + const ExpressiveBadge({ + super.key, + required this.child, + this.label, + this.color, + this.textColor, + this.showLabel = true, + this.padding, + this.enableAnimation = true, + }); + + @override + State createState() => _ExpressiveBadgeState(); +} + +class _ExpressiveBadgeState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _scaleAnimation; + late Animation _rotationAnimation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 600), + vsync: this, + ); + + _scaleAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation(parent: _controller, curve: Curves.elasticOut)); + + _rotationAnimation = Tween( + begin: -0.1, + end: 0.0, + ).animate(CurvedAnimation(parent: _controller, curve: Curves.elasticOut)); + + if (widget.enableAnimation) { + _controller.forward(); + } + } + + @override + void didUpdateWidget(ExpressiveBadge oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.color != widget.color || + oldWidget.textColor != widget.textColor) { + setState(() {}); + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + final badgeColor = widget.color ?? colorScheme.primary; + final badgeTextColor = widget.textColor ?? colorScheme.onPrimary; + + return AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return Transform.scale( + scale: _scaleAnimation.value, + child: Transform.rotate( + angle: _rotationAnimation.value, + child: Container( + padding: + widget.padding ?? + const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: badgeColor, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: badgeColor.withOpacity(0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + widget.child, + if (widget.showLabel && widget.label != null) ...[ + const SizedBox(width: 4), + Text( + widget.label!, + style: TextStyle( + color: badgeTextColor, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ], + ], + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/pages/add_app.dart b/lib/pages/add_app.dart index 2f424782..d41fff67 100644 --- a/lib/pages/add_app.dart +++ b/lib/pages/add_app.dart @@ -1,7 +1,7 @@ -import 'package:animations/animations.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:updatium/components/custom_app_bar.dart'; import 'package:updatium/components/generated_form.dart'; import 'package:updatium/components/generated_form_modal.dart'; import 'package:updatium/custom_errors.dart'; @@ -719,23 +719,7 @@ class AddAppPageState extends State { body: CustomScrollView( shrinkWrap: true, slivers: [ - SliverAppBar( - pinned: true, - automaticallyImplyLeading: false, - expandedHeight: MediaQuery.of(context).size.height * 0.15, - flexibleSpace: FlexibleSpaceBar( - titlePadding: const EdgeInsets.symmetric( - horizontal: 24, - vertical: 20, - ), - title: Text( - tr('addApp'), - style: TextStyle( - color: Theme.of(context).textTheme.bodyMedium!.color, - ), - ), - ), - ), + CustomAppBar(title: tr('addApp')), SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(16), @@ -766,28 +750,6 @@ class AddAppPageState extends State { ), ], ), - floatingActionButton: FloatingActionButton.extended( - onPressed: () { - Navigator.push( - context, - PageRouteBuilder( - pageBuilder: (context, animation, secondaryAnimation) => - const ImportExportPage(), - transitionsBuilder: - (context, animation, secondaryAnimation, child) { - return SharedAxisTransition( - animation: animation, - secondaryAnimation: secondaryAnimation, - transitionType: SharedAxisTransitionType.vertical, - child: child, - ); - }, - ), - ); - }, - icon: const Icon(Icons.import_export), - label: Text(tr('importExport')), - ), ); } } diff --git a/lib/pages/apps.dart b/lib/pages/apps.dart index 8e9669dc..967ee6fa 100644 --- a/lib/pages/apps.dart +++ b/lib/pages/apps.dart @@ -3,10 +3,13 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:updatium/components/expressive_refresh_indicator.dart'; import 'package:flutter/services.dart'; import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; import 'package:updatium/components/cached_app_icon.dart'; import 'package:updatium/components/app_button.dart'; +import 'package:updatium/components/custom_app_bar.dart'; +import 'package:updatium/components/expressive_components.dart'; import 'package:updatium/components/generated_form.dart'; import 'package:updatium/components/generated_form_modal.dart'; import 'package:updatium/custom_errors.dart'; @@ -66,7 +69,10 @@ void showChangeLogDialog( appSource.changeLogIfAnyIsMarkDown ? ConstrainedBox( constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * 0.5, + maxHeight: math.max( + 200, + MediaQuery.of(context).size.height - 400, + ), ), child: SizedBox( width: MediaQuery.of(context).size.width * 0.9, @@ -419,7 +425,7 @@ class AppsPageState extends State { children: [ Icon( Icons.widgets, - size: MediaQuery.of(context).size.width * 0.2, + size: 80, color: Theme.of( context, ).colorScheme.primary.withOpacity(0.6), @@ -517,7 +523,7 @@ class AppsPageState extends State { getAppIcon(int appIndex) { return CachedAppIconSimple( app: listedApps[appIndex].app, - size: MediaQuery.of(context).size.width * 0.1, + size: 48.0, onTap: () { // Handle tap if needed }, @@ -604,19 +610,15 @@ class AppsPageState extends State { action = Row( mainAxisSize: MainAxisSize.min, children: [ - Icon( - Icons.check_circle, - color: Colors.green[600], - size: MediaQuery.of(context).size.width * 0.05, - ), - SizedBox(width: MediaQuery.of(context).size.width * 0.015), + Icon(Icons.check_circle, color: Colors.green[600], size: 20), + const SizedBox(width: 6), Text(tr('updated'), style: TextStyle(color: Colors.green[600])), ], ); } return SizedBox( - width: MediaQuery.of(context).size.width * 0.25, + width: 120, height: double.infinity, child: Center(child: action), ); @@ -653,95 +655,72 @@ class AppsPageState extends State { ], ), ), - child: InkWell( - borderRadius: BorderRadius.circular(16), + child: ListTile( + tileColor: listedApps[index].app.pinned + ? Colors.grey.withOpacity(0.1) + : Colors.transparent, + selectedTileColor: Theme.of(context).colorScheme.primary.withOpacity( + listedApps[index].app.pinned ? 0.2 : 0.1, + ), + selected: selectedAppIds + .map((e) => e) + .contains(listedApps[index].app.id), + onLongPress: () { + toggleAppSelected(listedApps[index].app); + }, + leading: getAppIcon(index), + title: Text( + listedApps[index].name, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: listedApps[index].app.pinned + ? FontWeight.bold + : FontWeight.normal, + ), + ), + subtitle: Text( + tr('byX', args: [listedApps[index].author]), + maxLines: 1, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontWeight: listedApps[index].app.pinned + ? FontWeight.bold + : FontWeight.normal, + ), + ), + trailing: listedApps[index].downloadProgress != null + ? SizedBox( + child: Text( + listedApps[index].downloadProgress! >= 0 + ? tr( + 'percentProgress', + args: [ + listedApps[index].downloadProgress! + .toInt() + .toString(), + ], + ) + : tr('installing'), + textAlign: (listedApps[index].downloadProgress! >= 0) + ? TextAlign.start + : TextAlign.end, + ), + ) + : trailingRow, onTap: () { if (selectedAppIds.isNotEmpty) { toggleAppSelected(listedApps[index].app); } else { Navigator.push( context, - PageRouteBuilder( - pageBuilder: (context, animation, secondaryAnimation) => + MaterialPageRoute( + builder: (context) => AppPage(appId: listedApps[index].app.id), - transitionsBuilder: - (context, animation, secondaryAnimation, child) { - return SharedAxisTransition( - animation: animation, - secondaryAnimation: secondaryAnimation, - transitionType: SharedAxisTransitionType.horizontal, - child: child, - ); - }, ), ); } }, - onLongPress: () { - if (selectedAppIds.isEmpty) { - toggleAppSelected(listedApps[index].app); - } else { - toggleAppSelected(listedApps[index].app); - } - }, - child: ListTile( - contentPadding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 8, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - tileColor: Theme.of(context).colorScheme.surface, - selectedTileColor: Theme.of( - context, - ).colorScheme.surfaceContainerHighest, - leading: SizedBox( - height: MediaQuery.of(context).size.width * 0.1, - width: MediaQuery.of(context).size.width * 0.1, - child: getAppIcon(index), - ), - title: Text( - listedApps[index].name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: listedApps[index].app.pinned - ? FontWeight.w600 - : FontWeight.w500, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - subtitle: Text( - tr('byX', args: [listedApps[index].author]), - maxLines: 1, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - fontWeight: listedApps[index].app.pinned - ? FontWeight.w500 - : FontWeight.w400, - ), - ), - trailing: listedApps[index].downloadProgress != null - ? SizedBox( - child: Text( - listedApps[index].downloadProgress! >= 0 - ? tr( - 'percentProgress', - args: [ - listedApps[index].downloadProgress! - .toInt() - .toString(), - ], - ) - : tr('installing'), - textAlign: (listedApps[index].downloadProgress! >= 0) - ? TextAlign.start - : TextAlign.end, - ), - ) - : trailingRow, - ), ), ); } @@ -779,140 +758,123 @@ class AppsPageState extends State { final categories = listedApps[index].app.categories; final stops = categoryStops(categories); - return Card.outlined( - margin: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), - child: InkWell( - onTap: () { - if (selectedAppIds.isNotEmpty) { - toggleAppSelected(listedApps[index].app); - } else { - Navigator.push( - context, - PageRouteBuilder( - pageBuilder: (context, animation, secondaryAnimation) => - AppPage(appId: listedApps[index].app.id), - transitionsBuilder: - (context, animation, secondaryAnimation, child) { - return SharedAxisTransition( - animation: animation, - secondaryAnimation: secondaryAnimation, - transitionType: SharedAxisTransitionType.horizontal, - child: child, - ); - }, - ), - ); - } - }, - onLongPress: () { + return ExpressiveCard( + onTap: () { + if (selectedAppIds.isNotEmpty) { toggleAppSelected(listedApps[index].app); - }, - borderRadius: BorderRadius.circular(20), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - gradient: LinearGradient( - stops: stops, - begin: const Alignment(-1, -1), - end: const Alignment(1, 1), - colors: [ - ...listedApps[index].app.categories.map( - (e) => Color( - settingsProvider.categories[e] ?? transparent, - ).withAlpha(40), - ), - Color(transparent), - ], + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => AppPage(appId: listedApps[index].app.id), ), + ); + } + }, + onLongPress: () { + toggleAppSelected(listedApps[index].app); + }, + margin: EdgeInsets.zero, + padding: EdgeInsets.zero, + borderRadius: BorderRadius.circular(12), + elevation: 2, + enableAnimation: true, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + gradient: LinearGradient( + stops: stops, + begin: const Alignment(-1, -1), + end: const Alignment(1, 1), + colors: [ + ...listedApps[index].app.categories.map( + (e) => Color( + settingsProvider.categories[e] ?? transparent, + ).withAlpha(40), + ), + Color(transparent), + ], ), - child: Stack( - alignment: Alignment.center, - children: [ - if (selectedAppIds.contains(listedApps[index].app.id)) - Positioned.fill( - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: Theme.of(context).colorScheme.primaryContainer, - ), - child: Icon( - Icons.check_circle, - color: Theme.of(context).colorScheme.onPrimaryContainer, - size: MediaQuery.of(context).size.width * 0.08, - ), + ), + child: Stack( + alignment: Alignment.center, + children: [ + if (selectedAppIds.contains(listedApps[index].app.id)) + Positioned.fill( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Theme.of( + context, + ).colorScheme.primary.withOpacity(0.2), ), ), - if (listedApps[index].app.pinned) - Align( - alignment: Alignment.topLeft, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Icon( - Icons.push_pin, - size: MediaQuery.of(context).size.width * 0.04, - color: Theme.of(context).colorScheme.primary, - ), + ), + if (listedApps[index].app.pinned) + Align( + alignment: Alignment.topLeft, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + Icons.push_pin, + size: 16, + color: Theme.of(context).colorScheme.primary, ), ), - if (hasUpdate) - Align( - alignment: Alignment.topRight, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Icon( - Icons.circle, - size: MediaQuery.of(context).size.width * 0.025, - color: Theme.of(context).colorScheme.primary, - ), + ), + if (hasUpdate) + Align( + alignment: Alignment.topRight, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + Icons.circle, + size: 10, + color: Theme.of(context).colorScheme.primary, ), ), - Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - mainAxisSize: MainAxisSize.max, - children: [ - const SizedBox(height: 16), - SizedBox( - height: MediaQuery.of(context).size.width * 0.15, - width: MediaQuery.of(context).size.width * 0.15, - child: FittedBox( - fit: BoxFit.contain, - child: getAppIcon(index), - ), + ), + Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.max, + children: [ + const SizedBox(height: 16), + SizedBox( + height: 64, + width: 64, + child: FittedBox( + fit: BoxFit.contain, + child: getAppIcon(index), ), - const SizedBox(height: 12), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Flexible( - child: Text( - listedApps[index].name, - textAlign: TextAlign.center, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.titleSmall - ?.copyWith( - fontWeight: FontWeight.w600, - color: Theme.of(context).colorScheme.onSurface, - ), + ), + const SizedBox(height: 12), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Flexible( + child: Text( + listedApps[index].name, + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, ), ), ), - const SizedBox(height: 8), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: Flexible( - child: Text( - listedApps[index].author, - textAlign: TextAlign.center, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodySmall - ?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - fontSize: 11, - ), - ), + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Flexible( + child: Text( + listedApps[index].author, + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith(fontSize: 11), ), ), const SizedBox(height: 12), @@ -988,8 +950,36 @@ class AppsPageState extends State { ), ], ); - }, - ), + } + + return Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.check_circle, + color: Colors.green[600], + size: 18, + ), + const SizedBox(width: 6), + Text( + tr('updated'), + style: TextStyle(color: Colors.green[600]), + ), + ], + ); + }, + ), + ), + const SizedBox(height: 16), + ], + ), + if (listedApps[index].downloadProgress != null) + Positioned.fill( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Colors.black45, ), const SizedBox(height: 16), ], @@ -1009,13 +999,9 @@ class AppsPageState extends State { ), ), ), - ), - ], - ), - ), ), - ); - } + ), + ); getCategoryCollapsibleTile(int index) { var tiles = listedApps @@ -1050,14 +1036,13 @@ class AppsPageState extends State { padding: EdgeInsets.zero, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), - gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: - MediaQuery.of(context).size.width * 0.5, - childAspectRatio: 0.6, - crossAxisSpacing: - MediaQuery.of(context).size.width * 0.04, - mainAxisSpacing: MediaQuery.of(context).size.width * 0.04, - ), + gridDelegate: + const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 200, + childAspectRatio: 0.6, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + ), itemCount: listedApps .asMap() .entries @@ -1314,8 +1299,9 @@ class AppsPageState extends State { ), content: Text( tr('onlyWorksWithNonVersionDetectApps'), - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontStyle: FontStyle.italic, ), ), actions: [ @@ -1627,7 +1613,7 @@ class AppsPageState extends State { isFilterOff ? Icons.search_rounded : Icons.search_off_rounded, ), ), - SizedBox(width: MediaQuery.of(context).size.width * 0.04), + const SizedBox(width: 16), const VerticalDivider(), Expanded( child: Row( @@ -1685,9 +1671,8 @@ class AppsPageState extends State { return Scaffold( backgroundColor: Theme.of(context).colorScheme.surface, - body: RefreshIndicator( + body: ExpressiveRefreshIndicator( onRefresh: refresh, - displacement: MediaQuery.of(context).size.height * 0.1, child: Scrollbar( interactive: true, controller: scrollController, @@ -1695,23 +1680,7 @@ class AppsPageState extends State { physics: const AlwaysScrollableScrollPhysics(), controller: scrollController, slivers: [ - SliverAppBar( - pinned: true, - automaticallyImplyLeading: false, - expandedHeight: MediaQuery.of(context).size.height * 0.15, - flexibleSpace: FlexibleSpaceBar( - titlePadding: const EdgeInsets.symmetric( - horizontal: 24, - vertical: 20, - ), - title: Text( - tr('appsString'), - style: TextStyle( - color: Theme.of(context).textTheme.bodyMedium!.color, - ), - ), - ), - ), + CustomAppBar(title: tr('appsString')), ...getLoadingWidgets(), getDisplayedList(), ], diff --git a/lib/pages/home.dart b/lib/pages/home.dart index 96996986..ebef188d 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -5,6 +5,7 @@ import 'package:app_links/app_links.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:updatium/components/animated_navigation_bar.dart'; import 'package:updatium/components/app_button.dart'; import 'package:updatium/components/generated_form_modal.dart'; import 'package:updatium/custom_errors.dart'; @@ -53,6 +54,11 @@ class _HomePageState extends State { Icons.add_circle, AddAppPage(key: GlobalKey()), ), + NavigationPageItem( + tr('importExport'), + Icons.import_export, + const ImportExportPage(), + ), NavigationPageItem(tr('settings'), Icons.settings, const SettingsPage()), ]; @@ -328,7 +334,7 @@ class _HomePageState extends State { ) .widget, ), - bottomNavigationBar: NavigationBar( + bottomNavigationBar: AnimatedNavigationBar( destinations: pages .map( (e) => diff --git a/lib/pages/import_export.dart b/lib/pages/import_export.dart index 8e1d578a..723ecdd1 100644 --- a/lib/pages/import_export.dart +++ b/lib/pages/import_export.dart @@ -5,7 +5,9 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:updatium/app_sources/fdroidrepo.dart'; +import 'package:updatium/components/expressive_buttons.dart'; import 'package:updatium/components/app_button.dart'; +import 'package:updatium/components/custom_app_bar.dart'; import 'package:updatium/components/generated_form.dart'; import 'package:updatium/components/generated_form_modal.dart'; import 'package:updatium/custom_errors.dart'; @@ -368,22 +370,7 @@ class _ImportExportPageState extends State { backgroundColor: Theme.of(context).colorScheme.surface, body: CustomScrollView( slivers: [ - SliverAppBar( - pinned: true, - expandedHeight: MediaQuery.of(context).size.height * 0.15, - flexibleSpace: FlexibleSpaceBar( - titlePadding: const EdgeInsets.symmetric( - horizontal: 24, - vertical: 20, - ), - title: Text( - tr('importExport'), - style: TextStyle( - color: Theme.of(context).textTheme.bodyMedium!.color, - ), - ), - ), - ), + CustomAppBar(title: tr('importExport')), SliverFillRemaining( child: Padding( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), @@ -398,13 +385,12 @@ class _ImportExportPageState extends State { Row( children: [ Expanded( - child: FilledButton.icon( + child: ExpressiveFilledButton( onPressed: importInProgress ? null : () { runUpdatiumExport(pickOnly: true); }, - icon: const Icon(Icons.folder_open), child: Text( tr('pickExportDir'), textAlign: TextAlign.center, @@ -413,12 +399,11 @@ class _ImportExportPageState extends State { ), const SizedBox(width: 16), Expanded( - child: FilledButton.icon( + child: ExpressiveFilledButton( onPressed: importInProgress || snapshot.data == null ? null : runUpdatiumExport, - icon: const Icon(Icons.upload_file), child: Text( tr('updatiumExport'), textAlign: TextAlign.center, @@ -431,11 +416,10 @@ class _ImportExportPageState extends State { Row( children: [ Expanded( - child: FilledButton.icon( + child: ExpressiveFilledButton( onPressed: importInProgress ? null : runUpdatiumImport, - icon: const Icon(Icons.download), child: Text( tr('updatiumImport'), textAlign: TextAlign.center, @@ -509,8 +493,7 @@ class _ImportExportPageState extends State { Row( children: [ Expanded( - child: FilledButton.icon( - icon: const Icon(Icons.search), + child: ExpressiveFilledButton( onPressed: importInProgress ? null : () async { @@ -547,7 +530,7 @@ class _ImportExportPageState extends State { runSourceSearch(searchSource[0]); } }, - child: Text( + label: Text( tr( 'searchX', args: [lowerCaseIfEnglish(tr('source'))], @@ -558,15 +541,13 @@ class _ImportExportPageState extends State { ], ), const SizedBox(height: 8), - FilledButton.icon( + ExpressiveFilledButton( onPressed: importInProgress ? null : urlListImport, - icon: const Icon(Icons.list_alt), child: Text(tr('importFromURLList')), ), const SizedBox(height: 8), - FilledButton.icon( + ExpressiveFilledButton( onPressed: importInProgress ? null : runUrlImport, - icon: const Icon(Icons.link), child: Text(tr('importFromURLsInFile')), ), ], @@ -576,13 +557,12 @@ class _ImportExportPageState extends State { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: 8), - FilledButton.icon( + ExpressiveFilledButton( onPressed: importInProgress ? null : () { runMassSourceImport(source); }, - icon: const Icon(Icons.cloud_download), child: Text(tr('importX', args: [source.name])), ), ], @@ -593,9 +573,9 @@ class _ImportExportPageState extends State { Text( tr('importedAppsIdDisclaimer'), textAlign: TextAlign.start, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, + style: const TextStyle( fontStyle: FontStyle.italic, + fontSize: 12, ), ), const SizedBox(height: 8), @@ -645,35 +625,25 @@ class _ImportErrorDialogState extends State { const SizedBox(height: 16), Text( tr('followingURLsHadErrors'), - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: Theme.of(context).colorScheme.onSurface, - fontWeight: FontWeight.w500, - ), + style: Theme.of(context).textTheme.bodyLarge, ), ...widget.errors.map((e) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: 16), - Text(e[0], style: Theme.of(context).textTheme.titleSmall), - Text( - e[1], - style: Theme.of(context).textTheme.bodySmall?.copyWith( - fontStyle: FontStyle.italic, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), + Text(e[0]), + Text(e[1], style: const TextStyle(fontStyle: FontStyle.italic)), ], ); }), ], ), actions: [ - TextButton.icon( + ExpressiveButton( onPressed: () { Navigator.of(context).pop(null); }, - icon: const Icon(Icons.close), child: Text(tr('ok')), ), ], @@ -763,20 +733,20 @@ class _SelectionModalState extends State { } var noneSelected = entrySelections.values.where((v) => v == true).isEmpty; return noneSelected - ? TextButton( + ? ExpressiveButton( style: const ButtonStyle(visualDensity: VisualDensity.compact), onPressed: () { setState(() { - entrySelections.updateAll((key, value) => true); + selectAll(); }); }, child: Text(tr('selectAll')), ) - : TextButton( + : ExpressiveButton( style: const ButtonStyle(visualDensity: VisualDensity.compact), onPressed: () { setState(() { - entrySelections.updateAll((key, value) => false); + selectAll(deselect: true); }); }, child: Text(tr('deselectX', args: [tr('all')])), @@ -957,7 +927,7 @@ class _SelectionModalState extends State { onPressed: () { Navigator.of(context).pop(); }, - child: Text(tr('cancel')), + label: Text(tr('cancel')), ), AppTextButton( onPressed: entrySelections.values.where((b) => b).isEmpty @@ -970,7 +940,7 @@ class _SelectionModalState extends State { .toList(), ); }, - child: Text( + label: Text( widget.onlyOneSelectionAllowed ? tr('pick') : tr( diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index 43bf4781..7dc4b6a8 100644 --- a/lib/pages/settings.dart +++ b/lib/pages/settings.dart @@ -4,6 +4,7 @@ import 'package:equations/equations.dart'; import 'package:flex_color_picker/flex_color_picker.dart'; import 'package:flutter/material.dart'; import 'package:updatium/components/app_button.dart'; +import 'package:updatium/components/custom_app_bar.dart'; import 'package:updatium/components/generated_form.dart'; import 'package:updatium/components/generated_form_modal.dart'; import 'package:updatium/custom_errors.dart'; @@ -141,11 +142,11 @@ class _SettingsPageState extends State { tr('selectX', args: [tr('color').toLowerCase()]), style: Theme.of(context).textTheme.titleLarge, ), - wheelDiameter: MediaQuery.of(context).size.width * 0.45, - wheelSquareBorderRadius: MediaQuery.of(context).size.width * 0.075, - width: MediaQuery.of(context).size.width * 0.11, - height: MediaQuery.of(context).size.width * 0.11, - borderRadius: MediaQuery.of(context).size.width * 0.055, + wheelDiameter: 192, + wheelSquareBorderRadius: 32, + width: 48, + height: 48, + borderRadius: 24, spacing: 8, runSpacing: 8, enableShadesSelection: false, @@ -178,84 +179,44 @@ class _SettingsPageState extends State { } var colorPicker = ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - tileColor: Theme.of(context).colorScheme.surface, - title: Text( - tr('selectX', args: [tr('color').toLowerCase()]), - style: Theme.of(context).textTheme.titleMedium, - ), + dense: true, + contentPadding: EdgeInsets.zero, + title: Text(tr('selectX', args: [tr('color').toLowerCase()])), subtitle: Text( "${ColorTools.nameThatColor(settingsProvider.themeColor)} " "(${ColorTools.materialNameAndCode(settingsProvider.themeColor, colorSwatchNameMap: colorsNameMap)})", - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), ), - trailing: Container( - width: MediaQuery.of(context).size.width * 0.1, - height: MediaQuery.of(context).size.width * 0.1, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular( - MediaQuery.of(context).size.width * 0.05, - ), - color: settingsProvider.themeColor, - border: Border.all( - color: Theme.of(context).colorScheme.outline, - width: 1, - ), - ), - child: Material( - color: Colors.transparent, - child: InkWell( - borderRadius: BorderRadius.circular( - MediaQuery.of(context).size.width * 0.05, - ), - onTap: () async { - final Color colorBeforeDialog = settingsProvider.themeColor; - if (!(await colorPickerDialog())) { - setState(() { - settingsProvider.themeColor = colorBeforeDialog; - }); - } - }, - child: Icon( - Icons.palette, - color: settingsProvider.themeColor.computeLuminance() > 0.5 - ? Colors.black - : Colors.white, - size: MediaQuery.of(context).size.width * 0.05, - ), - ), - ), + trailing: ColorIndicator( + width: 40, + height: 40, + borderRadius: 20, + color: settingsProvider.themeColor, + onSelectFocus: false, + onSelect: () async { + final Color colorBeforeDialog = settingsProvider.themeColor; + if (!(await colorPickerDialog())) { + setState(() { + settingsProvider.themeColor = colorBeforeDialog; + }); + } + }, ), ); var useMaterialThemeSwitch = FutureBuilder( builder: (ctx, val) { return ((val.data?.version.sdkInt ?? 0) >= 31) - ? Card.outlined( - margin: const EdgeInsets.all(16), - child: Padding( - padding: const EdgeInsets.all(20), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Text( - tr('useMaterialYou'), - style: Theme.of(context).textTheme.titleMedium, - ), - ), - Switch( - value: settingsProvider.useMaterialYou, - onChanged: (value) { - settingsProvider.useMaterialYou = value; - }, - ), - ], + ? Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible(child: Text(tr('useMaterialYou'))), + Switch( + value: settingsProvider.useMaterialYou, + onChanged: (value) { + settingsProvider.useMaterialYou = value; + }, ), - ), + ], ) : const SizedBox.shrink(); }, @@ -396,23 +357,7 @@ class _SettingsPageState extends State { backgroundColor: Theme.of(context).colorScheme.surface, body: CustomScrollView( slivers: [ - SliverAppBar( - pinned: true, - automaticallyImplyLeading: false, - expandedHeight: MediaQuery.of(context).size.height * 0.15, - flexibleSpace: FlexibleSpaceBar( - titlePadding: const EdgeInsets.symmetric( - horizontal: 24, - vertical: 20, - ), - title: Text( - tr('settings'), - style: TextStyle( - color: Theme.of(context).textTheme.bodyMedium!.color, - ), - ), - ), - ), + CustomAppBar(title: tr('settings')), SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(16), diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index 34deef93..594bb591 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -2406,33 +2406,9 @@ class AppsProvider with ChangeNotifier { await saveApps([app], onlyIfExists: false); } } - List> errors = errorsMap.keys.map((e) { - // Log detailed error internally for debugging - print('Import error for $e: ${errorsMap[e]}'); - - // Return user-friendly error message - String userMessage; - final errorDetail = errorsMap[e].toString(); - - // Categorize errors and provide user-friendly messages - if (errorDetail.contains('timeout') || - errorDetail.contains('connection')) { - userMessage = tr('networkError'); - } else if (errorDetail.contains('404') || - errorDetail.contains('not found')) { - userMessage = tr('appNotFound'); - } else if (errorDetail.contains('parse') || - errorDetail.contains('format')) { - userMessage = tr('invalidUrlFormat'); - } else if (errorDetail.contains('permission') || - errorDetail.contains('access')) { - userMessage = tr('accessDenied'); - } else { - userMessage = tr('importFailed'); - } - - return [e, userMessage]; - }).toList(); + List> errors = errorsMap.keys + .map((e) => [e, errorsMap[e].toString()]) + .toList(); return errors; } } diff --git a/lib/utils/expressive_motion.dart b/lib/utils/expressive_motion.dart new file mode 100644 index 00000000..1cd12458 --- /dev/null +++ b/lib/utils/expressive_motion.dart @@ -0,0 +1,289 @@ +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 animation, + double scaleStart = 0.9, + double scaleEnd = 1.0, + }) { + return ScaleTransition( + scale: Tween(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 animation, + Offset slideStart = const Offset(0.0, 0.1), + }) { + return SlideTransition( + position: Tween(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 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 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( + duration: durationShort, + tween: Tween(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( + duration: durationMedium, + tween: Tween(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 expressivePageRoute({ + required WidgetBuilder builder, + Duration transitionDuration = durationMedium, + RouteSettings? settings, + }) { + return PageRouteBuilder( + settings: settings, + transitionDuration: transitionDuration, + pageBuilder: (context, animation, secondaryAnimation) => builder(context), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return SlideTransition( + position: + Tween( + 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( + duration: durationMedium, + tween: Tween(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 extends PageRoute { + 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 animation, + Animation secondaryAnimation, + ) { + return builder(context); + } + + @override + Widget buildTransitions( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) { + return ExpressiveMotion.slideFadeTransition( + animation: animation, + child: child, + ); + } +}