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;
WidgetsBinding.instance.addPostFrameCallback((_) { _requestFocusAndReveal(
if (mounted) { _pengumumanFocusNode(index),
_pengumumanFocusNode(index).requestFocus(); _pengumumanScrollController,
);
} }
void _requestFocusAndReveal(
FocusNode node,
ScrollController controller,
) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
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,7 +2390,9 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
), ),
), ),
SizedBox(height: 12 * s), SizedBox(height: 12 * s),
_TvEditableTextTile( _scrollAware(
controller: _pengumumanScrollController,
child: _TvEditableTextTile(
scale: s, scale: s,
label: 'Isi Text Slide', label: 'Isi Text Slide',
focusNode: _pengumumanFocusNode( focusNode: _pengumumanFocusNode(
@@ -2394,6 +2423,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
); );
}, },
), ),
),
SizedBox(height: 10 * s), SizedBox(height: 10 * s),
_buildPengumumanActionButton( _buildPengumumanActionButton(
rowIndex: textSlideDeleteRows[idx], rowIndex: textSlideDeleteRows[idx],
@@ -2579,7 +2609,9 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
), ),
), ),
SizedBox(height: 12 * s), SizedBox(height: 12 * s),
_TvEditableTextTile( _scrollAware(
controller: _pengumumanScrollController,
child: _TvEditableTextTile(
scale: s, scale: s,
label: 'Teks Berjalan', label: 'Teks Berjalan',
focusNode: _pengumumanFocusNode( focusNode: _pengumumanFocusNode(
@@ -2609,9 +2641,12 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
); );
}, },
), ),
),
SizedBox(height: 12 * s), SizedBox(height: 12 * s),
SizedBox( SizedBox(
width: 180 * s, width: 180 * s,
child: _scrollAware(
controller: _pengumumanScrollController,
child: _TvEditableTextTile( child: _TvEditableTextTile(
scale: s, scale: s,
label: 'Durasi (detik)', label: 'Durasi (detik)',
@@ -2643,6 +2678,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
}, },
), ),
), ),
),
SizedBox(height: 10 * s), SizedBox(height: 10 * s),
_buildPengumumanActionButton( _buildPengumumanActionButton(
rowIndex: runningTextDeleteRows[idx], rowIndex: runningTextDeleteRows[idx],
@@ -2881,17 +2917,26 @@ 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) {
onMoveLeft();
return KeyEventResult.handled; return KeyEventResult.handled;
} }
return KeyEventResult.ignored;
}
if (key == LogicalKeyboardKey.arrowUp) { if (key == LogicalKeyboardKey.arrowUp) {
onMoveUp?.call(); if (onMoveUp != null) {
onMoveUp();
return KeyEventResult.handled; return KeyEventResult.handled;
} }
return KeyEventResult.ignored;
}
if (key == LogicalKeyboardKey.arrowDown) { if (key == LogicalKeyboardKey.arrowDown) {
onMoveDown?.call(); if (onMoveDown != null) {
onMoveDown();
return KeyEventResult.handled; return KeyEventResult.handled;
} }
return KeyEventResult.ignored;
}
if (key == LogicalKeyboardKey.arrowRight) { if (key == LogicalKeyboardKey.arrowRight) {
return KeyEventResult.handled; return KeyEventResult.handled;
} }
@@ -4046,15 +4091,14 @@ 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),
child: _buildTvAdjustTile(
s: s, s: s,
focusNode: focusNode, focusNode: focusNode,
label: label, label: label,
valueLabel: valueLabel, valueLabel: valueLabel,
progress: progress.clamp(0.0, 1.0), progress: progress.clamp(0.0, 1.0),
helperText: '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,
@@ -4091,7 +4135,6 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
}); });
onValueChanged?.call(); onValueChanged?.call();
}, },
),
); );
} }
@@ -4109,7 +4152,9 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
required VoidCallback onDecrement, required VoidCallback onDecrement,
ValueChanged<double>? onProgressChanged, ValueChanged<double>? onProgressChanged,
}) { }) {
return _TvAdjustTile( return _scrollAware(
controller: _scrollControllerForTab(_selectedTab),
child: _TvAdjustTile(
scale: s, scale: s,
focusNode: focusNode, focusNode: focusNode,
label: label, label: label,
@@ -4122,6 +4167,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
onIncrement: onIncrement, onIncrement: onIncrement,
onDecrement: onDecrement, onDecrement: onDecrement,
onProgressChanged: onProgressChanged, 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) {
widget.onMoveUp!.call();
return KeyEventResult.handled; return KeyEventResult.handled;
} }
return KeyEventResult.ignored;
}
if (!_isEditing && key == LogicalKeyboardKey.arrowDown) { if (!_isEditing && key == LogicalKeyboardKey.arrowDown) {
widget.onMoveDown?.call(); if (widget.onMoveDown != null) {
widget.onMoveDown!.call();
return KeyEventResult.handled; return KeyEventResult.handled;
} }
return KeyEventResult.ignored;
}
if (!_isEditing && key == LogicalKeyboardKey.arrowLeft) { if (!_isEditing && key == LogicalKeyboardKey.arrowLeft) {
widget.onMoveLeft?.call(); if (widget.onMoveLeft != null) {
widget.onMoveLeft!.call();
return KeyEventResult.handled; 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,27 +5377,40 @@ 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) {
widget.onMoveLeft!.call();
return KeyEventResult.handled; return KeyEventResult.handled;
} }
return KeyEventResult.ignored;
}
if (!_isEditing && key == LogicalKeyboardKey.arrowUp) { if (!_isEditing && key == LogicalKeyboardKey.arrowUp) {
widget.onMoveUp?.call(); if (widget.onMoveUp != null) {
widget.onMoveUp!.call();
return KeyEventResult.handled; return KeyEventResult.handled;
} }
return KeyEventResult.ignored;
}
if (!_isEditing && key == LogicalKeyboardKey.arrowDown) { if (!_isEditing && key == LogicalKeyboardKey.arrowDown) {
widget.onMoveDown?.call(); if (widget.onMoveDown != null) {
widget.onMoveDown!.call();
return KeyEventResult.handled; return KeyEventResult.handled;
} }
return KeyEventResult.ignored;
}
if (_isEditing && key == LogicalKeyboardKey.escape) { if (_isEditing && key == LogicalKeyboardKey.escape) {
_finishEditing(); _finishEditing();
@@ -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'