refactor: visual tweaks
This commit is contained in:
@@ -38,6 +38,7 @@ import '../../../shared/widgets/sheet_handle.dart';
|
|||||||
import '../../../shared/widgets/measure_size.dart';
|
import '../../../shared/widgets/measure_size.dart';
|
||||||
import '../../../shared/widgets/conduit_components.dart';
|
import '../../../shared/widgets/conduit_components.dart';
|
||||||
import '../../../shared/widgets/middle_ellipsis_text.dart';
|
import '../../../shared/widgets/middle_ellipsis_text.dart';
|
||||||
|
import '../../../shared/widgets/modal_safe_area.dart';
|
||||||
import '../../../core/services/settings_service.dart';
|
import '../../../core/services/settings_service.dart';
|
||||||
// Removed unused PlatformUtils import
|
// Removed unused PlatformUtils import
|
||||||
import '../../../core/services/platform_service.dart' as ps;
|
import '../../../core/services/platform_service.dart' as ps;
|
||||||
@@ -1654,19 +1655,24 @@ class _ModelSelectorSheetState extends ConsumerState<_ModelSelectorSheet> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _filterModels(String query) {
|
void _filterModels(String query) {
|
||||||
// Debounce for fast search
|
setState(() => _searchQuery = query);
|
||||||
|
|
||||||
_searchDebounce?.cancel();
|
_searchDebounce?.cancel();
|
||||||
_searchDebounce = Timer(const Duration(milliseconds: 160), () {
|
_searchDebounce = Timer(const Duration(milliseconds: 160), () {
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
final normalized = query.trim().toLowerCase();
|
||||||
|
Iterable<Model> list = widget.models;
|
||||||
|
|
||||||
|
if (normalized.isNotEmpty) {
|
||||||
|
list = list.where((model) {
|
||||||
|
final name = model.name.toLowerCase();
|
||||||
|
final id = model.id.toLowerCase();
|
||||||
|
return name.contains(normalized) || id.contains(normalized);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_searchQuery = query.toLowerCase();
|
|
||||||
Iterable<Model> list = widget.models;
|
|
||||||
if (_searchQuery.isNotEmpty) {
|
|
||||||
list = list.where((model) {
|
|
||||||
return model.name.toLowerCase().contains(_searchQuery) ||
|
|
||||||
model.id.toLowerCase().contains(_searchQuery);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// No capability filters
|
|
||||||
_filteredModels = list.toList();
|
_filteredModels = list.toList();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1674,149 +1680,190 @@ class _ModelSelectorSheetState extends ConsumerState<_ModelSelectorSheet> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return DraggableScrollableSheet(
|
return Stack(
|
||||||
initialChildSize: 0.75,
|
children: [
|
||||||
maxChildSize: 0.92,
|
Positioned.fill(
|
||||||
minChildSize: 0.45,
|
child: GestureDetector(
|
||||||
builder: (context, scrollController) {
|
behavior: HitTestBehavior.opaque,
|
||||||
return Container(
|
onTap: () => Navigator.of(context).maybePop(),
|
||||||
decoration: BoxDecoration(
|
child: const SizedBox.shrink(),
|
||||||
color: context.conduitTheme.surfaceBackground,
|
|
||||||
borderRadius: const BorderRadius.vertical(
|
|
||||||
top: Radius.circular(AppBorderRadius.bottomSheet),
|
|
||||||
),
|
|
||||||
border: Border.all(
|
|
||||||
color: context.conduitTheme.dividerColor,
|
|
||||||
width: BorderWidth.regular,
|
|
||||||
),
|
|
||||||
boxShadow: ConduitShadows.modal,
|
|
||||||
),
|
),
|
||||||
child: SafeArea(
|
),
|
||||||
top: false,
|
DraggableScrollableSheet(
|
||||||
bottom: true,
|
expand: false,
|
||||||
child: Padding(
|
initialChildSize: 0.75,
|
||||||
padding: const EdgeInsets.all(Spacing.bottomSheetPadding),
|
maxChildSize: 0.92,
|
||||||
child: Column(
|
minChildSize: 0.45,
|
||||||
children: [
|
builder: (context, scrollController) {
|
||||||
// Handle bar (standardized)
|
return Container(
|
||||||
const SheetHandle(),
|
decoration: BoxDecoration(
|
||||||
|
color: context.conduitTheme.surfaceBackground,
|
||||||
|
borderRadius: const BorderRadius.vertical(
|
||||||
|
top: Radius.circular(AppBorderRadius.bottomSheet),
|
||||||
|
),
|
||||||
|
border: Border.all(
|
||||||
|
color: context.conduitTheme.dividerColor,
|
||||||
|
width: BorderWidth.regular,
|
||||||
|
),
|
||||||
|
boxShadow: ConduitShadows.modal,
|
||||||
|
),
|
||||||
|
child: ModalSheetSafeArea(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: Spacing.modalPadding,
|
||||||
|
vertical: Spacing.modalPadding,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Handle bar (standardized)
|
||||||
|
const SheetHandle(),
|
||||||
|
|
||||||
// Search field
|
// Search field
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: Spacing.md),
|
padding: const EdgeInsets.only(bottom: Spacing.md),
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _searchController,
|
controller: _searchController,
|
||||||
style: TextStyle(color: context.conduitTheme.textPrimary),
|
style: AppTypography.standard.copyWith(
|
||||||
decoration: InputDecoration(
|
color: context.conduitTheme.textPrimary,
|
||||||
hintText: AppLocalizations.of(context)!.searchModels,
|
|
||||||
hintStyle: TextStyle(
|
|
||||||
color: context.conduitTheme.inputPlaceholder,
|
|
||||||
),
|
),
|
||||||
prefixIcon: Icon(
|
onChanged: _filterModels,
|
||||||
Platform.isIOS ? CupertinoIcons.search : Icons.search,
|
decoration: InputDecoration(
|
||||||
color: context.conduitTheme.iconSecondary,
|
isDense: true,
|
||||||
),
|
hintText: AppLocalizations.of(context)!.searchModels,
|
||||||
filled: true,
|
hintStyle: AppTypography.standard.copyWith(
|
||||||
fillColor: context.conduitTheme.inputBackground,
|
color: context.conduitTheme.inputPlaceholder,
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(
|
|
||||||
AppBorderRadius.md,
|
|
||||||
),
|
),
|
||||||
borderSide: BorderSide.none,
|
prefixIcon: Icon(
|
||||||
),
|
Platform.isIOS
|
||||||
enabledBorder: OutlineInputBorder(
|
? CupertinoIcons.search
|
||||||
borderRadius: BorderRadius.circular(
|
: Icons.search,
|
||||||
AppBorderRadius.md,
|
color: context.conduitTheme.iconSecondary,
|
||||||
|
size: IconSize.input,
|
||||||
),
|
),
|
||||||
borderSide: BorderSide(
|
prefixIconConstraints: const BoxConstraints(
|
||||||
color: context.conduitTheme.inputBorder,
|
minWidth: TouchTarget.minimum,
|
||||||
width: 1,
|
minHeight: TouchTarget.minimum,
|
||||||
),
|
),
|
||||||
),
|
suffixIcon: _searchQuery.isNotEmpty
|
||||||
focusedBorder: OutlineInputBorder(
|
? IconButton(
|
||||||
borderRadius: BorderRadius.circular(
|
onPressed: () {
|
||||||
AppBorderRadius.md,
|
_searchController.clear();
|
||||||
|
_filterModels('');
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
Platform.isIOS
|
||||||
|
? CupertinoIcons.clear_circled_solid
|
||||||
|
: Icons.clear,
|
||||||
|
color: context.conduitTheme.iconSecondary,
|
||||||
|
size: IconSize.input,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
suffixIconConstraints: const BoxConstraints(
|
||||||
|
minWidth: TouchTarget.minimum,
|
||||||
|
minHeight: TouchTarget.minimum,
|
||||||
),
|
),
|
||||||
borderSide: BorderSide(
|
filled: true,
|
||||||
color: context.conduitTheme.buttonPrimary,
|
fillColor: context.conduitTheme.inputBackground,
|
||||||
width: 1,
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
AppBorderRadius.md,
|
||||||
|
),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
AppBorderRadius.md,
|
||||||
|
),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: context.conduitTheme.inputBorder,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
AppBorderRadius.md,
|
||||||
|
),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: context.conduitTheme.buttonPrimary,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: Spacing.md,
|
||||||
|
vertical: Spacing.xs,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: Spacing.md,
|
|
||||||
vertical: Spacing.md,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onChanged: _filterModels,
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
|
|
||||||
// Removed capability filters
|
// Removed capability filters
|
||||||
const SizedBox(height: Spacing.sm),
|
const SizedBox(height: Spacing.sm),
|
||||||
|
|
||||||
// Models list
|
// Models list
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Scrollbar(
|
child: Scrollbar(
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
child: _filteredModels.isEmpty
|
child: _filteredModels.isEmpty
|
||||||
? Center(
|
? Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
Platform.isIOS
|
Platform.isIOS
|
||||||
? CupertinoIcons.search_circle
|
? CupertinoIcons.search_circle
|
||||||
: Icons.search_off,
|
: Icons.search_off,
|
||||||
size: 48,
|
size: 48,
|
||||||
color: context.conduitTheme.iconSecondary,
|
color: context.conduitTheme.iconSecondary,
|
||||||
),
|
|
||||||
const SizedBox(height: Spacing.md),
|
|
||||||
Text(
|
|
||||||
'No results',
|
|
||||||
style: TextStyle(
|
|
||||||
color: context.conduitTheme.textSecondary,
|
|
||||||
fontSize: AppTypography.bodyLarge,
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: Spacing.md),
|
||||||
],
|
Text(
|
||||||
),
|
'No results',
|
||||||
)
|
style: TextStyle(
|
||||||
: ListView.builder(
|
color:
|
||||||
controller: scrollController,
|
context.conduitTheme.textSecondary,
|
||||||
padding: EdgeInsets.zero,
|
fontSize: AppTypography.bodyLarge,
|
||||||
itemCount: _filteredModels.length,
|
),
|
||||||
itemBuilder: (context, index) {
|
),
|
||||||
final model = _filteredModels[index];
|
],
|
||||||
final isSelected =
|
),
|
||||||
widget.ref
|
)
|
||||||
.watch(selectedModelProvider)
|
: ListView.builder(
|
||||||
?.id ==
|
controller: scrollController,
|
||||||
model.id;
|
padding: EdgeInsets.zero,
|
||||||
|
itemCount: _filteredModels.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final model = _filteredModels[index];
|
||||||
|
final isSelected =
|
||||||
|
widget.ref
|
||||||
|
.watch(selectedModelProvider)
|
||||||
|
?.id ==
|
||||||
|
model.id;
|
||||||
|
|
||||||
return _buildModelListTile(
|
return _buildModelListTile(
|
||||||
model: model,
|
model: model,
|
||||||
isSelected: isSelected,
|
isSelected: isSelected,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
HapticFeedback.selectionClick();
|
HapticFeedback.selectionClick();
|
||||||
widget.ref
|
widget.ref
|
||||||
.read(
|
.read(
|
||||||
selectedModelProvider.notifier,
|
selectedModelProvider.notifier,
|
||||||
)
|
)
|
||||||
.state =
|
.state =
|
||||||
model;
|
model;
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
);
|
),
|
||||||
},
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'dart:io' show Platform;
|
import 'dart:io' show Platform;
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
import 'dart:math' as math;
|
||||||
import '../providers/chat_providers.dart';
|
import '../providers/chat_providers.dart';
|
||||||
import '../../tools/providers/tools_providers.dart';
|
import '../../tools/providers/tools_providers.dart';
|
||||||
import '../../../core/models/tool.dart';
|
import '../../../core/models/tool.dart';
|
||||||
@@ -19,6 +20,7 @@ import '../../chat/services/voice_input_service.dart';
|
|||||||
|
|
||||||
import '../../../shared/utils/platform_utils.dart';
|
import '../../../shared/utils/platform_utils.dart';
|
||||||
import 'package:conduit/l10n/app_localizations.dart';
|
import 'package:conduit/l10n/app_localizations.dart';
|
||||||
|
import '../../../shared/widgets/modal_safe_area.dart';
|
||||||
|
|
||||||
class _SendMessageIntent extends Intent {
|
class _SendMessageIntent extends Intent {
|
||||||
const _SendMessageIntent();
|
const _SendMessageIntent();
|
||||||
@@ -1044,6 +1046,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
|
isScrollControlled: true,
|
||||||
builder: (modalContext) => Consumer(
|
builder: (modalContext) => Consumer(
|
||||||
builder: (innerContext, modalRef, _) {
|
builder: (innerContext, modalRef, _) {
|
||||||
final l10n = AppLocalizations.of(innerContext)!;
|
final l10n = AppLocalizations.of(innerContext)!;
|
||||||
@@ -1188,34 +1191,87 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
..add(_buildSectionLabel(l10n.tools))
|
..add(_buildSectionLabel(l10n.tools))
|
||||||
..add(toolsSection);
|
..add(toolsSection);
|
||||||
|
|
||||||
return Container(
|
// Measure content height and cap the sheet's max size to avoid extra blank space
|
||||||
decoration: BoxDecoration(
|
final GlobalKey sheetContentKey = GlobalKey();
|
||||||
color: theme.surfaceBackground,
|
double? measuredContentHeight;
|
||||||
borderRadius: const BorderRadius.vertical(
|
|
||||||
top: Radius.circular(AppBorderRadius.bottomSheet),
|
return StatefulBuilder(
|
||||||
),
|
builder: (context, setModalState) {
|
||||||
border: Border.all(
|
// Schedule a post-frame measurement of the content height
|
||||||
color: theme.dividerColor,
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
width: BorderWidth.thin,
|
final ctx = sheetContentKey.currentContext;
|
||||||
),
|
if (ctx != null) {
|
||||||
boxShadow: ConduitShadows.modal,
|
final renderObject = ctx.findRenderObject();
|
||||||
),
|
if (renderObject is RenderBox) {
|
||||||
child: SafeArea(
|
final double h = renderObject.size.height;
|
||||||
top: false,
|
if (h > 0 && h != measuredContentHeight) {
|
||||||
bottom: true,
|
measuredContentHeight = h;
|
||||||
child: SingleChildScrollView(
|
setModalState(() {});
|
||||||
padding: const EdgeInsets.fromLTRB(
|
}
|
||||||
Spacing.modalPadding,
|
}
|
||||||
Spacing.sm,
|
}
|
||||||
Spacing.modalPadding,
|
});
|
||||||
Spacing.modalPadding,
|
|
||||||
),
|
final media = MediaQuery.of(modalContext);
|
||||||
child: Column(
|
final double availableHeight =
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
media.size.height - media.padding.top;
|
||||||
children: bodyChildren,
|
|
||||||
),
|
double computedMax = 0.9;
|
||||||
),
|
if (measuredContentHeight != null && availableHeight > 0) {
|
||||||
),
|
computedMax = (measuredContentHeight! / availableHeight).clamp(
|
||||||
|
0.1,
|
||||||
|
0.9,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final double computedMin = math.min(0.25, computedMax);
|
||||||
|
final double computedInitial = math.min(0.4, computedMax);
|
||||||
|
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onTap: () => Navigator.of(modalContext).maybePop(),
|
||||||
|
child: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DraggableScrollableSheet(
|
||||||
|
expand: false,
|
||||||
|
initialChildSize: computedInitial,
|
||||||
|
minChildSize: computedMin,
|
||||||
|
maxChildSize: computedMax,
|
||||||
|
snap: true,
|
||||||
|
snapSizes: [computedMax],
|
||||||
|
builder: (sheetContext, scrollController) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.surfaceBackground,
|
||||||
|
borderRadius: const BorderRadius.vertical(
|
||||||
|
top: Radius.circular(AppBorderRadius.bottomSheet),
|
||||||
|
),
|
||||||
|
border: Border.all(
|
||||||
|
color: theme.dividerColor,
|
||||||
|
width: BorderWidth.thin,
|
||||||
|
),
|
||||||
|
boxShadow: ConduitShadows.modal,
|
||||||
|
),
|
||||||
|
child: ModalSheetSafeArea(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
controller: scrollController,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
child: Column(
|
||||||
|
key: sheetContentKey,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: bodyChildren,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -1308,7 +1364,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
boxShadow: value ? ConduitShadows.low : const [],
|
boxShadow: value ? ConduitShadows.low : const [],
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
_buildToolGlyph(icon: icon, selected: value, theme: theme),
|
_buildToolGlyph(icon: icon, selected: value, theme: theme),
|
||||||
const SizedBox(width: Spacing.sm),
|
const SizedBox(width: Spacing.sm),
|
||||||
@@ -1316,23 +1372,14 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Text(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
title,
|
||||||
children: [
|
style: AppTypography.bodySmallStyle.copyWith(
|
||||||
Expanded(
|
color: theme.textPrimary,
|
||||||
child: Text(
|
fontWeight: value ? FontWeight.w600 : FontWeight.w500,
|
||||||
title,
|
),
|
||||||
style: AppTypography.bodyLargeStyle.copyWith(
|
maxLines: 1,
|
||||||
color: theme.textPrimary,
|
overflow: TextOverflow.ellipsis,
|
||||||
fontWeight: value
|
|
||||||
? FontWeight.w600
|
|
||||||
: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: Spacing.xs),
|
|
||||||
_buildTogglePill(isOn: value, theme: theme),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
if (description.isNotEmpty) ...[
|
if (description.isNotEmpty) ...[
|
||||||
const SizedBox(height: Spacing.xs),
|
const SizedBox(height: Spacing.xs),
|
||||||
@@ -1340,7 +1387,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
description,
|
description,
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: AppTypography.bodySmallStyle.copyWith(
|
style: AppTypography.captionStyle.copyWith(
|
||||||
color: theme.textSecondary.withValues(
|
color: theme.textSecondary.withValues(
|
||||||
alpha: Alpha.strong,
|
alpha: Alpha.strong,
|
||||||
),
|
),
|
||||||
@@ -1350,6 +1397,8 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: Spacing.sm),
|
||||||
|
_buildTogglePill(isOn: value, theme: theme),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1402,7 +1451,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
boxShadow: selected ? ConduitShadows.low : const [],
|
boxShadow: selected ? ConduitShadows.low : const [],
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
_buildToolGlyph(
|
_buildToolGlyph(
|
||||||
icon: _toolIconFor(tool),
|
icon: _toolIconFor(tool),
|
||||||
@@ -1414,23 +1463,16 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Text(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
tool.name,
|
||||||
children: [
|
style: AppTypography.bodySmallStyle.copyWith(
|
||||||
Expanded(
|
color: theme.textPrimary,
|
||||||
child: Text(
|
fontWeight: selected
|
||||||
tool.name,
|
? FontWeight.w600
|
||||||
style: AppTypography.bodyLargeStyle.copyWith(
|
: FontWeight.w500,
|
||||||
color: theme.textPrimary,
|
),
|
||||||
fontWeight: selected
|
maxLines: 1,
|
||||||
? FontWeight.w600
|
overflow: TextOverflow.ellipsis,
|
||||||
: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: Spacing.xs),
|
|
||||||
_buildTogglePill(isOn: selected, theme: theme),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
if (description.isNotEmpty) ...[
|
if (description.isNotEmpty) ...[
|
||||||
const SizedBox(height: Spacing.xs),
|
const SizedBox(height: Spacing.xs),
|
||||||
@@ -1438,7 +1480,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
description,
|
description,
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: AppTypography.bodySmallStyle.copyWith(
|
style: AppTypography.captionStyle.copyWith(
|
||||||
color: theme.textSecondary.withValues(
|
color: theme.textSecondary.withValues(
|
||||||
alpha: Alpha.strong,
|
alpha: Alpha.strong,
|
||||||
),
|
),
|
||||||
@@ -1448,6 +1490,8 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: Spacing.sm),
|
||||||
|
_buildTogglePill(isOn: selected, theme: theme),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
|
|
||||||
import '../../../core/providers/app_providers.dart';
|
import '../../../core/providers/app_providers.dart';
|
||||||
import '../../../shared/theme/theme_extensions.dart';
|
import '../../../shared/theme/theme_extensions.dart';
|
||||||
|
import '../../../shared/widgets/modal_safe_area.dart';
|
||||||
import '../../chat/providers/chat_providers.dart' as chat;
|
import '../../chat/providers/chat_providers.dart' as chat;
|
||||||
// import '../../files/views/files_page.dart';
|
// import '../../files/views/files_page.dart';
|
||||||
import '../../profile/views/profile_page.dart';
|
import '../../profile/views/profile_page.dart';
|
||||||
@@ -798,6 +799,9 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
String folderName,
|
String folderName,
|
||||||
) {
|
) {
|
||||||
final theme = context.conduitTheme;
|
final theme = context.conduitTheme;
|
||||||
|
// Ensure consistent modal padding/insets across the app
|
||||||
|
// ignore: unnecessary_import
|
||||||
|
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
backgroundColor: theme.surfaceBackground,
|
backgroundColor: theme.surfaceBackground,
|
||||||
@@ -807,7 +811,11 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
builder: (sheetContext) {
|
builder: (sheetContext) {
|
||||||
return SafeArea(
|
return ModalSheetSafeArea(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: Spacing.modalPadding,
|
||||||
|
vertical: Spacing.modalPadding,
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@@ -1329,7 +1337,11 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
builder: (sheetContext) {
|
builder: (sheetContext) {
|
||||||
return SafeArea(
|
return ModalSheetSafeArea(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: Spacing.modalPadding,
|
||||||
|
vertical: Spacing.modalPadding,
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import 'dart:async';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import '../../chat/views/chat_page_helpers.dart';
|
import '../../chat/views/chat_page_helpers.dart';
|
||||||
import 'app_customization_page.dart';
|
import 'app_customization_page.dart';
|
||||||
|
import '../../../shared/widgets/modal_safe_area.dart';
|
||||||
|
|
||||||
/// Profile page (You tab) showing user info and main actions
|
/// Profile page (You tab) showing user info and main actions
|
||||||
/// Enhanced with production-grade design tokens for better cohesion
|
/// Enhanced with production-grade design tokens for better cohesion
|
||||||
@@ -236,7 +237,9 @@ class ProfilePage extends ConsumerWidget {
|
|||||||
android: Icons.tune,
|
android: Icons.tune,
|
||||||
),
|
),
|
||||||
title: AppLocalizations.of(context)!.appCustomization,
|
title: AppLocalizations.of(context)!.appCustomization,
|
||||||
subtitle: AppLocalizations.of(context)!.appCustomizationSubtitle,
|
subtitle: AppLocalizations.of(
|
||||||
|
context,
|
||||||
|
)!.appCustomizationSubtitle,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).push(
|
Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
@@ -334,7 +337,10 @@ class ProfilePage extends ConsumerWidget {
|
|||||||
(m) => m.id == settings.defaultModel,
|
(m) => m.id == settings.defaultModel,
|
||||||
orElse: () => models.isNotEmpty
|
orElse: () => models.isNotEmpty
|
||||||
? models.first
|
? models.first
|
||||||
: Model(id: 'none', name: AppLocalizations.of(context)!.noModelsAvailable),
|
: Model(
|
||||||
|
id: 'none',
|
||||||
|
name: AppLocalizations.of(context)!.noModelsAvailable,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
@@ -498,7 +504,9 @@ class ProfilePage extends ConsumerWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
AppLocalizations.of(ctx)!.versionLabel(info.version, info.buildNumber),
|
AppLocalizations.of(
|
||||||
|
ctx,
|
||||||
|
)!.versionLabel(info.version, info.buildNumber),
|
||||||
style: ctx.conduitTheme.bodyMedium?.copyWith(
|
style: ctx.conduitTheme.bodyMedium?.copyWith(
|
||||||
color: ctx.conduitTheme.textSecondary,
|
color: ctx.conduitTheme.textSecondary,
|
||||||
),
|
),
|
||||||
@@ -544,7 +552,10 @@ class ProfilePage extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
UiUtils.showMessage(context, AppLocalizations.of(context)!.unableToLoadAppInfo);
|
UiUtils.showMessage(
|
||||||
|
context,
|
||||||
|
AppLocalizations.of(context)!.unableToLoadAppInfo,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -647,10 +658,7 @@ class _DefaultModelBottomSheetState
|
|||||||
// If no default model is set (null), default to auto-select
|
// If no default model is set (null), default to auto-select
|
||||||
_selectedModelId = widget.currentDefaultModelId ?? 'auto-select';
|
_selectedModelId = widget.currentDefaultModelId ?? 'auto-select';
|
||||||
// Add auto-select as first item
|
// Add auto-select as first item
|
||||||
_filteredModels = [
|
_filteredModels = _allModels();
|
||||||
const Model(id: 'auto-select', name: 'Auto-select'),
|
|
||||||
...widget.models,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -660,215 +668,264 @@ class _DefaultModelBottomSheetState
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<Model> _allModels() {
|
||||||
|
return [
|
||||||
|
const Model(id: 'auto-select', name: 'Auto-select'),
|
||||||
|
...widget.models,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
void _filterModels(String query) {
|
void _filterModels(String query) {
|
||||||
|
setState(() => _searchQuery = query);
|
||||||
|
|
||||||
_searchDebounce?.cancel();
|
_searchDebounce?.cancel();
|
||||||
_searchDebounce = Timer(const Duration(milliseconds: 160), () {
|
_searchDebounce = Timer(const Duration(milliseconds: 160), () {
|
||||||
setState(() {
|
if (!mounted) return;
|
||||||
_searchQuery = query.toLowerCase();
|
|
||||||
List<Model> allModels = [
|
|
||||||
const Model(id: 'auto-select', name: 'Auto-select'),
|
|
||||||
...widget.models,
|
|
||||||
];
|
|
||||||
|
|
||||||
if (_searchQuery.isNotEmpty) {
|
final normalized = query.trim().toLowerCase();
|
||||||
_filteredModels = allModels.where((model) {
|
final allModels = _allModels();
|
||||||
return model.name.toLowerCase().contains(_searchQuery) ||
|
final filtered = normalized.isEmpty
|
||||||
model.id.toLowerCase().contains(_searchQuery);
|
? allModels
|
||||||
}).toList();
|
: allModels.where((model) {
|
||||||
} else {
|
final name = model.name.toLowerCase();
|
||||||
_filteredModels = allModels;
|
final id = model.id.toLowerCase();
|
||||||
}
|
return name.contains(normalized) || id.contains(normalized);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_filteredModels = filtered;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return DraggableScrollableSheet(
|
return Stack(
|
||||||
initialChildSize: 0.75,
|
children: [
|
||||||
maxChildSize: 0.92,
|
Positioned.fill(
|
||||||
minChildSize: 0.45,
|
child: GestureDetector(
|
||||||
builder: (context, scrollController) {
|
behavior: HitTestBehavior.opaque,
|
||||||
return Container(
|
onTap: () => Navigator.of(context).maybePop(),
|
||||||
decoration: BoxDecoration(
|
child: const SizedBox.shrink(),
|
||||||
color: context.conduitTheme.surfaceBackground,
|
|
||||||
borderRadius: const BorderRadius.vertical(
|
|
||||||
top: Radius.circular(AppBorderRadius.bottomSheet),
|
|
||||||
),
|
|
||||||
border: Border.all(
|
|
||||||
color: context.conduitTheme.dividerColor,
|
|
||||||
width: BorderWidth.regular,
|
|
||||||
),
|
|
||||||
boxShadow: ConduitShadows.modal,
|
|
||||||
),
|
),
|
||||||
child: SafeArea(
|
),
|
||||||
top: false,
|
DraggableScrollableSheet(
|
||||||
bottom: true,
|
expand: false,
|
||||||
child: Padding(
|
initialChildSize: 0.75,
|
||||||
padding: const EdgeInsets.all(Spacing.bottomSheetPadding),
|
maxChildSize: 0.92,
|
||||||
child: Column(
|
minChildSize: 0.45,
|
||||||
children: [
|
builder: (context, scrollController) {
|
||||||
// Handle bar (standardized)
|
return Container(
|
||||||
const SheetHandle(),
|
decoration: BoxDecoration(
|
||||||
|
color: context.conduitTheme.surfaceBackground,
|
||||||
|
borderRadius: const BorderRadius.vertical(
|
||||||
|
top: Radius.circular(AppBorderRadius.bottomSheet),
|
||||||
|
),
|
||||||
|
border: Border.all(
|
||||||
|
color: context.conduitTheme.dividerColor,
|
||||||
|
width: BorderWidth.regular,
|
||||||
|
),
|
||||||
|
boxShadow: ConduitShadows.modal,
|
||||||
|
),
|
||||||
|
child: ModalSheetSafeArea(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: Spacing.modalPadding,
|
||||||
|
vertical: Spacing.modalPadding,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Handle bar (standardized)
|
||||||
|
const SheetHandle(),
|
||||||
|
|
||||||
// Header removed (no icon/title or save button)
|
// Header removed (no icon/title or save button)
|
||||||
const SizedBox(height: Spacing.md),
|
const SizedBox(height: Spacing.md),
|
||||||
|
|
||||||
// Search field
|
// Search field
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: Spacing.md),
|
padding: const EdgeInsets.only(bottom: Spacing.md),
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _searchController,
|
controller: _searchController,
|
||||||
style: TextStyle(color: context.conduitTheme.textPrimary),
|
style: AppTypography.standard.copyWith(
|
||||||
decoration: InputDecoration(
|
color: context.conduitTheme.textPrimary,
|
||||||
hintText: AppLocalizations.of(context)!.searchModels,
|
|
||||||
hintStyle: TextStyle(
|
|
||||||
color: context.conduitTheme.inputPlaceholder,
|
|
||||||
),
|
),
|
||||||
prefixIcon: Icon(
|
onChanged: _filterModels,
|
||||||
Platform.isIOS ? CupertinoIcons.search : Icons.search,
|
decoration: InputDecoration(
|
||||||
color: context.conduitTheme.iconSecondary,
|
isDense: true,
|
||||||
),
|
hintText: AppLocalizations.of(context)!.searchModels,
|
||||||
filled: true,
|
hintStyle: AppTypography.standard.copyWith(
|
||||||
fillColor: context.conduitTheme.inputBackground,
|
color: context.conduitTheme.inputPlaceholder,
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(
|
|
||||||
AppBorderRadius.md,
|
|
||||||
),
|
),
|
||||||
borderSide: BorderSide.none,
|
prefixIcon: Icon(
|
||||||
),
|
Platform.isIOS
|
||||||
enabledBorder: OutlineInputBorder(
|
? CupertinoIcons.search
|
||||||
borderRadius: BorderRadius.circular(
|
: Icons.search,
|
||||||
AppBorderRadius.md,
|
color: context.conduitTheme.iconSecondary,
|
||||||
|
size: IconSize.input,
|
||||||
),
|
),
|
||||||
borderSide: BorderSide(
|
prefixIconConstraints: const BoxConstraints(
|
||||||
color: context.conduitTheme.inputBorder,
|
minWidth: TouchTarget.minimum,
|
||||||
width: 1,
|
minHeight: TouchTarget.minimum,
|
||||||
),
|
),
|
||||||
),
|
suffixIcon: _searchQuery.isNotEmpty
|
||||||
focusedBorder: OutlineInputBorder(
|
? IconButton(
|
||||||
borderRadius: BorderRadius.circular(
|
onPressed: () {
|
||||||
AppBorderRadius.md,
|
_searchController.clear();
|
||||||
|
_filterModels('');
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
Platform.isIOS
|
||||||
|
? CupertinoIcons.clear_circled_solid
|
||||||
|
: Icons.clear,
|
||||||
|
color: context.conduitTheme.iconSecondary,
|
||||||
|
size: IconSize.input,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
suffixIconConstraints: const BoxConstraints(
|
||||||
|
minWidth: TouchTarget.minimum,
|
||||||
|
minHeight: TouchTarget.minimum,
|
||||||
),
|
),
|
||||||
borderSide: BorderSide(
|
filled: true,
|
||||||
color: context.conduitTheme.buttonPrimary,
|
fillColor: context.conduitTheme.inputBackground,
|
||||||
width: 1,
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
AppBorderRadius.md,
|
||||||
|
),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
AppBorderRadius.md,
|
||||||
|
),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: context.conduitTheme.inputBorder,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
AppBorderRadius.md,
|
||||||
|
),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: context.conduitTheme.buttonPrimary,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: Spacing.md,
|
||||||
|
vertical: Spacing.xs,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: Spacing.md,
|
|
||||||
vertical: Spacing.md,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onChanged: _filterModels,
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
|
|
||||||
// Section header (cohesive with Chats Drawer)
|
// Section header (cohesive with Chats Drawer)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: Spacing.sm),
|
padding: const EdgeInsets.only(bottom: Spacing.sm),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
AppLocalizations.of(context)!.availableModels,
|
AppLocalizations.of(context)!.availableModels,
|
||||||
style: AppTypography.bodySmallStyle.copyWith(
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: context.conduitTheme.textSecondary,
|
|
||||||
letterSpacing: 0.2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: Spacing.xs),
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 6,
|
|
||||||
vertical: 2,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: context.conduitTheme.surfaceBackground
|
|
||||||
.withValues(alpha: 0.6),
|
|
||||||
borderRadius: BorderRadius.circular(
|
|
||||||
AppBorderRadius.xs,
|
|
||||||
),
|
|
||||||
border: Border.all(
|
|
||||||
color: context.conduitTheme.dividerColor,
|
|
||||||
width: BorderWidth.thin,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
'${_filteredModels.length}',
|
|
||||||
style: AppTypography.bodySmallStyle.copyWith(
|
style: AppTypography.bodySmallStyle.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
color: context.conduitTheme.textSecondary,
|
color: context.conduitTheme.textSecondary,
|
||||||
|
letterSpacing: 0.2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: Spacing.xs),
|
||||||
],
|
Container(
|
||||||
),
|
padding: const EdgeInsets.symmetric(
|
||||||
),
|
horizontal: 6,
|
||||||
|
vertical: 2,
|
||||||
const SizedBox(height: Spacing.sm),
|
|
||||||
|
|
||||||
// Models list
|
|
||||||
Expanded(
|
|
||||||
child: Scrollbar(
|
|
||||||
controller: scrollController,
|
|
||||||
child: _filteredModels.isEmpty
|
|
||||||
? Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Platform.isIOS
|
|
||||||
? CupertinoIcons.search_circle
|
|
||||||
: Icons.search_off,
|
|
||||||
size: 48,
|
|
||||||
color: context.conduitTheme.iconSecondary,
|
|
||||||
),
|
|
||||||
const SizedBox(height: Spacing.md),
|
|
||||||
Text(
|
|
||||||
AppLocalizations.of(context)!.noResults,
|
|
||||||
style: TextStyle(
|
|
||||||
color: context.conduitTheme.textSecondary,
|
|
||||||
fontSize: AppTypography.bodyLarge,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: ListView.builder(
|
|
||||||
controller: scrollController,
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
itemCount: _filteredModels.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final model = _filteredModels[index];
|
|
||||||
final isAutoSelect = model.id == 'auto-select';
|
|
||||||
final isSelected = isAutoSelect
|
|
||||||
? _selectedModelId == null ||
|
|
||||||
_selectedModelId == 'auto-select'
|
|
||||||
: _selectedModelId == model.id;
|
|
||||||
|
|
||||||
return _buildModelListTile(
|
|
||||||
model: model,
|
|
||||||
isSelected: isSelected,
|
|
||||||
isAutoSelect: isAutoSelect,
|
|
||||||
onTap: () {
|
|
||||||
HapticFeedback.lightImpact();
|
|
||||||
final selectedId = isAutoSelect
|
|
||||||
? 'auto-select'
|
|
||||||
: model.id;
|
|
||||||
// Return selection immediately; caller handles persisting
|
|
||||||
Navigator.pop(context, selectedId);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.conduitTheme.surfaceBackground
|
||||||
|
.withValues(alpha: 0.6),
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
AppBorderRadius.xs,
|
||||||
|
),
|
||||||
|
border: Border.all(
|
||||||
|
color: context.conduitTheme.dividerColor,
|
||||||
|
width: BorderWidth.thin,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'${_filteredModels.length}',
|
||||||
|
style: AppTypography.bodySmallStyle.copyWith(
|
||||||
|
color: context.conduitTheme.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
const SizedBox(height: Spacing.sm),
|
||||||
|
|
||||||
|
// Models list
|
||||||
|
Expanded(
|
||||||
|
child: Scrollbar(
|
||||||
|
controller: scrollController,
|
||||||
|
child: _filteredModels.isEmpty
|
||||||
|
? Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Platform.isIOS
|
||||||
|
? CupertinoIcons.search_circle
|
||||||
|
: Icons.search_off,
|
||||||
|
size: 48,
|
||||||
|
color: context.conduitTheme.iconSecondary,
|
||||||
|
),
|
||||||
|
const SizedBox(height: Spacing.md),
|
||||||
|
Text(
|
||||||
|
AppLocalizations.of(context)!.noResults,
|
||||||
|
style: TextStyle(
|
||||||
|
color:
|
||||||
|
context.conduitTheme.textSecondary,
|
||||||
|
fontSize: AppTypography.bodyLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: ListView.builder(
|
||||||
|
controller: scrollController,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
itemCount: _filteredModels.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final model = _filteredModels[index];
|
||||||
|
final isAutoSelect =
|
||||||
|
model.id == 'auto-select';
|
||||||
|
final isSelected = isAutoSelect
|
||||||
|
? _selectedModelId == null ||
|
||||||
|
_selectedModelId == 'auto-select'
|
||||||
|
: _selectedModelId == model.id;
|
||||||
|
|
||||||
|
return _buildModelListTile(
|
||||||
|
model: model,
|
||||||
|
isSelected: isSelected,
|
||||||
|
isAutoSelect: isAutoSelect,
|
||||||
|
onTap: () {
|
||||||
|
HapticFeedback.lightImpact();
|
||||||
|
final selectedId = isAutoSelect
|
||||||
|
? 'auto-select'
|
||||||
|
: model.id;
|
||||||
|
Navigator.pop(context, selectedId);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
);
|
),
|
||||||
},
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
37
lib/shared/widgets/modal_safe_area.dart
Normal file
37
lib/shared/widgets/modal_safe_area.dart
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import '../theme/theme_extensions.dart';
|
||||||
|
|
||||||
|
/// Consistent safe area wrapper for modal sheets presented across the app.
|
||||||
|
///
|
||||||
|
/// All modal-bottom sheets should rely on this widget to guarantee that
|
||||||
|
/// system insets (e.g. gesture areas or dynamic island) are respected while
|
||||||
|
/// maintaining the same padding rhythm used by the attachments sheet.
|
||||||
|
class ModalSheetSafeArea extends StatelessWidget {
|
||||||
|
const ModalSheetSafeArea({super.key, required this.child, this.padding});
|
||||||
|
|
||||||
|
/// Content rendered inside the safe area.
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
/// Optional custom padding that wraps the [child]. When omitted the default
|
||||||
|
/// modal spacing used by attachments/chat input is applied.
|
||||||
|
final EdgeInsets? padding;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final resolvedPadding =
|
||||||
|
padding ??
|
||||||
|
const EdgeInsets.fromLTRB(
|
||||||
|
Spacing.modalPadding,
|
||||||
|
Spacing.sm,
|
||||||
|
Spacing.modalPadding,
|
||||||
|
Spacing.modalPadding,
|
||||||
|
);
|
||||||
|
|
||||||
|
return SafeArea(
|
||||||
|
top: false,
|
||||||
|
bottom: true,
|
||||||
|
child: Padding(padding: resolvedPadding, child: child),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user