Chat UI redesign, splash screen, and onboarding carousel

- Redesign chat screens (both apps) to match Figma: pink theme with
  doodle pattern background, white app bar with centered name and
  chevron back, rose sender bubbles, white receiver bubbles, entry
  banners, and session-ended bottom bar
- Add splash_chat_hebat.png as native Android splash screen with
  Android 12+ support (values-v31)
- Add Flutter splash screen using splash_chat_hebat.png
- Add onboarding carousel (client_app only): 3 pages with 1s
  auto-advance, last page manual "Mulai" button, first-launch only
- Register image assets in both pubspec.yaml files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-17 22:05:15 +08:00
parent 50d31260dc
commit 97d50a8e08
26 changed files with 547 additions and 175 deletions

View File

@@ -0,0 +1,210 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../router.dart';
const _kOnboardingDone = 'onboarding_done';
const _kPink = Color(0xFFBE7C8A);
class _OnboardingPage {
final String title;
final String text;
final String image;
const _OnboardingPage({
required this.title,
required this.text,
required this.image,
});
}
const _pages = [
_OnboardingPage(
title: 'Langsung Curhat',
text: 'Tidak perlu form panjang atau janji. Masuk dan langsung ngobrol.',
image: 'assets/images/splash/splash_1.png',
),
_OnboardingPage(
title: '100% Anonim',
text: 'Identitas kamu tidak akan ditampilkan. Cerita dengan tenang, tanpa khawatir.',
image: 'assets/images/splash/splash_2.png',
),
_OnboardingPage(
title: 'Bestie yang Relevan',
text: 'Kamu akan dipasangkan dengan bestie berdasarkan topik & kondisi kamu saat ini.',
image: 'assets/images/splash/splash_3.png',
),
];
class OnboardingScreen extends ConsumerStatefulWidget {
const OnboardingScreen({super.key});
@override
ConsumerState<OnboardingScreen> createState() => _OnboardingScreenState();
}
class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
final _controller = PageController();
int _currentPage = 0;
@override
void initState() {
super.initState();
// Auto-advance: page 0 → 1 after 500ms
_scheduleAutoAdvance(0);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _scheduleAutoAdvance(int fromPage) {
// Only auto-advance for pages 0 and 1
if (fromPage >= 2) return;
Future.delayed(const Duration(seconds: 1), () {
if (mounted && _currentPage == fromPage) {
_controller.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
});
}
void _onPageChanged(int index) {
setState(() => _currentPage = index);
_scheduleAutoAdvance(index);
}
Future<void> _finish() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_kOnboardingDone, true);
ref.invalidate(onboardingDoneProvider);
if (mounted) {
context.go('/welcome');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Column(
children: [
Expanded(
child: PageView.builder(
controller: _controller,
itemCount: _pages.length,
onPageChanged: _onPageChanged,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
final page = _pages[index];
return _buildPage(page);
},
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32),
child: Row(
children: [
// Page indicators
Row(
children: List.generate(_pages.length, (index) {
final isActive = index == _currentPage;
return Container(
margin: const EdgeInsets.only(right: 8),
width: isActive ? 32 : 12,
height: 6,
decoration: BoxDecoration(
color: isActive ? _kPink : _kPink.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(3),
),
);
}),
),
const Spacer(),
// CTA button — only show "Mulai" on last page
if (_currentPage == _pages.length - 1)
GestureDetector(
onTap: _finish,
child: Container(
height: 56,
padding: const EdgeInsets.symmetric(horizontal: 32),
decoration: BoxDecoration(
color: _kPink,
borderRadius: BorderRadius.circular(16),
),
child: const Center(
child: Text(
'Mulai',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
),
],
),
),
],
),
),
);
}
Widget _buildPage(_OnboardingPage page) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
children: [
const Spacer(flex: 1),
// Image
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Image.asset(
page.image,
height: 280,
fit: BoxFit.contain,
),
),
const Spacer(flex: 1),
// Title
Text(
page.title,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: _kPink,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
// Description
Text(
page.text,
style: TextStyle(
fontSize: 16,
color: Colors.pink.shade300,
height: 1.5,
),
textAlign: TextAlign.center,
),
const Spacer(flex: 1),
],
),
);
}
}
/// Check if onboarding has been completed
Future<bool> isOnboardingDone() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool(_kOnboardingDone) ?? false;
}