diff --git a/lib/features/notes/views/note_editor_page.dart b/lib/features/notes/views/note_editor_page.dart index ebb1f3e..c3bbaeb 100644 --- a/lib/features/notes/views/note_editor_page.dart +++ b/lib/features/notes/views/note_editor_page.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io' show Platform; +import 'dart:ui' show ImageFilter; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -15,6 +16,7 @@ import '../../../core/widgets/error_boundary.dart'; import '../../../shared/theme/theme_extensions.dart'; import '../../../shared/utils/ui_utils.dart'; import '../../../shared/widgets/improved_loading_states.dart'; +import '../../../shared/widgets/middle_ellipsis_text.dart'; import '../../../shared/widgets/themed_dialogs.dart'; import '../../chat/services/voice_input_service.dart'; import '../providers/notes_providers.dart'; @@ -492,204 +494,392 @@ class _NoteEditorPageState extends ConsumerState { }, child: ErrorBoundary( child: Scaffold( - backgroundColor: sidebarTheme.background, - body: SafeArea( - child: Stack( - children: [ - Column( - children: [ - _buildHeader(context), - if (!_isLoading && _note != null) - _buildMetadataBar(context), - Expanded(child: _buildBody(context)), - ], + backgroundColor: context.conduitTheme.surfaceBackground, + extendBodyBehindAppBar: true, + appBar: _buildAppBar(context), + body: Stack( + children: [ + // Main content - scrolls behind floating elements + Positioned.fill( + child: _buildMainContent(context), + ), + // Floating action buttons + if (!_isLoading && _note != null) + Positioned( + left: Spacing.md, + right: Spacing.md, + bottom: Spacing.md + MediaQuery.of(context).padding.bottom, + child: _buildFloatingActionsRow(context), ), - // Floating action buttons - if (!_isLoading && _note != null) - _buildFloatingActions(context), - ], - ), + ], ), ), ), ); } - Widget _buildHeader(BuildContext context) { - final theme = context.conduitTheme; - final sidebarTheme = context.sidebarTheme; + PreferredSizeWidget _buildAppBar(BuildContext context) { + final theme = Theme.of(context); + final conduitTheme = context.conduitTheme; final l10n = AppLocalizations.of(context)!; - return Container( - padding: const EdgeInsets.fromLTRB( - Spacing.xs, - Spacing.sm, - Spacing.sm, - Spacing.xs, - ), - color: sidebarTheme.background, - child: Row( - children: [ - // Back button - IconButton( - icon: Icon( - UiUtils.platformIcon( - ios: CupertinoIcons.back, - android: Icons.arrow_back_rounded, - ), - color: theme.iconPrimary, - ), - onPressed: () async { - final navigator = Navigator.of(context); - await _onWillPop(); - if (!mounted) return; - navigator.pop(); - }, - tooltip: l10n.back, - ), - const SizedBox(width: Spacing.xs), - - // Title input - Expanded( - child: TextField( - controller: _titleController, - focusNode: _titleFocusNode, - enabled: !_isGeneratingTitle, - style: AppTypography.headlineSmallStyle.copyWith( - color: theme.textPrimary, - fontWeight: FontWeight.w600, - ), - decoration: InputDecoration( - hintText: _isGeneratingTitle - ? l10n.generatingTitle - : l10n.noteTitle, - hintStyle: AppTypography.headlineSmallStyle.copyWith( - color: theme.textSecondary.withValues(alpha: 0.4), - fontWeight: FontWeight.w600, - ), - filled: false, - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - disabledBorder: InputBorder.none, - contentPadding: EdgeInsets.zero, - isDense: true, - ), - textCapitalization: TextCapitalization.sentences, - textInputAction: TextInputAction.next, - onSubmitted: (_) => _contentFocusNode.requestFocus(), - ), - ), - - // Generate title button - aligned with other header icons - AnimatedOpacity( - opacity: _titleFocusNode.hasFocus && !_isGeneratingTitle - ? 1.0 - : 0.0, - duration: const Duration(milliseconds: 150), - child: IgnorePointer( - ignoring: !_titleFocusNode.hasFocus || _isGeneratingTitle, - child: IconButton( - icon: Icon( - Platform.isIOS - ? CupertinoIcons.sparkles - : Icons.auto_awesome_rounded, - color: theme.buttonPrimary, - ), - onPressed: _generateTitle, - tooltip: l10n.generateTitle, - ), - ), - ), - - // Save indicator - if (_isSaving) - Padding( - padding: const EdgeInsets.symmetric(horizontal: Spacing.sm), - child: SizedBox( - width: IconSize.sm, - height: IconSize.sm, - child: CircularProgressIndicator( - strokeWidth: BorderWidth.medium, - valueColor: AlwaysStoppedAnimation(theme.loadingIndicator), - ), - ), - ) - else if (_hasChanges) - Padding( - padding: const EdgeInsets.symmetric(horizontal: Spacing.sm), - child: Container( - width: 8, - height: 8, - decoration: BoxDecoration( - color: theme.warning, - shape: BoxShape.circle, - ), - ), - ), - - // Menu - PopupMenuButton( - icon: Icon( - Platform.isIOS - ? CupertinoIcons.ellipsis - : Icons.more_vert_rounded, - color: theme.iconPrimary, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.md), - ), - onSelected: (value) { - switch (value) { - case 'copy': - _copyToClipboard(); - case 'delete': - _deleteNote(); - } - }, - itemBuilder: (context) => [ - PopupMenuItem( - value: 'copy', - child: Row( - children: [ - Icon( - Platform.isIOS - ? CupertinoIcons.doc_on_clipboard - : Icons.copy_rounded, - color: theme.iconPrimary, - size: IconSize.md, - ), - const SizedBox(width: Spacing.sm), - Text(l10n.copy), - ], - ), - ), - PopupMenuItem( - value: 'delete', - child: Row( - children: [ - Icon( - Platform.isIOS - ? CupertinoIcons.delete - : Icons.delete_rounded, - color: theme.error, - size: IconSize.md, - ), - const SizedBox(width: Spacing.sm), - Text(l10n.delete, style: TextStyle(color: theme.error)), - ], - ), - ), + return PreferredSize( + preferredSize: const Size.fromHeight(kToolbarHeight + 40), + 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: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // App bar row with back button, title, and menu + SizedBox( + height: kToolbarHeight, + child: Row( + children: [ + // Leading (back button) + Padding( + padding: const EdgeInsets.only(left: Spacing.inputPadding), + child: Center( + child: GestureDetector( + onTap: () async { + final navigator = Navigator.of(context); + await _onWillPop(); + if (!mounted) return; + navigator.pop(); + }, + child: _buildAppBarPill( + context, + Icon( + UiUtils.platformIcon( + ios: CupertinoIcons.back, + android: Icons.arrow_back, + ), + color: conduitTheme.textPrimary, + size: IconSize.appBar, + ), + isCircular: true, + ), + ), + ), + ), + // Title centered + Expanded( + child: Center( + child: _buildAppBarPill( + context, + Padding( + padding: const EdgeInsets.symmetric( + horizontal: Spacing.sm, + vertical: Spacing.xs, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: _isGeneratingTitle + ? Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: IconSize.sm, + height: IconSize.sm, + child: CircularProgressIndicator( + strokeWidth: BorderWidth.medium, + valueColor: AlwaysStoppedAnimation( + conduitTheme.loadingIndicator, + ), + ), + ), + const SizedBox(width: Spacing.sm), + Text( + l10n.generatingTitle, + style: AppTypography.bodyMediumStyle.copyWith( + color: conduitTheme.textSecondary, + ), + ), + ], + ) + : ConstrainedBox( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.5, + ), + child: Stack( + alignment: Alignment.center, + children: [ + // Hidden TextField always in tree for focus + Opacity( + opacity: _titleFocusNode.hasFocus ? 1.0 : 0.0, + child: IntrinsicWidth( + child: TextField( + controller: _titleController, + focusNode: _titleFocusNode, + enabled: !_isGeneratingTitle, + style: AppTypography.headlineSmallStyle + .copyWith( + color: conduitTheme.textPrimary, + fontWeight: FontWeight.w600, + ), + decoration: InputDecoration( + hintText: l10n.untitled, + hintStyle: AppTypography.headlineSmallStyle + .copyWith( + color: conduitTheme.textSecondary + .withValues(alpha: 0.6), + fontWeight: FontWeight.w600, + ), + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + contentPadding: EdgeInsets.zero, + isDense: true, + ), + textAlign: TextAlign.center, + textCapitalization: + TextCapitalization.sentences, + textInputAction: TextInputAction.done, + onSubmitted: (_) => + _contentFocusNode.requestFocus(), + ), + ), + ), + // Visible text when not focused + if (!_titleFocusNode.hasFocus) + GestureDetector( + onTap: () => _titleFocusNode.requestFocus(), + child: MiddleEllipsisText( + _titleController.text.isEmpty + ? l10n.untitled + : _titleController.text, + style: AppTypography.headlineSmallStyle + .copyWith( + color: _titleController.text.isEmpty + ? conduitTheme.textSecondary + .withValues(alpha: 0.6) + : conduitTheme.textPrimary, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), + ), + if (_hasChanges && !_isSaving) + Padding( + padding: const EdgeInsets.only(left: Spacing.sm), + child: Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: conduitTheme.warning, + shape: BoxShape.circle, + ), + ), + ), + if (_isSaving) + Padding( + padding: const EdgeInsets.only(left: Spacing.sm), + child: SizedBox( + width: IconSize.sm, + height: IconSize.sm, + child: CircularProgressIndicator( + strokeWidth: BorderWidth.medium, + valueColor: AlwaysStoppedAnimation( + conduitTheme.loadingIndicator, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + // Actions (more menu) + Padding( + padding: const EdgeInsets.only(right: Spacing.inputPadding), + child: Center( + child: PopupMenuButton( + tooltip: '', + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppBorderRadius.md), + ), + onSelected: (value) { + switch (value) { + case 'generate_title': + _generateTitle(); + case 'copy': + _copyToClipboard(); + case 'delete': + _deleteNote(); + } + }, + itemBuilder: (context) => [ + PopupMenuItem( + value: 'generate_title', + child: Row( + children: [ + Icon( + Platform.isIOS + ? CupertinoIcons.sparkles + : Icons.auto_awesome_rounded, + color: conduitTheme.buttonPrimary, + size: IconSize.md, + ), + const SizedBox(width: Spacing.sm), + Text(l10n.generateTitle), + ], + ), + ), + PopupMenuItem( + value: 'copy', + child: Row( + children: [ + Icon( + Platform.isIOS + ? CupertinoIcons.doc_on_clipboard + : Icons.copy_rounded, + color: conduitTheme.iconPrimary, + size: IconSize.md, + ), + const SizedBox(width: Spacing.sm), + Text(l10n.copy), + ], + ), + ), + PopupMenuItem( + value: 'delete', + child: Row( + children: [ + Icon( + Platform.isIOS + ? CupertinoIcons.delete + : Icons.delete_rounded, + color: conduitTheme.error, + size: IconSize.md, + ), + const SizedBox(width: Spacing.sm), + Text( + l10n.delete, + style: TextStyle(color: conduitTheme.error), + ), + ], + ), + ), + ], + child: _buildAppBarPill( + context, + Icon( + Platform.isIOS + ? CupertinoIcons.ellipsis + : Icons.more_vert_rounded, + color: conduitTheme.textPrimary, + size: IconSize.appBar, + ), + isCircular: true, + ), + ), + ), + ), + ], + ), + ), + // Metadata stats row + if (!_isLoading && _note != null) + Padding( + padding: const EdgeInsets.only(bottom: Spacing.xs), + child: _buildFloatingMetadataBar(context), + ), + ], + ), + ), ), ); } - Widget _buildMetadataBar(BuildContext context) { - final theme = context.conduitTheme; + 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 _buildFloatingMetadataBar(BuildContext context) { + final theme = Theme.of(context); + final conduitTheme = context.conduitTheme; final l10n = AppLocalizations.of(context)!; + 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 dateFormat = DateFormat.MMMd(); final timeFormat = DateFormat.jm(); @@ -697,42 +887,51 @@ class _NoteEditorPageState extends ConsumerState { ? '${dateFormat.format(_note!.createdDateTime)} ${timeFormat.format(_note!.createdDateTime)}' : ''; - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.xs, - ), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - // Created date - _buildMetadataChip( - context, - icon: Platform.isIOS - ? CupertinoIcons.calendar - : Icons.calendar_today_rounded, - label: createdDate, - ), - _buildMetadataSeparator(theme), - // Word count - _buildMetadataChip( - context, - icon: Platform.isIOS - ? CupertinoIcons.doc_text - : Icons.article_rounded, - label: l10n.wordCount(_wordCount), - ), - _buildMetadataSeparator(theme), - // Character count - _buildMetadataChip( - context, - icon: Platform.isIOS - ? CupertinoIcons.textformat_abc - : Icons.text_fields_rounded, - label: l10n.charCount(_charCount), - ), - ], + return ClipRRect( + borderRadius: BorderRadius.circular(AppBorderRadius.pill), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: Spacing.md, + 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( + mainAxisSize: MainAxisSize.min, + children: [ + // Created date + _buildMetadataChip( + context, + icon: Platform.isIOS + ? CupertinoIcons.calendar + : Icons.calendar_today_rounded, + label: createdDate, + ), + _buildMetadataSeparator(conduitTheme), + // Word count + _buildMetadataChip( + context, + icon: Platform.isIOS + ? CupertinoIcons.doc_text + : Icons.article_rounded, + label: l10n.wordCount(_wordCount), + ), + _buildMetadataSeparator(conduitTheme), + // Character count + _buildMetadataChip( + context, + icon: Platform.isIOS + ? CupertinoIcons.textformat_abc + : Icons.text_fields_rounded, + label: l10n.charCount(_charCount), + ), + ], + ), ), ), ); @@ -783,6 +982,10 @@ class _NoteEditorPageState extends ConsumerState { ); } + Widget _buildMainContent(BuildContext context) { + return _buildBody(context); + } + Widget _buildBody(BuildContext context) { if (_isLoading) { return Center( @@ -796,21 +999,25 @@ class _NoteEditorPageState extends ConsumerState { return _buildNotFoundState(context); } + // Title is now edited in the app bar pill, so just show the content editor return _buildEditor(context); } Widget _buildEditor(BuildContext context) { final theme = context.conduitTheme; final l10n = AppLocalizations.of(context)!; + final topPadding = MediaQuery.of(context).padding.top; + // App bar height: kToolbarHeight + metadata bar (~40) + final appBarHeight = kToolbarHeight + 40; return GestureDetector( onTap: () => _contentFocusNode.requestFocus(), behavior: HitTestBehavior.opaque, child: SingleChildScrollView( controller: _scrollController, - padding: const EdgeInsets.fromLTRB( + padding: EdgeInsets.fromLTRB( Spacing.inputPadding, - Spacing.md, + topPadding + appBarHeight + Spacing.sm, // Space for floating app bar Spacing.inputPadding, 120, // Extra padding for floating buttons ), @@ -843,46 +1050,39 @@ class _NoteEditorPageState extends ConsumerState { ); } - Widget _buildFloatingActions(BuildContext context) { + Widget _buildFloatingActionsRow(BuildContext context) { final theme = context.conduitTheme; final l10n = AppLocalizations.of(context)!; - return Positioned( - left: Spacing.md, - right: Spacing.md, - bottom: Spacing.md, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - // Dictation button - _buildFloatingButton( - context, - icon: _isRecording - ? (Platform.isIOS - ? CupertinoIcons.stop_fill - : Icons.stop_rounded) - : (Platform.isIOS - ? CupertinoIcons.mic_fill - : Icons.mic_rounded), - color: _isRecording ? theme.error : null, - isLoading: false, - tooltip: _isRecording ? l10n.stopRecording : l10n.startDictation, - onPressed: _toggleDictation, - ), + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Dictation button + _buildFloatingButton( + context, + icon: _isRecording + ? (Platform.isIOS + ? CupertinoIcons.stop_fill + : Icons.stop_rounded) + : (Platform.isIOS ? CupertinoIcons.mic_fill : Icons.mic_rounded), + color: _isRecording ? theme.error : null, + isLoading: false, + tooltip: _isRecording ? l10n.stopRecording : l10n.startDictation, + onPressed: _toggleDictation, + ), - // AI button - _buildFloatingButton( - context, - icon: Platform.isIOS - ? CupertinoIcons.sparkles - : Icons.auto_awesome_rounded, - isLoading: _isEnhancing, - tooltip: l10n.enhanceWithAI, - onPressed: _isEnhancing ? null : _enhanceContent, - showMenu: true, - ), - ], - ), + // AI button + _buildFloatingButton( + context, + icon: Platform.isIOS + ? CupertinoIcons.sparkles + : Icons.auto_awesome_rounded, + isLoading: _isEnhancing, + tooltip: l10n.enhanceWithAI, + onPressed: _isEnhancing ? null : _enhanceContent, + showMenu: true, + ), + ], ); } @@ -895,34 +1095,52 @@ class _NoteEditorPageState extends ConsumerState { Color? color, bool showMenu = false, }) { - final theme = context.conduitTheme; - final sidebarTheme = context.sidebarTheme; + final theme = Theme.of(context); + final conduitTheme = context.conduitTheme; final l10n = AppLocalizations.of(context)!; + final isDark = theme.brightness == Brightness.dark; - final buttonChild = Container( - width: 52, - height: 52, - decoration: BoxDecoration( - color: theme.surfaceContainer, - shape: BoxShape.circle, - border: Border.all( - color: sidebarTheme.border.withValues(alpha: 0.2), - width: BorderWidth.thin, - ), - boxShadow: ConduitShadows.medium(context), - ), - child: isLoading - ? Center( - child: SizedBox( - width: IconSize.md, - height: IconSize.md, - child: CircularProgressIndicator( - strokeWidth: BorderWidth.medium, - valueColor: AlwaysStoppedAnimation(theme.loadingIndicator), + final backgroundColor = isDark + ? Color.lerp(conduitTheme.cardBackground, Colors.white, 0.08)! + .withValues(alpha: 0.85) + : Color.lerp(conduitTheme.inputBackground, Colors.black, 0.06)! + .withValues(alpha: 0.85); + + final borderColor = conduitTheme.cardBorder.withValues(alpha: 0.55); + + final buttonChild = ClipRRect( + borderRadius: BorderRadius.circular(AppBorderRadius.floatingButton), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16), + child: Container( + width: TouchTarget.button, + height: TouchTarget.button, + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(AppBorderRadius.floatingButton), + border: Border.all(color: borderColor, width: BorderWidth.thin), + boxShadow: ConduitShadows.button(context), + ), + child: isLoading + ? Center( + child: SizedBox( + width: IconSize.md, + height: IconSize.md, + child: CircularProgressIndicator( + strokeWidth: BorderWidth.medium, + valueColor: + AlwaysStoppedAnimation(conduitTheme.loadingIndicator), + ), + ), + ) + : Icon( + icon, + color: color ?? + conduitTheme.iconPrimary.withValues(alpha: 0.9), + size: IconSize.lg, ), - ), - ) - : Icon(icon, color: color ?? theme.iconPrimary, size: IconSize.lg), + ), + ), ); if (showMenu) { @@ -949,7 +1167,7 @@ class _NoteEditorPageState extends ConsumerState { Platform.isIOS ? CupertinoIcons.sparkles : Icons.auto_fix_high_rounded, - color: theme.buttonPrimary, + color: conduitTheme.buttonPrimary, size: IconSize.md, ), const SizedBox(width: Spacing.sm), @@ -965,7 +1183,7 @@ class _NoteEditorPageState extends ConsumerState { Platform.isIOS ? CupertinoIcons.textformat : Icons.title_rounded, - color: theme.buttonPrimary, + color: conduitTheme.buttonPrimary, size: IconSize.md, ), const SizedBox(width: Spacing.sm), @@ -984,7 +1202,9 @@ class _NoteEditorPageState extends ConsumerState { color: Colors.transparent, child: InkWell( onTap: onPressed, - customBorder: const CircleBorder(), + customBorder: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppBorderRadius.floatingButton), + ), child: buttonChild, ), ), diff --git a/lib/features/notes/views/notes_list_page.dart b/lib/features/notes/views/notes_list_page.dart index c8cb5f8..97d734e 100644 --- a/lib/features/notes/views/notes_list_page.dart +++ b/lib/features/notes/views/notes_list_page.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io' show Platform; +import 'dart:ui' show ImageFilter; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -122,151 +123,273 @@ class _NotesListPageState extends ConsumerState { return Scaffold(backgroundColor: sidebarTheme.background); } + final canPop = ModalRoute.of(context)?.canPop ?? false; + final l10n = AppLocalizations.of(context)!; + return ErrorBoundary( child: Scaffold( - backgroundColor: sidebarTheme.background, - body: SafeArea( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _buildHeader(context), - _buildSearchField(context), - Expanded(child: _buildBody(context)), - ], + 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), + ), + ], + ), + ), ), ), + body: _buildBody(context), floatingActionButton: _buildFAB(context), ), ); } - Widget _buildHeader(BuildContext context) { - final sidebarTheme = context.sidebarTheme; - final l10n = AppLocalizations.of(context)!; - final canPop = ModalRoute.of(context)?.canPop ?? false; + Widget _buildAppBarPill( + BuildContext context, + Widget child, { + bool isCircular = false, + }) { + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; - return Container( - padding: EdgeInsets.fromLTRB( - canPop ? Spacing.xs : Spacing.inputPadding, - Spacing.md, - Spacing.inputPadding, - Spacing.sm, - ), - child: Row( - children: [ - if (canPop) ...[ - IconButton( - icon: Icon( - UiUtils.platformIcon( - ios: CupertinoIcons.back, - android: Icons.arrow_back, - ), - color: sidebarTheme.foreground.withValues(alpha: 0.8), - ), - onPressed: () => Navigator.of(context).maybePop(), - tooltip: l10n.back, - ), - const SizedBox(width: Spacing.xs), - ], - Icon( - Platform.isIOS ? CupertinoIcons.doc_text_fill : Icons.notes_rounded, - color: sidebarTheme.foreground.withValues(alpha: 0.7), - size: IconSize.lg, - ), - const SizedBox(width: Spacing.sm), - Expanded( - child: Text( - l10n.notes, - style: AppTypography.headlineSmallStyle.copyWith( - color: sidebarTheme.foreground, - fontWeight: FontWeight.w700, + 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 _buildSearchField(BuildContext context) { - final sidebarTheme = context.sidebarTheme; + Widget _buildFloatingSearchField(BuildContext context) { + final theme = Theme.of(context); + final conduitTheme = context.conduitTheme; + final isDark = theme.brightness == Brightness.dark; final l10n = AppLocalizations.of(context)!; - return Padding( - padding: const EdgeInsets.fromLTRB( - Spacing.inputPadding, - Spacing.xs, - Spacing.inputPadding, - Spacing.sm, - ), - child: Material( - color: Colors.transparent, - child: TextField( - controller: _searchController, - focusNode: _searchFocusNode, - onChanged: (_) => _onSearchChanged(), - style: AppTypography.standard.copyWith( - color: sidebarTheme.foreground, + 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), ), - decoration: InputDecoration( - isDense: true, - hintText: l10n.searchNotes, - hintStyle: AppTypography.standard.copyWith( - color: sidebarTheme.foreground.withValues(alpha: 0.5), - ), - prefixIcon: Icon( - Platform.isIOS ? CupertinoIcons.search : Icons.search_rounded, - color: sidebarTheme.foreground.withValues(alpha: 0.6), - 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_rounded, - color: sidebarTheme.foreground.withValues(alpha: 0.6), - size: IconSize.input, - ), - ) - : null, - suffixIconConstraints: const BoxConstraints( - minWidth: TouchTarget.minimum, - minHeight: TouchTarget.minimum, - ), - filled: true, - fillColor: sidebarTheme.accent.withValues(alpha: 0.85), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.md), - borderSide: BorderSide.none, - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.md), - borderSide: BorderSide( - color: sidebarTheme.border.withValues(alpha: 0.2), - width: BorderWidth.thin, + child: Material( + color: Colors.transparent, + child: TextField( + controller: _searchController, + focusNode: _searchFocusNode, + onChanged: (_) => _onSearchChanged(), + style: AppTypography.standard.copyWith( + color: conduitTheme.textPrimary, ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.md), - borderSide: BorderSide( - color: sidebarTheme.ring.withValues(alpha: 0.5), - width: BorderWidth.regular, + 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, + ), ), ), - contentPadding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.sm, - ), ), ), ), @@ -322,7 +445,7 @@ class _NotesListPageState extends ConsumerState { ); slivers.add( SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: Spacing.sm), + padding: const EdgeInsets.symmetric(horizontal: Spacing.md), sliver: SliverList( delegate: SliverChildBuilderDelegate( (context, index) => @@ -347,12 +470,23 @@ class _NotesListPageState extends ConsumerState { } Widget _buildRefreshableScrollView(List slivers) { + // Add top padding for floating app bar and search bar + final topPadding = MediaQuery.of(context).padding.top; + // App bar height: kToolbarHeight + search bar (48) + padding (xs + sm) + final appBarHeight = kToolbarHeight + 48 + Spacing.xs + Spacing.sm; + final paddedSlivers = [ + SliverToBoxAdapter( + child: SizedBox(height: topPadding + appBarHeight), + ), + ...slivers, + ]; + return ConduitRefreshIndicator( onRefresh: _refreshNotes, child: CustomScrollView( controller: _scrollController, physics: const AlwaysScrollableScrollPhysics(), - slivers: slivers, + slivers: paddedSlivers, ), ); } @@ -691,10 +825,17 @@ class _NotesListPageState extends ConsumerState { final sidebarTheme = context.sidebarTheme; final l10n = AppLocalizations.of(context)!; final isSearchActive = _query.isNotEmpty; + final topPadding = MediaQuery.of(context).padding.top; + final appBarHeight = kToolbarHeight + 48 + Spacing.xs + Spacing.sm; return Center( child: Padding( - padding: const EdgeInsets.all(Spacing.xxl), + padding: EdgeInsets.fromLTRB( + Spacing.xxl, + topPadding + appBarHeight, + Spacing.xxl, + Spacing.xxl, + ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -765,17 +906,29 @@ class _NotesListPageState extends ConsumerState { Widget _buildLoading(BuildContext context) { final l10n = AppLocalizations.of(context)!; - return Center(child: ImprovedLoadingState(message: l10n.loadingNotes)); + final topPadding = MediaQuery.of(context).padding.top; + final appBarHeight = kToolbarHeight + 48 + Spacing.xs + Spacing.sm; + return Padding( + padding: EdgeInsets.only(top: topPadding + appBarHeight), + child: Center(child: ImprovedLoadingState(message: l10n.loadingNotes)), + ); } Widget _buildError(BuildContext context, Object error) { final theme = context.conduitTheme; final sidebarTheme = context.sidebarTheme; final l10n = AppLocalizations.of(context)!; + final topPadding = MediaQuery.of(context).padding.top; + final appBarHeight = kToolbarHeight + 48 + Spacing.xs + Spacing.sm; return Center( child: Padding( - padding: const EdgeInsets.all(Spacing.xxl), + padding: EdgeInsets.fromLTRB( + Spacing.xxl, + topPadding + appBarHeight, + Spacing.xxl, + Spacing.xxl, + ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ diff --git a/lib/features/profile/views/app_customization_page.dart b/lib/features/profile/views/app_customization_page.dart index 10b52f4..fed4434 100644 --- a/lib/features/profile/views/app_customization_page.dart +++ b/lib/features/profile/views/app_customization_page.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io' show Platform; +import 'dart:ui' show ImageFilter; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -44,79 +45,185 @@ class AppCustomizationPage extends ConsumerWidget { final currentLanguageCode = locale?.toLanguageTag() ?? 'system'; final languageLabel = _resolveLanguageLabel(context, currentLanguageCode); final activeTheme = ref.watch(appThemePaletteProvider); + 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: context.sidebarTheme.background, - appBar: _buildAppBar(context), - body: SafeArea( - child: ListView( - physics: const BouncingScrollPhysics( - parent: AlwaysScrollableScrollPhysics(), - ), - padding: const EdgeInsets.symmetric( - horizontal: Spacing.pagePadding, - vertical: Spacing.pagePadding, - ), - children: [ - _buildThemesDropdownSection( - context, - ref, - themeMode, - themeDescription, - activeTheme, - settings, + backgroundColor: 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), + ], ), - const SizedBox(height: Spacing.md), - _buildLanguageSection( - context, - ref, - currentLanguageCode, - languageLabel, + ), + 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), + ], + ), ), - const SizedBox(height: Spacing.xl), - _buildSttSection(context, ref, settings), - const SizedBox(height: Spacing.xl), - _buildTtsDropdownSection(context, ref, settings), - const SizedBox(height: Spacing.xl), - _buildChatSection(context, ref, settings), - const SizedBox(height: Spacing.xl), - _buildSocketHealthSection(context, ref), - ], + ), ), ), + body: ListView( + physics: const BouncingScrollPhysics( + parent: AlwaysScrollableScrollPhysics(), + ), + padding: EdgeInsets.fromLTRB( + Spacing.pagePadding, + topPadding, + Spacing.pagePadding, + Spacing.pagePadding + MediaQuery.of(context).padding.bottom, + ), + children: [ + _buildThemesDropdownSection( + context, + ref, + themeMode, + themeDescription, + activeTheme, + settings, + ), + const SizedBox(height: Spacing.md), + _buildLanguageSection( + context, + ref, + currentLanguageCode, + languageLabel, + ), + const SizedBox(height: Spacing.xl), + _buildSttSection(context, ref, settings), + const SizedBox(height: Spacing.xl), + _buildTtsDropdownSection(context, ref, settings), + const SizedBox(height: Spacing.xl), + _buildChatSection(context, ref, settings), + const SizedBox(height: Spacing.xl), + _buildSocketHealthSection(context, ref), + ], + ), ); } - PreferredSizeWidget _buildAppBar(BuildContext context) { - final canPop = ModalRoute.of(context)?.canPop ?? false; - return AppBar( - backgroundColor: context.sidebarTheme.background, - surfaceTintColor: Colors.transparent, - elevation: Elevation.none, - toolbarHeight: kToolbarHeight, - automaticallyImplyLeading: false, - leading: canPop - ? IconButton( - icon: Icon( - UiUtils.platformIcon( - ios: CupertinoIcons.back, - android: Icons.arrow_back, - ), - color: context.conduitTheme.iconPrimary, + 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), ), - onPressed: () => Navigator.of(context).maybePop(), - tooltip: AppLocalizations.of(context)!.back, - ) - : null, - titleSpacing: 0, - title: Text( - AppLocalizations.of(context)!.appCustomization, - style: AppTypography.headlineSmallStyle.copyWith( - color: context.conduitTheme.textPrimary, - fontWeight: FontWeight.w600, + 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, ), ), - centerTitle: true, ); } diff --git a/lib/features/profile/views/profile_page.dart b/lib/features/profile/views/profile_page.dart index 8937bc4..9acd2c4 100644 --- a/lib/features/profile/views/profile_page.dart +++ b/lib/features/profile/views/profile_page.dart @@ -9,6 +9,7 @@ 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'; @@ -66,52 +67,162 @@ 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: context.sidebarTheme.background, - appBar: _buildAppBar(context), + backgroundColor: 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), + ], + ), + ), + ), + ), + ), body: body, ); } - PreferredSizeWidget _buildAppBar(BuildContext context) { - final canPop = ModalRoute.of(context)?.canPop ?? false; - return AppBar( - backgroundColor: context.sidebarTheme.background, - surfaceTintColor: Colors.transparent, - elevation: Elevation.none, - toolbarHeight: kToolbarHeight, - automaticallyImplyLeading: false, - leading: canPop - ? IconButton( - icon: Icon( - UiUtils.platformIcon( - ios: CupertinoIcons.back, - android: Icons.arrow_back, - ), - color: context.conduitTheme.iconPrimary, + 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), ), - onPressed: () => Navigator.of(context).maybePop(), - tooltip: AppLocalizations.of(context)!.back, - ) - : null, - titleSpacing: 0, - title: Text( - AppLocalizations.of(context)!.you, - style: AppTypography.headlineSmallStyle.copyWith( - color: context.conduitTheme.textPrimary, - fontWeight: FontWeight.w600, + 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, ), ), - centerTitle: true, ); } Widget _buildCenteredState(BuildContext context, Widget child) { - return SafeArea( - child: Padding( - padding: const EdgeInsets.all(Spacing.pagePadding), - child: Center(child: child), + final topPadding = MediaQuery.of(context).padding.top + kToolbarHeight + 24; + return Padding( + padding: EdgeInsets.fromLTRB( + Spacing.pagePadding, + topPadding, + Spacing.pagePadding, + Spacing.pagePadding + MediaQuery.of(context).padding.bottom, ), + child: Center(child: child), ); } @@ -121,23 +232,26 @@ class ProfilePage extends ConsumerWidget { dynamic userData, ApiService? api, ) { - return SafeArea( - child: ListView( - physics: const BouncingScrollPhysics( - parent: AlwaysScrollableScrollPhysics(), - ), - padding: const EdgeInsets.symmetric( - horizontal: Spacing.pagePadding, - vertical: Spacing.pagePadding, - ), - children: [ - _buildProfileHeader(context, userData, api), - const SizedBox(height: Spacing.xl), - _buildAccountSection(context, ref), - const SizedBox(height: Spacing.xl), - _buildSupportSection(context), - ], + // Calculate top padding to account for app bar + safe area + final topPadding = MediaQuery.of(context).padding.top + kToolbarHeight + 24; + + return ListView( + physics: const BouncingScrollPhysics( + parent: AlwaysScrollableScrollPhysics(), ), + padding: EdgeInsets.fromLTRB( + Spacing.pagePadding, + topPadding, + Spacing.pagePadding, + Spacing.pagePadding + MediaQuery.of(context).padding.bottom, + ), + children: [ + _buildProfileHeader(context, userData, api), + const SizedBox(height: Spacing.xl), + _buildAccountSection(context, ref), + const SizedBox(height: Spacing.xl), + _buildSupportSection(context), + ], ); }