im so tired man

This commit is contained in:
Wowieee4
2026-05-28 11:27:06 +07:00
parent 66da2473e1
commit 6272ece15d
86 changed files with 1759 additions and 4090 deletions
@@ -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;