feat(note_editor): Refactor note editor UI with improved layout and styling
This commit is contained in:
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user