fix(tv-focus): stabilize tampilan navigation and reveal + bump 1.0.8+9

This commit is contained in:
dwindown
2026-04-05 14:30:10 +07:00
parent d507bf93d3
commit c70a6baf7b
2 changed files with 284 additions and 195 deletions

View File

@@ -527,6 +527,14 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
} }
Future<void> _showCitySearchDialog(double s) async { Future<void> _showCitySearchDialog(double s) async {
bool isActivateKey(LogicalKeyboardKey key) {
return key == LogicalKeyboardKey.enter ||
key == LogicalKeyboardKey.select ||
key == LogicalKeyboardKey.numpadEnter ||
key == LogicalKeyboardKey.space ||
key == LogicalKeyboardKey.gameButtonA;
}
final queryCtrl = TextEditingController(); final queryCtrl = TextEditingController();
final queryFocusNode = FocusNode(debugLabel: 'city_query'); final queryFocusNode = FocusNode(debugLabel: 'city_query');
final searchFocusNode = FocusNode(debugLabel: 'city_search'); final searchFocusNode = FocusNode(debugLabel: 'city_search');
@@ -639,6 +647,9 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
label: 'Kata Kunci Kota / Kabupaten', label: 'Kata Kunci Kota / Kabupaten',
controller: queryCtrl, controller: queryCtrl,
focusNode: queryFocusNode, focusNode: queryFocusNode,
onMoveDown: () {
searchFocusNode.requestFocus();
},
onEditComplete: () { onEditComplete: () {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (ctx.mounted) { if (ctx.mounted) {
@@ -667,8 +678,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
} }
return KeyEventResult.handled; return KeyEventResult.handled;
} }
if (key == LogicalKeyboardKey.enter || if (isActivateKey(key)) {
key == LogicalKeyboardKey.select) {
runSearch(); runSearch();
return KeyEventResult.handled; return KeyEventResult.handled;
} }
@@ -804,10 +814,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
return KeyEventResult.ignored; return KeyEventResult.ignored;
} }
final key = event.logicalKey; final key = event.logicalKey;
if (key == if (isActivateKey(key)) {
LogicalKeyboardKey.enter ||
key ==
LogicalKeyboardKey.select) {
selectCity(city); selectCity(city);
return KeyEventResult.handled; return KeyEventResult.handled;
} }
@@ -1185,41 +1192,37 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
void _focusIdentityRow(int index) { void _focusIdentityRow(int index) {
if (_selectedTab != 0) return; if (_selectedTab != 0) return;
if (index < 0 || index >= _identityFocusNodes.length) return; if (index < 0 || index >= _identityFocusNodes.length) return;
WidgetsBinding.instance.addPostFrameCallback((_) { _requestFocusAndReveal(
if (mounted) { _identityFocusNodes[index],
_identityFocusNodes[index].requestFocus(); _identityScrollController,
} );
});
} }
void _focusJumatRow(int index) { void _focusJumatRow(int index) {
if (_selectedTab != 4) return; if (_selectedTab != 4) return;
if (index < 0 || index >= _jumatFocusNodes.length) return; if (index < 0 || index >= _jumatFocusNodes.length) return;
WidgetsBinding.instance.addPostFrameCallback((_) { _requestFocusAndReveal(
if (mounted) { _jumatFocusNodes[index],
_jumatFocusNodes[index].requestFocus(); _jumatScrollController,
} );
});
} }
void _focusSimulasiRow(int index) { void _focusSimulasiRow(int index) {
if (_selectedTab != 5) return; if (_selectedTab != 5) return;
if (index < 0 || index >= _simulasiFocusNodes.length) return; if (index < 0 || index >= _simulasiFocusNodes.length) return;
WidgetsBinding.instance.addPostFrameCallback((_) { _requestFocusAndReveal(
if (mounted) { _simulasiFocusNodes[index],
_simulasiFocusNodes[index].requestFocus(); _simulasiScrollController,
} );
});
} }
void _focusTentangRow(int index) { void _focusTentangRow(int index) {
if (_selectedTab != 6) return; if (_selectedTab != 6) return;
if (index < 0 || index >= _tentangRowCount()) return; if (index < 0 || index >= _tentangRowCount()) return;
WidgetsBinding.instance.addPostFrameCallback((_) { _requestFocusAndReveal(
if (mounted) { _tentangFocusNodes[index],
_tentangFocusNodes[index].requestFocus(); _tentangScrollController,
} );
});
} }
int _tentangRowCount() { int _tentangRowCount() {
@@ -1229,11 +1232,10 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
void _focusJadwalRow(int index) { void _focusJadwalRow(int index) {
if (_selectedTab != 1) return; if (_selectedTab != 1) return;
if (index < 0 || index >= _jadwalFocusNodes.length) return; if (index < 0 || index >= _jadwalFocusNodes.length) return;
WidgetsBinding.instance.addPostFrameCallback((_) { _requestFocusAndReveal(
if (mounted) { _jadwalFocusNodes[index],
_jadwalFocusNodes[index].requestFocus(); _jadwalScrollController,
} );
});
} }
FocusNode _tampilanFocusNode(int index) { FocusNode _tampilanFocusNode(int index) {
@@ -1276,11 +1278,10 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
if (_selectedTab != 2) return; if (_selectedTab != 2) return;
final max = _tampilanRowCount(); final max = _tampilanRowCount();
if (index < 0 || index >= max) return; if (index < 0 || index >= max) return;
WidgetsBinding.instance.addPostFrameCallback((_) { _requestFocusAndReveal(
if (mounted) { _tampilanFocusNode(index),
_tampilanFocusNode(index).requestFocus(); _tampilanScrollController,
} );
});
} }
int _pengumumanRowCount() { int _pengumumanRowCount() {
@@ -1298,10 +1299,30 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
if (_selectedTab != 3) return; if (_selectedTab != 3) return;
final max = _pengumumanRowCount(); final max = _pengumumanRowCount();
if (index < 0 || index >= max) return; if (index < 0 || index >= max) return;
_requestFocusAndReveal(
_pengumumanFocusNode(index),
_pengumumanScrollController,
);
}
void _requestFocusAndReveal(
FocusNode node,
ScrollController controller,
) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) { if (!mounted) return;
_pengumumanFocusNode(index).requestFocus(); node.requestFocus();
} WidgetsBinding.instance.addPostFrameCallback((__) {
if (!mounted || !controller.hasClients) return;
final focusContext = node.context;
if (focusContext == null || !focusContext.mounted) return;
Scrollable.ensureVisible(
focusContext,
duration: const Duration(milliseconds: 220),
curve: Curves.easeOutCubic,
alignment: 0.18,
);
});
}); });
} }
@@ -1977,8 +1998,14 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
label: 'Gunakan Foto Unsplash API', label: 'Gunakan Foto Unsplash API',
value: _useUnsplash, value: _useUnsplash,
onChanged: (val) { onChanged: (val) {
final previous = _useUnsplash;
setState(() => _useUnsplash = val); setState(() => _useUnsplash = val);
_queueTampilanAutoSave(); _queueTampilanAutoSave();
if (!previous && val) {
_focusTampilanRow(useUnsplashRow + 1);
} else if (previous && !val) {
_focusTampilanRow(useUnsplashRow);
}
}, },
trueLabel: 'Aktif', trueLabel: 'Aktif',
falseLabel: 'Nonaktif', falseLabel: 'Nonaktif',
@@ -2363,36 +2390,39 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
), ),
), ),
SizedBox(height: 12 * s), SizedBox(height: 12 * s),
_TvEditableTextTile( _scrollAware(
scale: s, controller: _pengumumanScrollController,
label: 'Isi Text Slide', child: _TvEditableTextTile(
focusNode: _pengumumanFocusNode( scale: s,
textSlideTextRows[idx], label: 'Isi Text Slide',
focusNode: _pengumumanFocusNode(
textSlideTextRows[idx],
),
controller: textCtrl,
maxLines: 3,
onMoveLeft: () => _focusNavTab(_selectedTab),
onMoveUp: () => _focusPengumumanRow(
idx == 0
? announcementDurationRow
: textSlideDeleteRows[idx - 1],
),
onMoveDown: () => _focusPengumumanRow(
textSlideDeleteRows[idx],
),
onChanged: (val) {
_textSlides[idx] = val;
_queuePengumumanAutoSave(
message:
'Text slide tengah otomatis tersimpan',
);
},
onEditComplete: () {
_queuePengumumanAutoSave(
message:
'Text slide tengah otomatis tersimpan',
);
},
), ),
controller: textCtrl,
maxLines: 3,
onMoveLeft: () => _focusNavTab(_selectedTab),
onMoveUp: () => _focusPengumumanRow(
idx == 0
? announcementDurationRow
: textSlideDeleteRows[idx - 1],
),
onMoveDown: () => _focusPengumumanRow(
textSlideDeleteRows[idx],
),
onChanged: (val) {
_textSlides[idx] = val;
_queuePengumumanAutoSave(
message:
'Text slide tengah otomatis tersimpan',
);
},
onEditComplete: () {
_queuePengumumanAutoSave(
message:
'Text slide tengah otomatis tersimpan',
);
},
), ),
SizedBox(height: 10 * s), SizedBox(height: 10 * s),
_buildPengumumanActionButton( _buildPengumumanActionButton(
@@ -2579,57 +2609,26 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
), ),
), ),
SizedBox(height: 12 * s), SizedBox(height: 12 * s),
_TvEditableTextTile( _scrollAware(
scale: s, controller: _pengumumanScrollController,
label: 'Teks Berjalan',
focusNode: _pengumumanFocusNode(
runningTextTextRows[idx],
),
controller: textCtrl,
onMoveLeft: () => _focusNavTab(_selectedTab),
onMoveUp: () => _focusPengumumanRow(
idx == 0
? marqueeModeRow
: runningTextDeleteRows[idx - 1],
),
onMoveDown: () => _focusPengumumanRow(
runningTextDurationRows[idx],
),
onChanged: (val) {
_runningTexts[idx] = val;
_queuePengumumanAutoSave(
message:
'Teks berjalan otomatis tersimpan',
);
},
onEditComplete: () {
_queuePengumumanAutoSave(
message:
'Teks berjalan otomatis tersimpan',
);
},
),
SizedBox(height: 12 * s),
SizedBox(
width: 180 * s,
child: _TvEditableTextTile( child: _TvEditableTextTile(
scale: s, scale: s,
label: 'Durasi (detik)', label: 'Teks Berjalan',
focusNode: _pengumumanFocusNode( focusNode: _pengumumanFocusNode(
runningTextDurationRows[idx],
),
controller: durCtrl,
keyboardType: TextInputType.number,
onMoveLeft: () => _focusNavTab(_selectedTab),
onMoveUp: () => _focusPengumumanRow(
runningTextTextRows[idx], runningTextTextRows[idx],
), ),
controller: textCtrl,
onMoveLeft: () => _focusNavTab(_selectedTab),
onMoveUp: () => _focusPengumumanRow(
idx == 0
? marqueeModeRow
: runningTextDeleteRows[idx - 1],
),
onMoveDown: () => _focusPengumumanRow( onMoveDown: () => _focusPengumumanRow(
runningTextDeleteRows[idx], runningTextDurationRows[idx],
), ),
onChanged: (val) { onChanged: (val) {
_runningTextDurations[idx] = _runningTexts[idx] = val;
int.tryParse(val) ?? 12;
_queuePengumumanAutoSave( _queuePengumumanAutoSave(
message: message:
'Teks berjalan otomatis tersimpan', 'Teks berjalan otomatis tersimpan',
@@ -2643,6 +2642,43 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
}, },
), ),
), ),
SizedBox(height: 12 * s),
SizedBox(
width: 180 * s,
child: _scrollAware(
controller: _pengumumanScrollController,
child: _TvEditableTextTile(
scale: s,
label: 'Durasi (detik)',
focusNode: _pengumumanFocusNode(
runningTextDurationRows[idx],
),
controller: durCtrl,
keyboardType: TextInputType.number,
onMoveLeft: () => _focusNavTab(_selectedTab),
onMoveUp: () => _focusPengumumanRow(
runningTextTextRows[idx],
),
onMoveDown: () => _focusPengumumanRow(
runningTextDeleteRows[idx],
),
onChanged: (val) {
_runningTextDurations[idx] =
int.tryParse(val) ?? 12;
_queuePengumumanAutoSave(
message:
'Teks berjalan otomatis tersimpan',
);
},
onEditComplete: () {
_queuePengumumanAutoSave(
message:
'Teks berjalan otomatis tersimpan',
);
},
),
),
),
SizedBox(height: 10 * s), SizedBox(height: 10 * s),
_buildPengumumanActionButton( _buildPengumumanActionButton(
rowIndex: runningTextDeleteRows[idx], rowIndex: runningTextDeleteRows[idx],
@@ -2881,16 +2917,25 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
} }
final key = event.logicalKey; final key = event.logicalKey;
if (key == LogicalKeyboardKey.arrowLeft) { if (key == LogicalKeyboardKey.arrowLeft) {
onMoveLeft?.call(); if (onMoveLeft != null) {
return KeyEventResult.handled; onMoveLeft();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
} }
if (key == LogicalKeyboardKey.arrowUp) { if (key == LogicalKeyboardKey.arrowUp) {
onMoveUp?.call(); if (onMoveUp != null) {
return KeyEventResult.handled; onMoveUp();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
} }
if (key == LogicalKeyboardKey.arrowDown) { if (key == LogicalKeyboardKey.arrowDown) {
onMoveDown?.call(); if (onMoveDown != null) {
return KeyEventResult.handled; onMoveDown();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
} }
if (key == LogicalKeyboardKey.arrowRight) { if (key == LogicalKeyboardKey.arrowRight) {
return KeyEventResult.handled; return KeyEventResult.handled;
@@ -4046,52 +4091,50 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
final denominator = max - min; final denominator = max - min;
final progress = denominator <= 0 ? 0.0 : ((value - min) / denominator); final progress = denominator <= 0 ? 0.0 : ((value - min) / denominator);
return _scrollAware( return _buildTvAdjustTile(
controller: _scrollControllerForTab(_selectedTab), s: s,
child: _buildTvAdjustTile( focusNode: focusNode,
s: s, label: label,
focusNode: focusNode, valueLabel: valueLabel,
label: label, progress: progress.clamp(0.0, 1.0),
valueLabel: valueLabel, helperText:
progress: progress.clamp(0.0, 1.0), 'Tekan OK untuk mulai ubah. Saat aktif, gunakan ← → untuk menyesuaikan nilai.',
helperText: 'Tekan OK untuk mulai ubah. Saat aktif, gunakan ← → untuk menyesuaikan nilai.', onMoveLeft: onMoveLeft,
onMoveLeft: onMoveLeft, onMoveUp: onMoveUp,
onMoveUp: onMoveUp, onMoveDown: onMoveDown,
onMoveDown: onMoveDown, onIncrement: () {
onIncrement: () { _bumpCtrlInt(
_bumpCtrlInt( controller,
controller, delta: 1,
delta: 1, min: min,
min: min, max: max,
max: max, fallback: fallback,
fallback: fallback, );
); onValueChanged?.call();
onValueChanged?.call(); },
}, onDecrement: () {
onDecrement: () { _bumpCtrlInt(
_bumpCtrlInt( controller,
controller, delta: -1,
delta: -1, min: min,
min: min, max: max,
max: max, fallback: fallback,
fallback: fallback, );
); onValueChanged?.call();
onValueChanged?.call(); },
}, onProgressChanged: denominator <= 0
onProgressChanged: denominator <= 0 ? null
? null : (nextProgress) {
: (nextProgress) { final mapped = (min + ((max - min) * nextProgress))
final mapped = (min + ((max - min) * nextProgress)) .round()
.round() .clamp(min, max)
.clamp(min, max) .toInt();
.toInt(); if (mapped == _parseCtrlInt(controller, fallback)) return;
if (mapped == _parseCtrlInt(controller, fallback)) return; setState(() {
setState(() { controller.text = mapped.toString();
controller.text = mapped.toString(); });
}); onValueChanged?.call();
onValueChanged?.call(); },
},
),
); );
} }
@@ -4109,19 +4152,22 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
required VoidCallback onDecrement, required VoidCallback onDecrement,
ValueChanged<double>? onProgressChanged, ValueChanged<double>? onProgressChanged,
}) { }) {
return _TvAdjustTile( return _scrollAware(
scale: s, controller: _scrollControllerForTab(_selectedTab),
focusNode: focusNode, child: _TvAdjustTile(
label: label, scale: s,
valueLabel: valueLabel, focusNode: focusNode,
progress: progress, label: label,
helperText: helperText, valueLabel: valueLabel,
onMoveLeft: onMoveLeft, progress: progress,
onMoveUp: onMoveUp, helperText: helperText,
onMoveDown: onMoveDown, onMoveLeft: onMoveLeft,
onIncrement: onIncrement, onMoveUp: onMoveUp,
onDecrement: onDecrement, onMoveDown: onMoveDown,
onProgressChanged: onProgressChanged, onIncrement: onIncrement,
onDecrement: onDecrement,
onProgressChanged: onProgressChanged,
),
); );
} }
@@ -4972,29 +5018,60 @@ class _TvAdjustTileState extends State<_TvAdjustTile> {
} }
final key = event.logicalKey; final key = event.logicalKey;
if (key == LogicalKeyboardKey.select || key == LogicalKeyboardKey.enter) { final isActivateKey = key == LogicalKeyboardKey.select ||
key == LogicalKeyboardKey.enter ||
key == LogicalKeyboardKey.numpadEnter ||
key == LogicalKeyboardKey.space ||
key == LogicalKeyboardKey.gameButtonA;
if (isActivateKey) {
setState(() => _isEditing = !_isEditing); setState(() => _isEditing = !_isEditing);
return KeyEventResult.handled; return KeyEventResult.handled;
} }
if (!_isEditing && key == LogicalKeyboardKey.arrowUp) { if (!_isEditing && key == LogicalKeyboardKey.arrowUp) {
widget.onMoveUp?.call(); if (widget.onMoveUp != null) {
return KeyEventResult.handled; widget.onMoveUp!.call();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
} }
if (!_isEditing && key == LogicalKeyboardKey.arrowDown) { if (!_isEditing && key == LogicalKeyboardKey.arrowDown) {
widget.onMoveDown?.call(); if (widget.onMoveDown != null) {
return KeyEventResult.handled; widget.onMoveDown!.call();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
} }
if (!_isEditing && key == LogicalKeyboardKey.arrowLeft) { if (!_isEditing && key == LogicalKeyboardKey.arrowLeft) {
widget.onMoveLeft?.call(); if (widget.onMoveLeft != null) {
return KeyEventResult.handled; widget.onMoveLeft!.call();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
} }
if (!_isEditing && key == LogicalKeyboardKey.arrowRight) { if (!_isEditing && key == LogicalKeyboardKey.arrowRight) {
return KeyEventResult.handled; return KeyEventResult.ignored;
} }
if (!_isEditing) return KeyEventResult.ignored; if (!_isEditing) return KeyEventResult.ignored;
if (key == LogicalKeyboardKey.arrowUp) {
setState(() => _isEditing = false);
if (widget.onMoveUp != null) {
widget.onMoveUp!.call();
}
return KeyEventResult.handled;
}
if (key == LogicalKeyboardKey.arrowDown) {
setState(() => _isEditing = false);
if (widget.onMoveDown != null) {
widget.onMoveDown!.call();
}
return KeyEventResult.handled;
}
if (key == LogicalKeyboardKey.arrowLeft) { if (key == LogicalKeyboardKey.arrowLeft) {
widget.onDecrement(); widget.onDecrement();
return KeyEventResult.handled; return KeyEventResult.handled;
@@ -5300,26 +5377,39 @@ class _TvEditableTextTileState extends State<_TvEditableTextTile> {
} }
final key = event.logicalKey; final key = event.logicalKey;
final isActivateKey = key == LogicalKeyboardKey.enter ||
key == LogicalKeyboardKey.select ||
key == LogicalKeyboardKey.numpadEnter ||
key == LogicalKeyboardKey.space ||
key == LogicalKeyboardKey.gameButtonA;
if (!_isEditing && if (!_isEditing &&
(key == LogicalKeyboardKey.enter || isActivateKey) {
key == LogicalKeyboardKey.select)) {
_startEditing(); _startEditing();
return KeyEventResult.handled; return KeyEventResult.handled;
} }
if (!_isEditing && key == LogicalKeyboardKey.arrowLeft) { if (!_isEditing && key == LogicalKeyboardKey.arrowLeft) {
widget.onMoveLeft?.call(); if (widget.onMoveLeft != null) {
return KeyEventResult.handled; widget.onMoveLeft!.call();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
} }
if (!_isEditing && key == LogicalKeyboardKey.arrowUp) { if (!_isEditing && key == LogicalKeyboardKey.arrowUp) {
widget.onMoveUp?.call(); if (widget.onMoveUp != null) {
return KeyEventResult.handled; widget.onMoveUp!.call();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
} }
if (!_isEditing && key == LogicalKeyboardKey.arrowDown) { if (!_isEditing && key == LogicalKeyboardKey.arrowDown) {
widget.onMoveDown?.call(); if (widget.onMoveDown != null) {
return KeyEventResult.handled; widget.onMoveDown!.call();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
} }
if (_isEditing && key == LogicalKeyboardKey.escape) { if (_isEditing && key == LogicalKeyboardKey.escape) {
@@ -5329,8 +5419,7 @@ class _TvEditableTextTileState extends State<_TvEditableTextTile> {
if (_isEditing && if (_isEditing &&
widget.maxLines == 1 && widget.maxLines == 1 &&
(key == LogicalKeyboardKey.enter || isActivateKey) {
key == LogicalKeyboardKey.select)) {
_finishEditing(); _finishEditing();
return KeyEventResult.handled; return KeyEventResult.handled;
} }

View File

@@ -1,7 +1,7 @@
name: jamshalat_masjid_screen name: jamshalat_masjid_screen
description: Smart Digital Prayer Clock for Android TV Box description: Smart Digital Prayer Clock for Android TV Box
publish_to: 'none' publish_to: 'none'
version: 1.0.6+7 version: 1.0.8+9
environment: environment:
sdk: '>=3.0.0 <4.0.0' sdk: '>=3.0.0 <4.0.0'