Add Android TV admin unlock and focus-driven controls
This commit is contained in:
@@ -56,6 +56,15 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
final _iqomahAsharCtrl = TextEditingController();
|
||||
final _iqomahMaghribCtrl = TextEditingController();
|
||||
final _iqomahIsyaCtrl = TextEditingController();
|
||||
final _preAdzanLeadCtrl = TextEditingController();
|
||||
final _blankNormalCtrl = TextEditingController();
|
||||
final _blankJumatCtrl = TextEditingController();
|
||||
|
||||
final _identityScrollController = ScrollController();
|
||||
final _jadwalScrollController = ScrollController();
|
||||
final _tampilanScrollController = ScrollController();
|
||||
final _jumatScrollController = ScrollController();
|
||||
final _simulasiScrollController = ScrollController();
|
||||
int _hijriOffsetDays = 0;
|
||||
|
||||
@override
|
||||
@@ -95,6 +104,9 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
_iqomahAsharCtrl.text = settings.iqomahAshar.toString();
|
||||
_iqomahMaghribCtrl.text = settings.iqomahMaghrib.toString();
|
||||
_iqomahIsyaCtrl.text = settings.iqomahIsya.toString();
|
||||
_preAdzanLeadCtrl.text = settings.preAdzanLead.toString();
|
||||
_blankNormalCtrl.text = settings.blankScreenNormal.toString();
|
||||
_blankJumatCtrl.text = settings.blankScreenJumat.toString();
|
||||
_hijriOffsetDays = settings.hijriOffsetDays;
|
||||
|
||||
// Update preview live as admin types
|
||||
@@ -118,6 +130,14 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
_iqomahAsharCtrl.dispose();
|
||||
_iqomahMaghribCtrl.dispose();
|
||||
_iqomahIsyaCtrl.dispose();
|
||||
_preAdzanLeadCtrl.dispose();
|
||||
_blankNormalCtrl.dispose();
|
||||
_blankJumatCtrl.dispose();
|
||||
_identityScrollController.dispose();
|
||||
_jadwalScrollController.dispose();
|
||||
_tampilanScrollController.dispose();
|
||||
_jumatScrollController.dispose();
|
||||
_simulasiScrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -167,8 +187,11 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveIqomahSettings() async {
|
||||
Future<void> _saveJadwalTimingSettings() async {
|
||||
await ref.read(settingsProvider.notifier).updateSettings((s) {
|
||||
s.preAdzanLead = int.tryParse(_preAdzanLeadCtrl.text.trim()) ?? 10;
|
||||
s.blankScreenNormal = int.tryParse(_blankNormalCtrl.text.trim()) ?? 15;
|
||||
s.blankScreenJumat = int.tryParse(_blankJumatCtrl.text.trim()) ?? 45;
|
||||
s.iqomahSubuh = int.tryParse(_iqomahSubuhCtrl.text.trim()) ?? 15;
|
||||
s.iqomahDzuhur = int.tryParse(_iqomahDzuhurCtrl.text.trim()) ?? 10;
|
||||
s.iqomahAshar = int.tryParse(_iqomahAsharCtrl.text.trim()) ?? 10;
|
||||
@@ -176,10 +199,14 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
s.iqomahIsya = int.tryParse(_iqomahIsyaCtrl.text.trim()) ?? 10;
|
||||
return s;
|
||||
});
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Jeda Iqamah berhasil disimpan', style: GoogleFonts.manrope()),
|
||||
content: Text(
|
||||
'Pengaturan jadwal dan durasi berhasil disimpan',
|
||||
style: GoogleFonts.manrope(),
|
||||
),
|
||||
backgroundColor: SacredColors.primaryContainer,
|
||||
),
|
||||
);
|
||||
@@ -353,9 +380,11 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
),
|
||||
iconTheme: const IconThemeData(color: SacredColors.primary),
|
||||
),
|
||||
body: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
body: FocusTraversalGroup(
|
||||
policy: ReadingOrderTraversalPolicy(),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Nav rail area
|
||||
Container(
|
||||
width: 350 * s,
|
||||
@@ -423,6 +452,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -445,6 +475,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
|
||||
Widget _buildJumatTab(double s) {
|
||||
return SingleChildScrollView(
|
||||
controller: _jumatScrollController,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -567,6 +598,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
|
||||
Widget _buildTampilanTab(double s) {
|
||||
return SingleChildScrollView(
|
||||
controller: _tampilanScrollController,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -591,26 +623,39 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
children: [
|
||||
_sectionLabel('Tipografi & Skala Teks', s),
|
||||
SizedBox(height: 12 * s),
|
||||
DropdownButtonFormField<int>(
|
||||
initialValue: _textScaleIndex,
|
||||
onChanged: (val) => setState(() => _textScaleIndex = val ?? 1),
|
||||
items: const [
|
||||
DropdownMenuItem(value: 0, child: Text('Kecil (Small)')),
|
||||
DropdownMenuItem(value: 1, child: Text('Normal (Medium)')),
|
||||
DropdownMenuItem(value: 2, child: Text('Besar (Large)')),
|
||||
SegmentedButton<int>(
|
||||
segments: const [
|
||||
ButtonSegment(value: 0, label: Text('Kecil')),
|
||||
ButtonSegment(value: 1, label: Text('Normal')),
|
||||
ButtonSegment(value: 2, label: Text('Besar')),
|
||||
],
|
||||
style: GoogleFonts.plusJakartaSans(fontSize: 22 * s, color: SacredColors.onSurface),
|
||||
dropdownColor: SacredColors.surfaceContainerHighest,
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
fillColor: SacredColors.surfaceContainerLowest,
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(SacredRadii.md)),
|
||||
),
|
||||
selected: {_textScaleIndex},
|
||||
onSelectionChanged: (val) {
|
||||
setState(() => _textScaleIndex = val.first);
|
||||
},
|
||||
),
|
||||
SizedBox(height: 28 * s),
|
||||
_buildTextField('Durasi Layar Utama (Detik)', _mainDurCtrl, s),
|
||||
_buildTvIntStepperField(
|
||||
s: s,
|
||||
label: 'Durasi Layar Utama',
|
||||
controller: _mainDurCtrl,
|
||||
fallback: 15,
|
||||
min: 5,
|
||||
max: 120,
|
||||
suffix: 'detik',
|
||||
fastStep: 10,
|
||||
),
|
||||
SizedBox(height: 24 * s),
|
||||
_buildTextField('Durasi Tiap Slideshow (Detik)', _slideDurCtrl, s),
|
||||
_buildTvIntStepperField(
|
||||
s: s,
|
||||
label: 'Durasi Tiap Slideshow',
|
||||
controller: _slideDurCtrl,
|
||||
fallback: 10,
|
||||
min: 5,
|
||||
max: 120,
|
||||
suffix: 'detik',
|
||||
fastStep: 10,
|
||||
),
|
||||
SizedBox(height: 40 * s),
|
||||
|
||||
_sectionLabel('Ukuran Teks Per Kelompok', s),
|
||||
@@ -656,7 +701,15 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
SizedBox(height: 12 * s),
|
||||
_buildTextField('Kata Kunci (Contoh: mosque, architecture)', _unsplashKeywordCtrl, s),
|
||||
SizedBox(height: 12 * s),
|
||||
_buildTextField('Rotasi Foto (Jam)', _unsplashRotationCtrl, s),
|
||||
_buildTvIntStepperField(
|
||||
s: s,
|
||||
label: 'Rotasi Foto',
|
||||
controller: _unsplashRotationCtrl,
|
||||
fallback: 6,
|
||||
min: 1,
|
||||
max: 24,
|
||||
suffix: 'jam',
|
||||
),
|
||||
],
|
||||
|
||||
SizedBox(height: 56 * s),
|
||||
@@ -968,15 +1021,18 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
}
|
||||
|
||||
Widget _adminCard(double s, {required Widget child}) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.all(36 * s),
|
||||
decoration: BoxDecoration(
|
||||
color: SacredColors.surfaceContainerHighest.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(SacredRadii.xl),
|
||||
border: Border.all(color: SacredColors.outlineVariant.withValues(alpha: 0.2)),
|
||||
return _scrollAware(
|
||||
controller: _scrollControllerForTab(_selectedTab),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.all(36 * s),
|
||||
decoration: BoxDecoration(
|
||||
color: SacredColors.surfaceContainerHighest.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(SacredRadii.xl),
|
||||
border: Border.all(color: SacredColors.outlineVariant.withValues(alpha: 0.2)),
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -994,7 +1050,9 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
|
||||
|
||||
Widget _buildIdentityTab(double s) {
|
||||
return Column(
|
||||
return SingleChildScrollView(
|
||||
controller: _identityScrollController,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
@@ -1077,6 +1135,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1086,6 +1145,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
final displayedHijri = ref.watch(hijriDateProvider).valueOrNull;
|
||||
|
||||
return SingleChildScrollView(
|
||||
controller: _jadwalScrollController,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -1259,39 +1319,143 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
|
||||
SizedBox(height: 64 * s),
|
||||
|
||||
// Jeda Waktu Iqamah Settings Card
|
||||
// Waktu & Durasi Card
|
||||
_adminCard(s, child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_sectionLabel('Jeda Waktu Iqamah (Menit)', s),
|
||||
_sectionLabel('Waktu & Durasi', s),
|
||||
SizedBox(height: 8 * s),
|
||||
Text(
|
||||
'Tentukan durasi hitung mundur dari Adzan selesai (1 menit setelah masuk waktu) hingga iqamah. Selama jeda ini, jamaah dapat melakukan shalat sunnah.',
|
||||
'Seluruh pengaturan angka utama untuk alur jadwal ditangani dengan stepper agar nyaman dipakai dengan remote Android TV.',
|
||||
style: GoogleFonts.manrope(fontSize: 14 * s, color: SacredColors.onSurfaceVariant),
|
||||
),
|
||||
SizedBox(height: 32 * s),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildTextField('Iqamah Subuh', _iqomahSubuhCtrl, s)),
|
||||
Expanded(
|
||||
child: _buildTvIntStepperField(
|
||||
s: s,
|
||||
label: 'Pra-Adzan',
|
||||
controller: _preAdzanLeadCtrl,
|
||||
fallback: 10,
|
||||
min: 0,
|
||||
max: 60,
|
||||
suffix: 'menit',
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16 * s),
|
||||
Expanded(child: _buildTextField('Iqamah Dzuhur', _iqomahDzuhurCtrl, s)),
|
||||
Expanded(
|
||||
child: _buildTvIntStepperField(
|
||||
s: s,
|
||||
label: 'Blank Screen Normal',
|
||||
controller: _blankNormalCtrl,
|
||||
fallback: 15,
|
||||
min: 0,
|
||||
max: 120,
|
||||
suffix: 'menit',
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16 * s),
|
||||
Expanded(child: _buildTextField('Iqamah Ashar', _iqomahAsharCtrl, s)),
|
||||
Expanded(
|
||||
child: _buildTvIntStepperField(
|
||||
s: s,
|
||||
label: 'Blank Screen Jumat',
|
||||
controller: _blankJumatCtrl,
|
||||
fallback: 45,
|
||||
min: 0,
|
||||
max: 180,
|
||||
suffix: 'menit',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 28 * s),
|
||||
Text(
|
||||
'Jeda Waktu Iqamah (Menit)',
|
||||
style: GoogleFonts.manrope(
|
||||
fontSize: 16 * s,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: SacredColors.onSurface,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8 * s),
|
||||
Text(
|
||||
'Tentukan durasi hitung mundur dari selesai Adzan hingga iqamah untuk tiap shalat fardhu.',
|
||||
style: GoogleFonts.manrope(fontSize: 14 * s, color: SacredColors.onSurfaceVariant),
|
||||
),
|
||||
SizedBox(height: 24 * s),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildTvIntStepperField(
|
||||
s: s,
|
||||
label: 'Iqamah Subuh',
|
||||
controller: _iqomahSubuhCtrl,
|
||||
fallback: 15,
|
||||
min: 0,
|
||||
max: 60,
|
||||
suffix: 'menit',
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16 * s),
|
||||
Expanded(
|
||||
child: _buildTvIntStepperField(
|
||||
s: s,
|
||||
label: 'Iqamah Dzuhur',
|
||||
controller: _iqomahDzuhurCtrl,
|
||||
fallback: 10,
|
||||
min: 0,
|
||||
max: 60,
|
||||
suffix: 'menit',
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16 * s),
|
||||
Expanded(
|
||||
child: _buildTvIntStepperField(
|
||||
s: s,
|
||||
label: 'Iqamah Ashar',
|
||||
controller: _iqomahAsharCtrl,
|
||||
fallback: 10,
|
||||
min: 0,
|
||||
max: 60,
|
||||
suffix: 'menit',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 16 * s),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildTextField('Iqamah Maghrib', _iqomahMaghribCtrl, s)),
|
||||
Expanded(
|
||||
child: _buildTvIntStepperField(
|
||||
s: s,
|
||||
label: 'Iqamah Maghrib',
|
||||
controller: _iqomahMaghribCtrl,
|
||||
fallback: 7,
|
||||
min: 0,
|
||||
max: 60,
|
||||
suffix: 'menit',
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16 * s),
|
||||
Expanded(child: _buildTextField('Iqamah Isya', _iqomahIsyaCtrl, s)),
|
||||
Expanded(
|
||||
child: _buildTvIntStepperField(
|
||||
s: s,
|
||||
label: 'Iqamah Isya',
|
||||
controller: _iqomahIsyaCtrl,
|
||||
fallback: 10,
|
||||
min: 0,
|
||||
max: 60,
|
||||
suffix: 'menit',
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16 * s),
|
||||
Expanded(child: SizedBox()), // spacer
|
||||
],
|
||||
),
|
||||
SizedBox(height: 32 * s),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _saveIqomahSettings,
|
||||
onPressed: _saveJadwalTimingSettings,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: SacredColors.secondary,
|
||||
foregroundColor: Colors.black,
|
||||
@@ -1300,7 +1464,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(SacredRadii.lg)),
|
||||
),
|
||||
icon: const Icon(Icons.timer),
|
||||
label: const Text('SIMPAN JEDA IQAMAH'),
|
||||
label: const Text('SIMPAN PENGATURAN JADWAL'),
|
||||
),
|
||||
],
|
||||
)),
|
||||
@@ -1575,8 +1739,207 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
ScrollController _scrollControllerForTab(int tabIndex) {
|
||||
switch (tabIndex) {
|
||||
case 0:
|
||||
return _identityScrollController;
|
||||
case 1:
|
||||
return _jadwalScrollController;
|
||||
case 2:
|
||||
return _tampilanScrollController;
|
||||
case 3:
|
||||
return _jumatScrollController;
|
||||
case 4:
|
||||
default:
|
||||
return _simulasiScrollController;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _scrollAware({
|
||||
required ScrollController controller,
|
||||
required Widget child,
|
||||
}) {
|
||||
return Builder(
|
||||
builder: (context) {
|
||||
return Focus(
|
||||
onFocusChange: (hasFocus) {
|
||||
if (!hasFocus || !controller.hasClients) return;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (context.mounted) {
|
||||
Scrollable.ensureVisible(
|
||||
context,
|
||||
duration: const Duration(milliseconds: 220),
|
||||
curve: Curves.easeOutCubic,
|
||||
alignment: 0.18,
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
int _parseCtrlInt(TextEditingController ctrl, int fallback) {
|
||||
return int.tryParse(ctrl.text.trim()) ?? fallback;
|
||||
}
|
||||
|
||||
void _bumpCtrlInt(
|
||||
TextEditingController ctrl, {
|
||||
required int delta,
|
||||
required int min,
|
||||
required int max,
|
||||
required int fallback,
|
||||
}) {
|
||||
final next = (_parseCtrlInt(ctrl, fallback) + delta).clamp(min, max);
|
||||
setState(() {
|
||||
ctrl.text = next.toString();
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildTvIntStepperField({
|
||||
required double s,
|
||||
required String label,
|
||||
required TextEditingController controller,
|
||||
required int fallback,
|
||||
required int min,
|
||||
required int max,
|
||||
String suffix = '',
|
||||
int fastStep = 5,
|
||||
}) {
|
||||
final value = _parseCtrlInt(controller, fallback);
|
||||
final valueLabel = suffix.isEmpty ? '$value' : '$value $suffix';
|
||||
|
||||
return _scrollAware(
|
||||
controller: _scrollControllerForTab(_selectedTab),
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(16 * s),
|
||||
decoration: BoxDecoration(
|
||||
color: SacredColors.surfaceContainerLowest,
|
||||
borderRadius: BorderRadius.circular(SacredRadii.md),
|
||||
border: Border.all(
|
||||
color: SacredColors.outlineVariant.withValues(alpha: 0.25),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: GoogleFonts.manrope(
|
||||
fontSize: 15 * s,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: SacredColors.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 14 * s,
|
||||
vertical: 5 * s,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: SacredColors.primary.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(SacredRadii.sm),
|
||||
),
|
||||
child: Text(
|
||||
valueLabel,
|
||||
style: GoogleFonts.manrope(
|
||||
fontSize: 16 * s,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: SacredColors.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 14 * s),
|
||||
Row(
|
||||
children: [
|
||||
_tvStepBtn(
|
||||
s: s,
|
||||
label: '−−',
|
||||
onPressed: () => _bumpCtrlInt(
|
||||
controller,
|
||||
delta: -fastStep,
|
||||
min: min,
|
||||
max: max,
|
||||
fallback: fallback,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 6 * s),
|
||||
_tvStepBtn(
|
||||
s: s,
|
||||
label: '−',
|
||||
onPressed: () => _bumpCtrlInt(
|
||||
controller,
|
||||
delta: -1,
|
||||
min: min,
|
||||
max: max,
|
||||
fallback: fallback,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10 * s),
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 6 * s,
|
||||
decoration: BoxDecoration(
|
||||
color: SacredColors.outlineVariant.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(3 * s),
|
||||
),
|
||||
child: FractionallySizedBox(
|
||||
alignment: Alignment.centerLeft,
|
||||
widthFactor: ((value - min) / (max - min)).clamp(0.0, 1.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: SacredColors.primary,
|
||||
borderRadius: BorderRadius.circular(3 * s),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10 * s),
|
||||
_tvStepBtn(
|
||||
s: s,
|
||||
label: '+',
|
||||
onPressed: () => _bumpCtrlInt(
|
||||
controller,
|
||||
delta: 1,
|
||||
min: min,
|
||||
max: max,
|
||||
fallback: fallback,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 6 * s),
|
||||
_tvStepBtn(
|
||||
s: s,
|
||||
label: '++',
|
||||
onPressed: () => _bumpCtrlInt(
|
||||
controller,
|
||||
delta: fastStep,
|
||||
min: min,
|
||||
max: max,
|
||||
fallback: fallback,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTextField(String label, TextEditingController ctrl, double s, {int maxLines = 1}) {
|
||||
return Column(
|
||||
return _scrollAware(
|
||||
controller: _scrollControllerForTab(_selectedTab),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
@@ -1609,6 +1972,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1783,6 +2147,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
|
||||
Widget _buildSimulasiTab(double s) {
|
||||
return SingleChildScrollView(
|
||||
controller: _simulasiScrollController,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -1946,7 +2311,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
class _NavButton extends StatelessWidget {
|
||||
class _NavButton extends StatefulWidget {
|
||||
final String title;
|
||||
final dynamic icon;
|
||||
final bool isActive;
|
||||
@@ -1961,40 +2326,63 @@ class _NavButton extends StatelessWidget {
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_NavButton> createState() => _NavButtonState();
|
||||
}
|
||||
|
||||
class _NavButtonState extends State<_NavButton> {
|
||||
bool _isFocused = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = scale;
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(SacredRadii.lg),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.symmetric(horizontal: 24 * s, vertical: 24 * s),
|
||||
decoration: BoxDecoration(
|
||||
color: isActive ? SacredColors.primaryContainer : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(SacredRadii.lg),
|
||||
border: isActive ? Border.all(color: SacredColors.primary.withValues(alpha: 0.3)) : null,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
HugeIcon(
|
||||
icon: icon,
|
||||
color: isActive ? SacredColors.onPrimaryContainer : SacredColors.onSurfaceVariant,
|
||||
size: 28 * s,
|
||||
),
|
||||
SizedBox(width: 20 * s),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: GoogleFonts.plusJakartaSans(
|
||||
fontSize: 18 * s,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isActive ? SacredColors.onPrimaryContainer : SacredColors.onSurfaceVariant,
|
||||
letterSpacing: 1 * s,
|
||||
final s = widget.scale;
|
||||
final highlight = widget.isActive || _isFocused;
|
||||
|
||||
return FocusableActionDetector(
|
||||
onShowFocusHighlight: (value) => setState(() => _isFocused = value),
|
||||
child: InkWell(
|
||||
onTap: widget.onTap,
|
||||
focusColor: SacredColors.primary.withValues(alpha: 0.22),
|
||||
hoverColor: SacredColors.primary.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(SacredRadii.lg),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.symmetric(horizontal: 24 * s, vertical: 24 * s),
|
||||
decoration: BoxDecoration(
|
||||
color: highlight ? SacredColors.primaryContainer : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(SacredRadii.lg),
|
||||
border: highlight
|
||||
? Border.all(
|
||||
color: SacredColors.primary.withValues(alpha: 0.4),
|
||||
width: _isFocused ? 2 : 1,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
HugeIcon(
|
||||
icon: widget.icon,
|
||||
color: highlight
|
||||
? SacredColors.onPrimaryContainer
|
||||
: SacredColors.onSurfaceVariant,
|
||||
size: 28 * s,
|
||||
),
|
||||
SizedBox(width: 20 * s),
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.title,
|
||||
style: GoogleFonts.plusJakartaSans(
|
||||
fontSize: 18 * s,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: highlight
|
||||
? SacredColors.onPrimaryContainer
|
||||
: SacredColors.onSurfaceVariant,
|
||||
letterSpacing: 1 * s,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user