Files
iiEsaywebUIapp/lib/features/onboarding/views/onboarding_sheet.dart

311 lines
10 KiB
Dart
Raw Normal View History

2025-08-10 01:20:45 +05:30
import 'package:flutter/cupertino.dart';
2025-09-16 16:24:45 +05:30
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
2025-08-10 01:20:45 +05:30
import 'package:flutter_animate/flutter_animate.dart';
import 'package:conduit/l10n/app_localizations.dart';
2025-08-10 01:20:45 +05:30
import '../../auth/providers/unified_auth_providers.dart';
import '../../../core/providers/app_providers.dart';
2025-09-16 16:24:45 +05:30
import '../../../core/utils/user_display_name.dart';
import '../../../shared/theme/theme_extensions.dart';
import '../../../shared/widgets/sheet_handle.dart';
class OnboardingSheet extends ConsumerStatefulWidget {
2025-08-10 01:20:45 +05:30
const OnboardingSheet({super.key});
@override
2025-09-16 16:24:45 +05:30
ConsumerState<OnboardingSheet> createState() => _OnboardingSheetState();
2025-08-10 01:20:45 +05:30
}
2025-09-16 16:24:45 +05:30
class _OnboardingSheetState extends ConsumerState<OnboardingSheet> {
2025-08-10 01:20:45 +05:30
final PageController _controller = PageController();
int _index = 0;
2025-09-16 16:24:45 +05:30
void _next(int pageCount) {
if (_index < pageCount - 1) {
_controller.nextPage(
duration: AnimationDuration.fast,
curve: AnimationCurves.easeInOut,
);
} else {
Navigator.pop(context);
}
}
List<_OnboardingPage> _buildPages(
AppLocalizations l10n,
String greetingName,
) {
return [
_OnboardingPage(
2025-09-16 16:24:45 +05:30
title: l10n.onboardStartTitle(greetingName),
subtitle: l10n.onboardStartSubtitle,
icon: CupertinoIcons.chat_bubble_2,
bullets: [l10n.onboardStartBullet1, l10n.onboardStartBullet2],
),
_OnboardingPage(
title: l10n.onboardAttachTitle,
subtitle: l10n.onboardAttachSubtitle,
icon: CupertinoIcons.doc_on_doc,
bullets: [l10n.onboardAttachBullet1, l10n.onboardAttachBullet2],
),
_OnboardingPage(
title: l10n.onboardSpeakTitle,
subtitle: l10n.onboardSpeakSubtitle,
icon: CupertinoIcons.mic_fill,
bullets: [l10n.onboardSpeakBullet1, l10n.onboardSpeakBullet2],
),
_OnboardingPage(
title: l10n.onboardQuickTitle,
subtitle: l10n.onboardQuickSubtitle,
icon: CupertinoIcons.line_horizontal_3,
bullets: [l10n.onboardQuickBullet1, l10n.onboardQuickBullet2],
),
];
}
2025-08-10 01:20:45 +05:30
@override
Widget build(BuildContext context) {
final height = MediaQuery.of(context).size.height;
2025-09-16 16:24:45 +05:30
final l10n = AppLocalizations.of(context)!;
final authUser = ref.watch(currentUserProvider2);
final asyncUser = ref.watch(currentUserProvider);
final user = asyncUser.maybeWhen(
data: (value) => value ?? authUser,
orElse: () => authUser,
);
2025-09-16 16:24:45 +05:30
final greetingName = deriveUserDisplayName(user);
final pages = _buildPages(l10n, greetingName);
final pageCount = pages.length;
2025-08-10 01:20:45 +05:30
return Container(
height: height * 0.7,
decoration: BoxDecoration(
color: context.conduitTheme.surfaceBackground,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(AppBorderRadius.modal),
),
boxShadow: ConduitShadows.modal(context),
2025-08-10 01:20:45 +05:30
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(Spacing.lg),
child: Column(
children: [
2025-08-22 01:24:04 +05:30
// Handle bar (standardized)
const SheetHandle(),
2025-08-10 01:20:45 +05:30
Expanded(
child: PageView.builder(
controller: _controller,
2025-09-16 16:24:45 +05:30
itemCount: pageCount,
2025-08-10 01:20:45 +05:30
onPageChanged: (i) => setState(() => _index = i),
itemBuilder: (context, i) {
2025-09-16 16:24:45 +05:30
final page = pages[i];
2025-08-10 01:20:45 +05:30
final content = _IllustratedPage(page: page);
// Ensure content can scroll vertically when space is tight,
// while keeping it centered when there is enough space.
return LayoutBuilder(
builder: (context, constraints) {
final centered = ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: Center(child: content),
);
return SingleChildScrollView(
physics: const ClampingScrollPhysics(),
child: centered,
);
},
);
2025-08-10 01:20:45 +05:30
},
),
),
const SizedBox(height: Spacing.md),
Row(
mainAxisAlignment: MainAxisAlignment.center,
2025-09-16 16:24:45 +05:30
children: List.generate(pageCount, (i) {
2025-08-10 01:20:45 +05:30
final active = i == _index;
return AnimatedContainer(
duration: AnimationDuration.fast,
margin: const EdgeInsets.symmetric(horizontal: 4),
height: 6,
width: active ? 20 : 6,
decoration: BoxDecoration(
color: active
? context.conduitTheme.buttonPrimary
: context.conduitTheme.dividerColor,
borderRadius: BorderRadius.circular(
AppBorderRadius.badge,
),
),
);
}),
),
const SizedBox(height: Spacing.lg),
Row(
children: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
2025-09-16 16:24:45 +05:30
l10n.skip,
2025-08-10 01:20:45 +05:30
style: TextStyle(
color: context.conduitTheme.textSecondary,
),
),
),
const Spacer(),
FilledButton(
2025-09-16 16:24:45 +05:30
onPressed: () => _next(pageCount),
2025-08-10 01:20:45 +05:30
style: FilledButton.styleFrom(
backgroundColor: context.conduitTheme.buttonPrimary,
foregroundColor: context.conduitTheme.buttonPrimaryText,
padding: const EdgeInsets.symmetric(
horizontal: Spacing.lg,
vertical: Spacing.sm,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
AppBorderRadius.button,
),
),
),
child: Text(
2025-09-16 16:24:45 +05:30
_index == pageCount - 1 ? l10n.done : l10n.next,
),
2025-08-10 01:20:45 +05:30
),
],
),
],
),
),
),
);
}
}
class _OnboardingPage {
final String title;
final String subtitle;
final IconData icon;
final List<String>? bullets;
const _OnboardingPage({
required this.title,
required this.subtitle,
required this.icon,
this.bullets,
});
}
class _IllustratedPage extends StatelessWidget {
final _OnboardingPage page;
const _IllustratedPage({required this.page});
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Aurora blob illustration
SizedBox(
height: 160,
child: Stack(
alignment: Alignment.center,
children: [
Positioned(top: 10, left: 24, child: _blob(context, 90, 0.18)),
Positioned(
bottom: 0,
right: 16,
child: _blob(context, 130, 0.12),
),
Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: context.conduitTheme.buttonPrimary,
borderRadius: BorderRadius.circular(AppBorderRadius.avatar),
boxShadow: ConduitShadows.glow(context),
2025-08-10 01:20:45 +05:30
),
child: Icon(page.icon, color: context.conduitTheme.textInverse),
).animate().scale(duration: AnimationDuration.fast),
],
),
),
const SizedBox(height: Spacing.lg),
Text(
page.title,
style: TextStyle(
fontSize: AppTypography.headlineMedium,
fontWeight: FontWeight.w700,
color: context.conduitTheme.textPrimary,
),
textAlign: TextAlign.center,
),
const SizedBox(height: Spacing.sm),
Text(
page.subtitle,
style: TextStyle(
fontSize: AppTypography.bodyLarge,
color: context.conduitTheme.textSecondary,
),
textAlign: TextAlign.center,
),
if (page.bullets != null && page.bullets!.isNotEmpty) ...[
const SizedBox(height: Spacing.md),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: page.bullets!
.map(
(b) => Padding(
padding: const EdgeInsets.symmetric(
horizontal: Spacing.lg,
vertical: 4,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 6,
height: 6,
margin: const EdgeInsets.only(top: 8, right: 8),
decoration: BoxDecoration(
color: context.conduitTheme.buttonPrimary,
shape: BoxShape.circle,
),
),
Expanded(
child: Text(
b,
style: TextStyle(
color: context.conduitTheme.textSecondary,
fontSize: AppTypography.bodyMedium,
),
),
),
],
),
),
)
.toList(),
),
],
],
);
}
Widget _blob(BuildContext context, double size, double alpha) {
return Container(
width: size,
height: size,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: context.conduitTheme.buttonPrimary.withValues(alpha: alpha),
boxShadow: ConduitShadows.glow(context),
2025-08-10 01:20:45 +05:30
),
);
}
}