From 7619040e27e4a83bfac3894ac0fe393b8230d5e2 Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Mon, 15 Dec 2025 20:17:56 +0530 Subject: [PATCH] fix(navigation): Adjust bottom padding for safe area in chats drawer --- lib/features/chat/views/chat_page.dart | 54 +-- .../navigation/widgets/chats_drawer.dart | 258 ++++++-------- .../notes/views/note_editor_page.dart | 53 +-- lib/features/notes/views/notes_list_page.dart | 315 ++++-------------- .../profile/views/app_customization_page.dart | 142 +------- lib/features/profile/views/profile_page.dart | 141 +------- lib/shared/widgets/conduit_components.dart | 303 +++++++++++++++++ 7 files changed, 495 insertions(+), 771 deletions(-) diff --git a/lib/features/chat/views/chat_page.dart b/lib/features/chat/views/chat_page.dart index a2dbcdb..f6de623 100644 --- a/lib/features/chat/views/chat_page.dart +++ b/lib/features/chat/views/chat_page.dart @@ -956,57 +956,9 @@ class _ChatPageState extends ConsumerState { required Widget child, bool isCircular = false, }) { - final theme = Theme.of(context); - final isDark = theme.brightness == Brightness.dark; - - // Use same high-contrast colors as the floating chat input - final backgroundColor = isDark - ? Color.lerp(context.conduitTheme.cardBackground, Colors.white, 0.08)! - : Color.lerp(context.conduitTheme.inputBackground, Colors.black, 0.06)!; - - final borderColor = context.conduitTheme.cardBorder.withValues( - alpha: isDark ? 0.65 : 0.55, - ); - - final borderRadius = isCircular - ? BorderRadius.circular(100) - : BorderRadius.circular(AppBorderRadius.pill); - - // For circular buttons, ensure the entire widget is constrained to a square - if (isCircular) { - return SizedBox( - width: 44, - height: 44, - child: ClipRRect( - borderRadius: borderRadius, - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16), - child: Container( - decoration: BoxDecoration( - color: backgroundColor.withValues(alpha: 0.85), - borderRadius: borderRadius, - border: Border.all(color: borderColor, width: BorderWidth.thin), - ), - child: Center(child: child), - ), - ), - ), - ); - } - - return ClipRRect( - borderRadius: borderRadius, - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16), - child: Container( - decoration: BoxDecoration( - color: backgroundColor.withValues(alpha: 0.85), - borderRadius: borderRadius, - border: Border.all(color: borderColor, width: BorderWidth.thin), - ), - child: child, - ), - ), + return FloatingAppBarPill( + isCircular: isCircular, + child: child, ); } diff --git a/lib/features/navigation/widgets/chats_drawer.dart b/lib/features/navigation/widgets/chats_drawer.dart index 2dc16b9..a62e452 100644 --- a/lib/features/navigation/widgets/chats_drawer.dart +++ b/lib/features/navigation/widgets/chats_drawer.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:io' show Platform; -import 'dart:ui' show ImageFilter; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -23,6 +22,7 @@ import '../../../core/utils/user_avatar_utils.dart'; import '../../../shared/utils/conversation_context_menu.dart'; import '../../../shared/widgets/user_avatar.dart'; import '../../../shared/widgets/model_avatar.dart'; +import '../../../shared/widgets/conduit_components.dart'; import '../../../shared/widgets/responsive_drawer_layout.dart'; import '../../../core/models/model.dart'; import '../../../core/models/conversation.dart'; @@ -268,81 +268,60 @@ class _ChatsDrawerState extends ConsumerState { } Widget _buildFloatingSearchField(BuildContext context) { - final theme = Theme.of(context); final conduitTheme = context.conduitTheme; - final isDark = theme.brightness == Brightness.dark; - final backgroundColor = isDark - ? Color.lerp(conduitTheme.cardBackground, Colors.white, 0.08)! - : Color.lerp(conduitTheme.inputBackground, Colors.black, 0.06)!; - - final borderColor = conduitTheme.cardBorder.withValues( - alpha: isDark ? 0.65 : 0.55, - ); - - return ClipRRect( - borderRadius: BorderRadius.circular(AppBorderRadius.pill), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16), - child: Container( - decoration: BoxDecoration( - color: backgroundColor.withValues(alpha: 0.85), - borderRadius: BorderRadius.circular(AppBorderRadius.pill), - border: Border.all(color: borderColor, width: BorderWidth.thin), + return FloatingAppBarPill( + child: Material( + color: Colors.transparent, + child: TextField( + controller: _searchController, + focusNode: _searchFocusNode, + onChanged: (_) => _onSearchChanged(), + style: AppTypography.standard.copyWith( + color: conduitTheme.textPrimary, ), - child: Material( - color: Colors.transparent, - child: TextField( - controller: _searchController, - focusNode: _searchFocusNode, - onChanged: (_) => _onSearchChanged(), - style: AppTypography.standard.copyWith( - color: conduitTheme.textPrimary, - ), - decoration: InputDecoration( - isDense: true, - hintText: AppLocalizations.of(context)!.searchConversations, - hintStyle: AppTypography.standard.copyWith( - color: conduitTheme.textSecondary.withValues(alpha: 0.6), - ), - prefixIcon: Icon( - Platform.isIOS ? CupertinoIcons.search : Icons.search, - color: conduitTheme.iconSecondary, - size: IconSize.input, - ), - prefixIconConstraints: const BoxConstraints( - minWidth: TouchTarget.minimum, - minHeight: TouchTarget.minimum, - ), - suffixIcon: _query.isNotEmpty - ? IconButton( - onPressed: () { - _searchController.clear(); - setState(() => _query = ''); - _searchFocusNode.unfocus(); - }, - icon: Icon( - Platform.isIOS - ? CupertinoIcons.clear_circled_solid - : Icons.clear, - color: conduitTheme.iconSecondary, - size: IconSize.input, - ), - ) - : null, - suffixIconConstraints: const BoxConstraints( - minWidth: TouchTarget.minimum, - minHeight: TouchTarget.minimum, - ), - filled: false, - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - contentPadding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.sm, - ), - ), + decoration: InputDecoration( + isDense: true, + hintText: AppLocalizations.of(context)!.searchConversations, + hintStyle: AppTypography.standard.copyWith( + color: conduitTheme.textSecondary.withValues(alpha: 0.6), + ), + prefixIcon: Icon( + Platform.isIOS ? CupertinoIcons.search : Icons.search, + color: conduitTheme.iconSecondary, + size: IconSize.input, + ), + prefixIconConstraints: const BoxConstraints( + minWidth: TouchTarget.minimum, + minHeight: TouchTarget.minimum, + ), + suffixIcon: _query.isNotEmpty + ? IconButton( + onPressed: () { + _searchController.clear(); + setState(() => _query = ''); + _searchFocusNode.unfocus(); + }, + icon: Icon( + Platform.isIOS + ? CupertinoIcons.clear_circled_solid + : Icons.clear, + color: conduitTheme.iconSecondary, + size: IconSize.input, + ), + ) + : null, + suffixIconConstraints: const BoxConstraints( + minWidth: TouchTarget.minimum, + minHeight: TouchTarget.minimum, + ), + filled: false, + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + horizontal: Spacing.md, + vertical: Spacing.sm, ), ), ), @@ -1700,9 +1679,7 @@ class _ChatsDrawerState extends ConsumerState { } Widget _buildFloatingBottomSection(BuildContext context) { - final theme = Theme.of(context); final conduitTheme = context.conduitTheme; - final isDark = theme.brightness == Brightness.dark; final authUser = ref.watch(currentUserProvider2); final asyncUser = ref.watch(currentUserProvider); final user = asyncUser.maybeWhen( @@ -1722,98 +1699,81 @@ class _ChatsDrawerState extends ConsumerState { final initial = initialFor(displayName); final avatarUrl = resolveUserAvatarUrlForUser(api, user); - final backgroundColor = isDark - ? Color.lerp(conduitTheme.cardBackground, Colors.white, 0.08)! - : Color.lerp(conduitTheme.inputBackground, Colors.black, 0.06)!; - - final borderColor = conduitTheme.cardBorder.withValues( - alpha: isDark ? 0.65 : 0.55, - ); - if (user == null) return const SizedBox.shrink(); - return ClipRRect( - borderRadius: BorderRadius.circular(AppBorderRadius.pill), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16), - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.sm, - vertical: Spacing.xs, - ), - decoration: BoxDecoration( - color: backgroundColor.withValues(alpha: 0.85), - borderRadius: BorderRadius.circular(AppBorderRadius.pill), - border: Border.all(color: borderColor, width: BorderWidth.thin), - ), - child: Row( - children: [ - Container( - width: 36, - height: 36, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular( - AppBorderRadius.avatar, - ), - border: Border.all( - color: conduitTheme.buttonPrimary.withValues(alpha: 0.25), - width: BorderWidth.thin, - ), + return FloatingAppBarPill( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: Spacing.sm, + vertical: Spacing.xs, + ), + child: Row( + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + AppBorderRadius.avatar, ), - clipBehavior: Clip.hardEdge, - child: UserAvatar( - size: 36, - imageUrl: avatarUrl, - fallbackText: initial, + border: Border.all( + color: conduitTheme.buttonPrimary.withValues(alpha: 0.25), + width: BorderWidth.thin, ), ), - const SizedBox(width: Spacing.sm), - Expanded( - child: Text( - displayName, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: AppTypography.bodySmallStyle.copyWith( - color: conduitTheme.textPrimary, - fontWeight: FontWeight.w600, - decoration: TextDecoration.none, - ), + clipBehavior: Clip.hardEdge, + child: UserAvatar( + size: 36, + imageUrl: avatarUrl, + fallbackText: initial, + ), + ), + const SizedBox(width: Spacing.sm), + Expanded( + child: Text( + displayName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppTypography.bodySmallStyle.copyWith( + color: conduitTheme.textPrimary, + fontWeight: FontWeight.w600, + decoration: TextDecoration.none, ), ), - // Notes icon (hidden when feature is disabled) - if (notesEnabled) - IconButton( - tooltip: AppLocalizations.of(context)!.notes, - onPressed: () { - Navigator.of(context).maybePop(); - context.pushNamed(RouteNames.notes); - }, - visualDensity: VisualDensity.compact, - icon: Icon( - Platform.isIOS - ? CupertinoIcons.doc_text - : Icons.note_alt_outlined, - color: conduitTheme.iconPrimary, - size: IconSize.medium, - ), - ), + ), + // Notes icon (hidden when feature is disabled) + if (notesEnabled) IconButton( - tooltip: AppLocalizations.of(context)!.manage, + tooltip: AppLocalizations.of(context)!.notes, onPressed: () { Navigator.of(context).maybePop(); - context.pushNamed(RouteNames.profile); + context.pushNamed(RouteNames.notes); }, visualDensity: VisualDensity.compact, icon: Icon( Platform.isIOS - ? CupertinoIcons.settings - : Icons.settings_rounded, + ? CupertinoIcons.doc_text + : Icons.note_alt_outlined, color: conduitTheme.iconPrimary, size: IconSize.medium, ), ), - ], - ), + IconButton( + tooltip: AppLocalizations.of(context)!.manage, + onPressed: () { + Navigator.of(context).maybePop(); + context.pushNamed(RouteNames.profile); + }, + visualDensity: VisualDensity.compact, + icon: Icon( + Platform.isIOS + ? CupertinoIcons.settings + : Icons.settings_rounded, + color: conduitTheme.iconPrimary, + size: IconSize.medium, + ), + ), + ], ), ), ); diff --git a/lib/features/notes/views/note_editor_page.dart b/lib/features/notes/views/note_editor_page.dart index c3bbaeb..ce847c0 100644 --- a/lib/features/notes/views/note_editor_page.dart +++ b/lib/features/notes/views/note_editor_page.dart @@ -15,6 +15,7 @@ import '../../../core/providers/app_providers.dart'; import '../../../core/widgets/error_boundary.dart'; import '../../../shared/theme/theme_extensions.dart'; import '../../../shared/utils/ui_utils.dart'; +import '../../../shared/widgets/conduit_components.dart'; import '../../../shared/widgets/improved_loading_states.dart'; import '../../../shared/widgets/middle_ellipsis_text.dart'; import '../../../shared/widgets/themed_dialogs.dart'; @@ -815,55 +816,9 @@ class _NoteEditorPageState extends ConsumerState { Widget child, { bool isCircular = false, }) { - final theme = Theme.of(context); - final isDark = theme.brightness == Brightness.dark; - - final backgroundColor = isDark - ? Color.lerp(context.conduitTheme.cardBackground, Colors.white, 0.08)! - : Color.lerp(context.conduitTheme.inputBackground, Colors.black, 0.06)!; - - final borderColor = context.conduitTheme.cardBorder.withValues( - alpha: isDark ? 0.65 : 0.55, - ); - - final borderRadius = isCircular - ? BorderRadius.circular(100) - : BorderRadius.circular(AppBorderRadius.pill); - - if (isCircular) { - return SizedBox( - width: 44, - height: 44, - child: ClipRRect( - borderRadius: borderRadius, - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16), - child: Container( - decoration: BoxDecoration( - color: backgroundColor.withValues(alpha: 0.85), - borderRadius: borderRadius, - border: Border.all(color: borderColor, width: BorderWidth.thin), - ), - child: Center(child: child), - ), - ), - ), - ); - } - - return ClipRRect( - borderRadius: borderRadius, - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16), - child: Container( - decoration: BoxDecoration( - color: backgroundColor.withValues(alpha: 0.85), - borderRadius: borderRadius, - border: Border.all(color: borderColor, width: BorderWidth.thin), - ), - child: child, - ), - ), + return FloatingAppBarPill( + isCircular: isCircular, + child: child, ); } diff --git a/lib/features/notes/views/notes_list_page.dart b/lib/features/notes/views/notes_list_page.dart index 97d734e..c6ac950 100644 --- a/lib/features/notes/views/notes_list_page.dart +++ b/lib/features/notes/views/notes_list_page.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:io' show Platform; -import 'dart:ui' show ImageFilter; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -15,7 +14,7 @@ import '../../../core/providers/app_providers.dart'; import '../../../core/services/navigation_service.dart'; import '../../../core/widgets/error_boundary.dart'; import '../../../shared/theme/theme_extensions.dart'; -import '../../../shared/utils/ui_utils.dart'; +import '../../../shared/widgets/conduit_components.dart'; import '../../../shared/widgets/improved_loading_states.dart'; import '../../../shared/widgets/loading_states.dart'; import '../../../shared/widgets/themed_dialogs.dart'; @@ -130,123 +129,23 @@ class _NotesListPageState extends ConsumerState { child: Scaffold( backgroundColor: context.conduitTheme.surfaceBackground, extendBodyBehindAppBar: true, - appBar: PreferredSize( - preferredSize: const Size.fromHeight(kToolbarHeight + 64), - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - stops: const [0.0, 0.4, 1.0], - colors: [ - Theme.of(context).scaffoldBackgroundColor, - Theme.of(context).scaffoldBackgroundColor.withValues( - alpha: 0.85, - ), - Theme.of(context).scaffoldBackgroundColor.withValues( - alpha: 0.0, - ), - ], - ), - ), - child: SafeArea( - bottom: false, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // App bar row with back button and title - SizedBox( - height: kToolbarHeight, - child: Row( - children: [ - // Leading (back button) - if (canPop) - Padding( - padding: const EdgeInsets.only( - left: Spacing.inputPadding, - ), - child: Center( - child: GestureDetector( - onTap: () => Navigator.of(context).maybePop(), - child: _buildAppBarPill( - context, - Icon( - UiUtils.platformIcon( - ios: CupertinoIcons.back, - android: Icons.arrow_back, - ), - color: context.conduitTheme.textPrimary, - size: IconSize.appBar, - ), - isCircular: true, - ), - ), - ), - ) - else - const SizedBox(width: Spacing.inputPadding), - // Title centered - Expanded( - child: Center( - child: _buildAppBarPill( - context, - Padding( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.xs, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Platform.isIOS - ? CupertinoIcons.doc_text_fill - : Icons.notes_rounded, - color: context.conduitTheme.textPrimary - .withValues(alpha: 0.7), - size: IconSize.md, - ), - const SizedBox(width: Spacing.sm), - Text( - l10n.notes, - style: - AppTypography.headlineSmallStyle - .copyWith( - color: context - .conduitTheme - .textPrimary, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ), - ), - ), - ), - // Trailing spacer to balance - if (canPop) - const SizedBox( - width: 44 + Spacing.inputPadding, - ) - else - const SizedBox(width: Spacing.inputPadding), - ], - ), - ), - // Search bar directly below title - Padding( - padding: const EdgeInsets.fromLTRB( - Spacing.inputPadding, - Spacing.xs, - Spacing.inputPadding, - Spacing.sm, - ), - child: _buildFloatingSearchField(context), - ), - ], - ), + appBar: FloatingAppBar( + leading: canPop ? const FloatingAppBarBackButton() : null, + title: FloatingAppBarTitle( + text: l10n.notes, + icon: Platform.isIOS + ? CupertinoIcons.doc_text_fill + : Icons.notes_rounded, + ), + bottomHeight: 64, + bottom: Padding( + padding: const EdgeInsets.fromLTRB( + Spacing.inputPadding, + Spacing.xs, + Spacing.inputPadding, + Spacing.sm, ), + child: _buildFloatingSearchField(context), ), ), body: _buildBody(context), @@ -255,140 +154,62 @@ class _NotesListPageState extends ConsumerState { ); } - Widget _buildAppBarPill( - BuildContext context, - Widget child, { - bool isCircular = false, - }) { - final theme = Theme.of(context); - final isDark = theme.brightness == Brightness.dark; - - final backgroundColor = isDark - ? Color.lerp(context.conduitTheme.cardBackground, Colors.white, 0.08)! - : Color.lerp(context.conduitTheme.inputBackground, Colors.black, 0.06)!; - - final borderColor = context.conduitTheme.cardBorder.withValues( - alpha: isDark ? 0.65 : 0.55, - ); - - final borderRadius = isCircular - ? BorderRadius.circular(100) - : BorderRadius.circular(AppBorderRadius.pill); - - if (isCircular) { - return SizedBox( - width: 44, - height: 44, - child: ClipRRect( - borderRadius: borderRadius, - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16), - child: Container( - decoration: BoxDecoration( - color: backgroundColor.withValues(alpha: 0.85), - borderRadius: borderRadius, - border: Border.all(color: borderColor, width: BorderWidth.thin), - ), - child: Center(child: child), - ), - ), - ), - ); - } - - return ClipRRect( - borderRadius: borderRadius, - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16), - child: Container( - decoration: BoxDecoration( - color: backgroundColor.withValues(alpha: 0.85), - borderRadius: borderRadius, - border: Border.all(color: borderColor, width: BorderWidth.thin), - ), - child: child, - ), - ), - ); - } - Widget _buildFloatingSearchField(BuildContext context) { - final theme = Theme.of(context); final conduitTheme = context.conduitTheme; - final isDark = theme.brightness == Brightness.dark; final l10n = AppLocalizations.of(context)!; - final backgroundColor = isDark - ? Color.lerp(conduitTheme.cardBackground, Colors.white, 0.08)! - : Color.lerp(conduitTheme.inputBackground, Colors.black, 0.06)!; - - final borderColor = conduitTheme.cardBorder.withValues( - alpha: isDark ? 0.65 : 0.55, - ); - - return ClipRRect( - borderRadius: BorderRadius.circular(AppBorderRadius.pill), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16), - child: Container( - decoration: BoxDecoration( - color: backgroundColor.withValues(alpha: 0.85), - borderRadius: BorderRadius.circular(AppBorderRadius.pill), - border: Border.all(color: borderColor, width: BorderWidth.thin), + return FloatingAppBarPill( + child: Material( + color: Colors.transparent, + child: TextField( + controller: _searchController, + focusNode: _searchFocusNode, + onChanged: (_) => _onSearchChanged(), + style: AppTypography.standard.copyWith( + color: conduitTheme.textPrimary, ), - child: Material( - color: Colors.transparent, - child: TextField( - controller: _searchController, - focusNode: _searchFocusNode, - onChanged: (_) => _onSearchChanged(), - style: AppTypography.standard.copyWith( - color: conduitTheme.textPrimary, - ), - decoration: InputDecoration( - isDense: true, - hintText: l10n.searchNotes, - hintStyle: AppTypography.standard.copyWith( - color: conduitTheme.textSecondary.withValues(alpha: 0.6), - ), - prefixIcon: Icon( - Platform.isIOS ? CupertinoIcons.search : Icons.search, - color: conduitTheme.iconSecondary, - size: IconSize.input, - ), - prefixIconConstraints: const BoxConstraints( - minWidth: TouchTarget.minimum, - minHeight: TouchTarget.minimum, - ), - suffixIcon: _query.isNotEmpty - ? IconButton( - onPressed: () { - _searchController.clear(); - setState(() => _query = ''); - _searchFocusNode.unfocus(); - }, - icon: Icon( - Platform.isIOS - ? CupertinoIcons.clear_circled_solid - : Icons.clear, - color: conduitTheme.iconSecondary, - size: IconSize.input, - ), - ) - : null, - suffixIconConstraints: const BoxConstraints( - minWidth: TouchTarget.minimum, - minHeight: TouchTarget.minimum, - ), - filled: false, - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - contentPadding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.sm, - ), - ), + decoration: InputDecoration( + isDense: true, + hintText: l10n.searchNotes, + hintStyle: AppTypography.standard.copyWith( + color: conduitTheme.textSecondary.withValues(alpha: 0.6), + ), + prefixIcon: Icon( + Platform.isIOS ? CupertinoIcons.search : Icons.search, + color: conduitTheme.iconSecondary, + size: IconSize.input, + ), + prefixIconConstraints: const BoxConstraints( + minWidth: TouchTarget.minimum, + minHeight: TouchTarget.minimum, + ), + suffixIcon: _query.isNotEmpty + ? IconButton( + onPressed: () { + _searchController.clear(); + setState(() => _query = ''); + _searchFocusNode.unfocus(); + }, + icon: Icon( + Platform.isIOS + ? CupertinoIcons.clear_circled_solid + : Icons.clear, + color: conduitTheme.iconSecondary, + size: IconSize.input, + ), + ) + : null, + suffixIconConstraints: const BoxConstraints( + minWidth: TouchTarget.minimum, + minHeight: TouchTarget.minimum, + ), + filled: false, + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + horizontal: Spacing.md, + vertical: Spacing.sm, ), ), ), diff --git a/lib/features/profile/views/app_customization_page.dart b/lib/features/profile/views/app_customization_page.dart index fed4434..311cd83 100644 --- a/lib/features/profile/views/app_customization_page.dart +++ b/lib/features/profile/views/app_customization_page.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:io' show Platform; -import 'dart:ui' show ImageFilter; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -48,88 +47,12 @@ class AppCustomizationPage extends ConsumerWidget { final canPop = ModalRoute.of(context)?.canPop ?? false; final topPadding = MediaQuery.of(context).padding.top + kToolbarHeight + 24; - final theme = Theme.of(context); - final conduitTheme = context.conduitTheme; - return Scaffold( - backgroundColor: conduitTheme.surfaceBackground, + backgroundColor: context.conduitTheme.surfaceBackground, extendBodyBehindAppBar: true, - appBar: PreferredSize( - preferredSize: const Size.fromHeight(kToolbarHeight + 8), - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - stops: const [0.0, 0.4, 1.0], - colors: [ - theme.scaffoldBackgroundColor, - theme.scaffoldBackgroundColor.withValues(alpha: 0.85), - theme.scaffoldBackgroundColor.withValues(alpha: 0.0), - ], - ), - ), - child: SafeArea( - bottom: false, - child: SizedBox( - height: kToolbarHeight, - child: Row( - children: [ - // Leading (back button) - if (canPop) - Padding( - padding: const EdgeInsets.only(left: Spacing.inputPadding), - child: Center( - child: GestureDetector( - onTap: () => Navigator.of(context).maybePop(), - child: _buildAppBarPill( - context, - Icon( - UiUtils.platformIcon( - ios: CupertinoIcons.back, - android: Icons.arrow_back, - ), - color: conduitTheme.textPrimary, - size: IconSize.appBar, - ), - isCircular: true, - ), - ), - ), - ) - else - const SizedBox(width: Spacing.inputPadding), - // Title centered - Expanded( - child: Center( - child: _buildAppBarPill( - context, - Padding( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.xs, - ), - child: Text( - l10n.appCustomization, - style: AppTypography.headlineSmallStyle.copyWith( - color: conduitTheme.textPrimary, - fontWeight: FontWeight.w600, - ), - ), - ), - ), - ), - ), - // Trailing spacer to balance - if (canPop) - const SizedBox(width: 44 + Spacing.inputPadding) - else - const SizedBox(width: Spacing.inputPadding), - ], - ), - ), - ), - ), + appBar: FloatingAppBar( + leading: canPop ? const FloatingAppBarBackButton() : null, + title: FloatingAppBarTitle(text: l10n.appCustomization), ), body: ListView( physics: const BouncingScrollPhysics( @@ -170,63 +93,6 @@ class AppCustomizationPage extends ConsumerWidget { ); } - Widget _buildAppBarPill( - BuildContext context, - Widget child, { - bool isCircular = false, - }) { - final theme = Theme.of(context); - final isDark = theme.brightness == Brightness.dark; - - final backgroundColor = isDark - ? Color.lerp(context.conduitTheme.cardBackground, Colors.white, 0.08)! - : Color.lerp(context.conduitTheme.inputBackground, Colors.black, 0.06)!; - - final borderColor = context.conduitTheme.cardBorder.withValues( - alpha: isDark ? 0.65 : 0.55, - ); - - final borderRadius = isCircular - ? BorderRadius.circular(100) - : BorderRadius.circular(AppBorderRadius.pill); - - if (isCircular) { - return SizedBox( - width: 44, - height: 44, - child: ClipRRect( - borderRadius: borderRadius, - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16), - child: Container( - decoration: BoxDecoration( - color: backgroundColor.withValues(alpha: 0.85), - borderRadius: borderRadius, - border: Border.all(color: borderColor, width: BorderWidth.thin), - ), - child: Center(child: child), - ), - ), - ), - ); - } - - return ClipRRect( - borderRadius: borderRadius, - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16), - child: Container( - decoration: BoxDecoration( - color: backgroundColor.withValues(alpha: 0.85), - borderRadius: borderRadius, - border: Border.all(color: borderColor, width: BorderWidth.thin), - ), - child: child, - ), - ), - ); - } - Widget _buildThemesDropdownSection( BuildContext context, WidgetRef ref, diff --git a/lib/features/profile/views/profile_page.dart b/lib/features/profile/views/profile_page.dart index 9acd2c4..f1213a3 100644 --- a/lib/features/profile/views/profile_page.dart +++ b/lib/features/profile/views/profile_page.dart @@ -9,7 +9,6 @@ import 'package:url_launcher/url_launcher_string.dart'; import 'package:conduit/l10n/app_localizations.dart'; import '../../../core/widgets/error_boundary.dart'; import '../../../shared/widgets/improved_loading_states.dart'; -import 'dart:ui' show ImageFilter; import '../../../shared/utils/ui_utils.dart'; import '../../../shared/widgets/themed_dialogs.dart'; @@ -68,151 +67,19 @@ class ProfilePage extends ConsumerWidget { Scaffold _buildScaffold(BuildContext context, {required Widget body}) { final canPop = ModalRoute.of(context)?.canPop ?? false; - final theme = Theme.of(context); final l10n = AppLocalizations.of(context)!; - final conduitTheme = context.conduitTheme; return Scaffold( - backgroundColor: conduitTheme.surfaceBackground, + backgroundColor: context.conduitTheme.surfaceBackground, extendBodyBehindAppBar: true, - appBar: PreferredSize( - preferredSize: const Size.fromHeight(kToolbarHeight + 8), - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - stops: const [0.0, 0.4, 1.0], - colors: [ - theme.scaffoldBackgroundColor, - theme.scaffoldBackgroundColor.withValues(alpha: 0.85), - theme.scaffoldBackgroundColor.withValues(alpha: 0.0), - ], - ), - ), - child: SafeArea( - bottom: false, - child: SizedBox( - height: kToolbarHeight, - child: Row( - children: [ - // Leading (back button) - if (canPop) - Padding( - padding: const EdgeInsets.only(left: Spacing.inputPadding), - child: Center( - child: GestureDetector( - onTap: () => Navigator.of(context).maybePop(), - child: _buildAppBarPill( - context, - Icon( - UiUtils.platformIcon( - ios: CupertinoIcons.back, - android: Icons.arrow_back, - ), - color: conduitTheme.textPrimary, - size: IconSize.appBar, - ), - isCircular: true, - ), - ), - ), - ) - else - const SizedBox(width: Spacing.inputPadding), - // Title centered - Expanded( - child: Center( - child: _buildAppBarPill( - context, - Padding( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.xs, - ), - child: Text( - l10n.you, - style: AppTypography.headlineSmallStyle.copyWith( - color: conduitTheme.textPrimary, - fontWeight: FontWeight.w600, - ), - ), - ), - ), - ), - ), - // Trailing spacer to balance - if (canPop) - const SizedBox(width: 44 + Spacing.inputPadding) - else - const SizedBox(width: Spacing.inputPadding), - ], - ), - ), - ), - ), + appBar: FloatingAppBar( + leading: canPop ? const FloatingAppBarBackButton() : null, + title: FloatingAppBarTitle(text: l10n.you), ), body: body, ); } - Widget _buildAppBarPill( - BuildContext context, - Widget child, { - bool isCircular = false, - }) { - final theme = Theme.of(context); - final isDark = theme.brightness == Brightness.dark; - - final backgroundColor = isDark - ? Color.lerp(context.conduitTheme.cardBackground, Colors.white, 0.08)! - : Color.lerp(context.conduitTheme.inputBackground, Colors.black, 0.06)!; - - final borderColor = context.conduitTheme.cardBorder.withValues( - alpha: isDark ? 0.65 : 0.55, - ); - - final borderRadius = isCircular - ? BorderRadius.circular(100) - : BorderRadius.circular(AppBorderRadius.pill); - - if (isCircular) { - return SizedBox( - width: 44, - height: 44, - child: ClipRRect( - borderRadius: borderRadius, - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16), - child: Container( - decoration: BoxDecoration( - color: backgroundColor.withValues(alpha: 0.85), - borderRadius: borderRadius, - border: Border.all(color: borderColor, width: BorderWidth.thin), - ), - child: Center(child: child), - ), - ), - ), - ); - } - - return ClipRRect( - borderRadius: borderRadius, - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16), - child: Container( - decoration: BoxDecoration( - color: backgroundColor.withValues(alpha: 0.85), - borderRadius: borderRadius, - border: Border.all(color: borderColor, width: BorderWidth.thin), - ), - child: child, - ), - ), - ); - } - Widget _buildCenteredState(BuildContext context, Widget child) { final topPadding = MediaQuery.of(context).padding.top + kToolbarHeight + 24; return Padding( diff --git a/lib/shared/widgets/conduit_components.dart b/lib/shared/widgets/conduit_components.dart index f62e7d9..f6f8242 100644 --- a/lib/shared/widgets/conduit_components.dart +++ b/lib/shared/widgets/conduit_components.dart @@ -1,3 +1,5 @@ +import 'dart:ui' show ImageFilter; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../theme/theme_extensions.dart'; @@ -10,6 +12,307 @@ import '../../core/services/settings_service.dart'; /// Unified component library following Conduit design patterns /// This provides consistent, reusable UI components throughout the app +// ============================================================================= +// FLOATING APP BAR COMPONENTS +// ============================================================================= + +/// A pill-shaped container with blur effect for floating app bar elements. +/// Used for back buttons, titles, and action buttons in the floating app bar. +class FloatingAppBarPill extends StatelessWidget { + final Widget child; + final bool isCircular; + + const FloatingAppBarPill({ + super.key, + required this.child, + this.isCircular = false, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final conduitTheme = context.conduitTheme; + final isDark = theme.brightness == Brightness.dark; + + final backgroundColor = isDark + ? Color.lerp(conduitTheme.cardBackground, Colors.white, 0.08)! + : Color.lerp(conduitTheme.inputBackground, Colors.black, 0.06)!; + + final borderColor = conduitTheme.cardBorder.withValues( + alpha: isDark ? 0.65 : 0.55, + ); + + final borderRadius = isCircular + ? BorderRadius.circular(100) + : BorderRadius.circular(AppBorderRadius.pill); + + if (isCircular) { + return SizedBox( + width: 44, + height: 44, + child: ClipRRect( + borderRadius: borderRadius, + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16), + child: Container( + decoration: BoxDecoration( + color: backgroundColor.withValues(alpha: 0.85), + borderRadius: borderRadius, + border: Border.all(color: borderColor, width: BorderWidth.thin), + ), + child: Center(child: child), + ), + ), + ), + ); + } + + return ClipRRect( + borderRadius: borderRadius, + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16), + child: Container( + decoration: BoxDecoration( + color: backgroundColor.withValues(alpha: 0.85), + borderRadius: borderRadius, + border: Border.all(color: borderColor, width: BorderWidth.thin), + ), + child: child, + ), + ), + ); + } +} + +/// A floating app bar with gradient background and pill-shaped elements. +/// Provides a consistent app bar style across the app with blur effects. +/// +/// Supports: +/// - Simple title with optional leading/actions +/// - Custom title widget for complex layouts +/// - Bottom widget for search bars or other content +/// - Flexible actions positioning +class FloatingAppBar extends StatelessWidget implements PreferredSizeWidget { + /// Leading widget (typically a back button or menu button) + final Widget? leading; + + /// Title widget - can be a simple [FloatingAppBarTitle] or custom widget + final Widget title; + + /// Action widgets displayed on the right side + final List? actions; + + /// Bottom widget displayed below the main row (e.g., search bar) + final Widget? bottom; + + /// Height of the bottom widget (used for preferredSize calculation) + final double bottomHeight; + + /// Whether to show a trailing spacer when there's a leading widget but no actions + /// Set to false if you want the title to use all available space + final bool balanceLeading; + + const FloatingAppBar({ + super.key, + this.leading, + required this.title, + this.actions, + this.bottom, + this.bottomHeight = 0, + this.balanceLeading = true, + }); + + @override + Size get preferredSize => Size.fromHeight(kToolbarHeight + bottomHeight); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + stops: const [0.0, 0.4, 1.0], + colors: [ + theme.scaffoldBackgroundColor, + theme.scaffoldBackgroundColor.withValues(alpha: 0.85), + theme.scaffoldBackgroundColor.withValues(alpha: 0.0), + ], + ), + ), + child: SafeArea( + bottom: false, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: kToolbarHeight, + child: Row( + children: [ + // Leading + if (leading != null) + Padding( + padding: const EdgeInsets.only(left: Spacing.inputPadding), + child: Center(child: leading), + ) + else + const SizedBox(width: Spacing.inputPadding), + // Title centered + Expanded( + child: Center(child: title), + ), + // Actions or trailing spacer + if (actions != null && actions!.isNotEmpty) + Row( + mainAxisSize: MainAxisSize.min, + children: actions!, + ) + else if (leading != null && balanceLeading) + const SizedBox(width: 44 + Spacing.inputPadding) + else + const SizedBox(width: Spacing.inputPadding), + ], + ), + ), + if (bottom != null) bottom!, + ], + ), + ), + ); + } +} + +/// Helper to build a standard floating app bar title pill with text. +class FloatingAppBarTitle extends StatelessWidget { + final String text; + final IconData? icon; + + const FloatingAppBarTitle({ + super.key, + required this.text, + this.icon, + }); + + @override + Widget build(BuildContext context) { + final conduitTheme = context.conduitTheme; + + return FloatingAppBarPill( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: Spacing.md, + vertical: Spacing.xs, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (icon != null) ...[ + Icon( + icon, + color: conduitTheme.textPrimary.withValues(alpha: 0.7), + size: IconSize.md, + ), + const SizedBox(width: Spacing.sm), + ], + Text( + text, + style: AppTypography.headlineSmallStyle.copyWith( + color: conduitTheme.textPrimary, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ); + } +} + +/// Helper to build a standard floating app bar back button. +class FloatingAppBarBackButton extends StatelessWidget { + final VoidCallback? onTap; + final IconData? icon; + + const FloatingAppBarBackButton({ + super.key, + this.onTap, + this.icon, + }); + + @override + Widget build(BuildContext context) { + final conduitTheme = context.conduitTheme; + final isIOS = Theme.of(context).platform == TargetPlatform.iOS; + + return GestureDetector( + onTap: onTap ?? () => Navigator.of(context).maybePop(), + child: FloatingAppBarPill( + isCircular: true, + child: Icon( + icon ?? (isIOS ? Icons.arrow_back_ios_new : Icons.arrow_back), + color: conduitTheme.textPrimary, + size: IconSize.appBar, + ), + ), + ); + } +} + +/// Helper to build a floating app bar icon button (circular pill with icon). +class FloatingAppBarIconButton extends StatelessWidget { + final IconData icon; + final VoidCallback? onTap; + final Color? iconColor; + + const FloatingAppBarIconButton({ + super.key, + required this.icon, + this.onTap, + this.iconColor, + }); + + @override + Widget build(BuildContext context) { + final conduitTheme = context.conduitTheme; + + return GestureDetector( + onTap: onTap, + child: FloatingAppBarPill( + isCircular: true, + child: Icon( + icon, + color: iconColor ?? conduitTheme.textPrimary, + size: IconSize.appBar, + ), + ), + ); + } +} + +/// Helper to build a floating app bar action with padding. +class FloatingAppBarAction extends StatelessWidget { + final Widget child; + + const FloatingAppBarAction({ + super.key, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(right: Spacing.inputPadding), + child: Center(child: child), + ); + } +} + +// ============================================================================= +// EXISTING COMPONENTS +// ============================================================================= + class ConduitButton extends ConsumerWidget { final String text; final VoidCallback? onPressed;