im so tired man
This commit is contained in:
@@ -26,7 +26,9 @@ class _UserShellState extends State<UserShell> {
|
||||
super.initState();
|
||||
_loadVoiceCommands();
|
||||
_startHardwareShortcuts();
|
||||
sl<SttService>().startListening();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_startVoiceListening();
|
||||
});
|
||||
sl<VoiceCommandHandler>().onCommand = (key) {
|
||||
if (!mounted) return;
|
||||
switch (key) {
|
||||
@@ -64,6 +66,17 @@ class _UserShellState extends State<UserShell> {
|
||||
};
|
||||
}
|
||||
|
||||
Future<void> _startVoiceListening() async {
|
||||
await runFriendlyAction(
|
||||
() async {
|
||||
await sl<SttService>().init();
|
||||
await sl<SttService>().startListening();
|
||||
},
|
||||
onError: (_) {},
|
||||
fallback: 'Voice listener belum bisa dimuat.',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _loadVoiceCommands() async {
|
||||
await runFriendlyAction(
|
||||
() async {
|
||||
@@ -182,42 +195,41 @@ class _AppShell extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.surface,
|
||||
body: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
switchInCurve: Curves.easeOutCubic,
|
||||
switchOutCurve: Curves.easeInCubic,
|
||||
child: KeyedSubtree(
|
||||
key: ValueKey(location),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: const Border(top: BorderSide(color: AppColors.border)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.06),
|
||||
blurRadius: 18,
|
||||
offset: const Offset(0, -8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: NavigationBar(
|
||||
selectedIndex: _selectedIndex,
|
||||
onDestinationSelected: (index) => context.go(items[index].route),
|
||||
destinations: [
|
||||
for (final item in items)
|
||||
NavigationDestination(
|
||||
icon: Icon(item.icon),
|
||||
selectedIcon: Icon(item.selectedIcon),
|
||||
label: item.label,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final useRail = constraints.maxWidth >= 760;
|
||||
final content = AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
switchInCurve: Curves.easeOutCubic,
|
||||
switchOutCurve: Curves.easeInCubic,
|
||||
child: KeyedSubtree(
|
||||
key: ValueKey(location),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.surface,
|
||||
body: useRail
|
||||
? Row(
|
||||
children: [
|
||||
_RailNavigation(
|
||||
items: items,
|
||||
selectedIndex: _selectedIndex,
|
||||
),
|
||||
const VerticalDivider(width: 1, color: AppColors.border),
|
||||
Expanded(child: content),
|
||||
],
|
||||
)
|
||||
: content,
|
||||
bottomNavigationBar: useRail
|
||||
? null
|
||||
: _BottomScrollNavigation(
|
||||
items: items,
|
||||
selectedIndex: _selectedIndex,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -227,6 +239,194 @@ class _AppShell extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _RailNavigation extends StatelessWidget {
|
||||
final List<_ShellItem> items;
|
||||
final int selectedIndex;
|
||||
|
||||
const _RailNavigation({
|
||||
required this.items,
|
||||
required this.selectedIndex,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final compact = constraints.maxHeight < 520;
|
||||
final width = compact ? 76.0 : 86.0;
|
||||
final itemHeight = compact ? 58.0 : 70.0;
|
||||
|
||||
return DecoratedBox(
|
||||
decoration: const BoxDecoration(color: Colors.white),
|
||||
child: SafeArea(
|
||||
right: false,
|
||||
child: SizedBox(
|
||||
width: width,
|
||||
child: ListView.separated(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: compact ? 6 : 12,
|
||||
),
|
||||
itemCount: items.length,
|
||||
separatorBuilder: (_, __) => SizedBox(height: compact ? 2 : 6),
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
final selected = index == selectedIndex;
|
||||
return Semantics(
|
||||
button: true,
|
||||
selected: selected,
|
||||
label: item.label,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
onTap: () => context.go(items[index].route),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 160),
|
||||
height: itemHeight,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: selected
|
||||
? const Color(0xFFEFF6FF)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
selected ? item.selectedIcon : item.icon,
|
||||
size: compact ? 23 : 25,
|
||||
color: selected
|
||||
? AppColors.primary
|
||||
: const Color(0xFF334155),
|
||||
),
|
||||
SizedBox(height: compact ? 2 : 5),
|
||||
Text(
|
||||
item.label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: compact ? 10 : 12,
|
||||
height: 1,
|
||||
fontWeight: selected
|
||||
? FontWeight.w800
|
||||
: FontWeight.w600,
|
||||
color: selected
|
||||
? const Color(0xFF1D4ED8)
|
||||
: const Color(0xFF334155),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BottomScrollNavigation extends StatelessWidget {
|
||||
final List<_ShellItem> items;
|
||||
final int selectedIndex;
|
||||
|
||||
const _BottomScrollNavigation({
|
||||
required this.items,
|
||||
required this.selectedIndex,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bottom = MediaQuery.of(context).padding.bottom;
|
||||
final extraBottom = bottom > 12 ? 12.0 : bottom;
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: const Border(top: BorderSide(color: AppColors.border)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.06),
|
||||
blurRadius: 18,
|
||||
offset: const Offset(0, -8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: SizedBox(
|
||||
height: 68 + extraBottom,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||
itemCount: items.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(width: 6),
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
final selected = index == selectedIndex;
|
||||
return Semantics(
|
||||
button: true,
|
||||
selected: selected,
|
||||
label: item.label,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: () => context.go(item.route),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 160),
|
||||
width: 72,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: selected
|
||||
? const Color(0xFFEFF6FF)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: selected
|
||||
? const Color(0xFFBFDBFE)
|
||||
: Colors.transparent,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
selected ? item.selectedIcon : item.icon,
|
||||
color: selected
|
||||
? AppColors.primary
|
||||
: const Color(0xFF64748B),
|
||||
size: 22,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
item.label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight:
|
||||
selected ? FontWeight.w800 : FontWeight.w600,
|
||||
color: selected
|
||||
? AppColors.primary
|
||||
: const Color(0xFF64748B),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ShellItem {
|
||||
final String label;
|
||||
final IconData icon;
|
||||
|
||||
@@ -19,62 +19,127 @@ class FeaturePage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 14, 16, 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 12, end: 0),
|
||||
duration: const Duration(milliseconds: 360),
|
||||
curve: Curves.easeOutCubic,
|
||||
builder: (_, offset, child) => Opacity(
|
||||
opacity: (1 - offset / 12).clamp(0.0, 1.0),
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, offset),
|
||||
child: child,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final short = constraints.maxHeight < 520;
|
||||
final compact = constraints.maxWidth < 420 || short;
|
||||
final wide = constraints.maxWidth >= 900;
|
||||
final horizontal = compact ? 12.0 : 20.0;
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
horizontal,
|
||||
short ? 8 : 12,
|
||||
horizontal,
|
||||
short ? 10 : 14,
|
||||
),
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: wide ? 1160 : double.infinity,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 12, end: 0),
|
||||
duration: const Duration(milliseconds: 360),
|
||||
curve: Curves.easeOutCubic,
|
||||
builder: (_, offset, child) => Opacity(
|
||||
opacity: (1 - offset / 12).clamp(0.0, 1.0),
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, offset),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
child: compact
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_FeatureHeading(
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
compact: compact,
|
||||
),
|
||||
if (trailing != null) ...[
|
||||
const SizedBox(height: 10),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: trailing!,
|
||||
),
|
||||
],
|
||||
],
|
||||
)
|
||||
: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _FeatureHeading(
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
compact: compact,
|
||||
),
|
||||
),
|
||||
if (trailing != null) trailing!,
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: short ? 8 : (compact ? 12 : 16)),
|
||||
Expanded(
|
||||
child: child,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headlineSmall
|
||||
?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
color: AppColors.text,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
subtitle,
|
||||
style: const TextStyle(
|
||||
color: AppColors.muted,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (trailing != null) trailing!,
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Expanded(child: child),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FeatureHeading extends StatelessWidget {
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final bool compact;
|
||||
|
||||
const _FeatureHeading({
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.compact,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final short = MediaQuery.sizeOf(context).height < 520;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
maxLines: short ? 1 : 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontSize: compact ? 22 : null,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: AppColors.text,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
subtitle,
|
||||
maxLines: short ? 1 : (compact ? 2 : 3),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: AppColors.muted,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.25,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FeatureEmptyPanel extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String title;
|
||||
|
||||
Reference in New Issue
Block a user