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 {
bool isActivateKey(LogicalKeyboardKey key) {
return key == LogicalKeyboardKey.enter ||
key == LogicalKeyboardKey.select ||
key == LogicalKeyboardKey.numpadEnter ||
key == LogicalKeyboardKey.space ||
key == LogicalKeyboardKey.gameButtonA;
}
final queryCtrl = TextEditingController();
final queryFocusNode = FocusNode(debugLabel: 'city_query');
final searchFocusNode = FocusNode(debugLabel: 'city_search');
@@ -639,6 +647,9 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
label: 'Kata Kunci Kota / Kabupaten',
controller: queryCtrl,
focusNode: queryFocusNode,
onMoveDown: () {
searchFocusNode.requestFocus();
},
onEditComplete: () {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (ctx.mounted) {
@@ -667,8 +678,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
}
return KeyEventResult.handled;
}
if (key == LogicalKeyboardKey.enter ||
key == LogicalKeyboardKey.select) {
if (isActivateKey(key)) {
runSearch();
return KeyEventResult.handled;
}
@@ -804,10 +814,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
return KeyEventResult.ignored;
}
final key = event.logicalKey;
if (key ==
LogicalKeyboardKey.enter ||
key ==
LogicalKeyboardKey.select) {
if (isActivateKey(key)) {
selectCity(city);
return KeyEventResult.handled;
}
@@ -1185,41 +1192,37 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
void _focusIdentityRow(int index) {
if (_selectedTab != 0) return;
if (index < 0 || index >= _identityFocusNodes.length) return;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_identityFocusNodes[index].requestFocus();
}
});
_requestFocusAndReveal(
_identityFocusNodes[index],
_identityScrollController,
);
}
void _focusJumatRow(int index) {
if (_selectedTab != 4) return;
if (index < 0 || index >= _jumatFocusNodes.length) return;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_jumatFocusNodes[index].requestFocus();
}
});
_requestFocusAndReveal(
_jumatFocusNodes[index],
_jumatScrollController,
);
}
void _focusSimulasiRow(int index) {
if (_selectedTab != 5) return;
if (index < 0 || index >= _simulasiFocusNodes.length) return;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_simulasiFocusNodes[index].requestFocus();
}
});
_requestFocusAndReveal(
_simulasiFocusNodes[index],
_simulasiScrollController,
);
}
void _focusTentangRow(int index) {
if (_selectedTab != 6) return;
if (index < 0 || index >= _tentangRowCount()) return;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_tentangFocusNodes[index].requestFocus();
}
});
_requestFocusAndReveal(
_tentangFocusNodes[index],
_tentangScrollController,
);
}
int _tentangRowCount() {
@@ -1229,11 +1232,10 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
void _focusJadwalRow(int index) {
if (_selectedTab != 1) return;
if (index < 0 || index >= _jadwalFocusNodes.length) return;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_jadwalFocusNodes[index].requestFocus();
}
});
_requestFocusAndReveal(
_jadwalFocusNodes[index],
_jadwalScrollController,
);
}
FocusNode _tampilanFocusNode(int index) {
@@ -1276,11 +1278,10 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
if (_selectedTab != 2) return;
final max = _tampilanRowCount();
if (index < 0 || index >= max) return;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_tampilanFocusNode(index).requestFocus();
}
});
_requestFocusAndReveal(
_tampilanFocusNode(index),
_tampilanScrollController,
);
}
int _pengumumanRowCount() {
@@ -1298,10 +1299,30 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
if (_selectedTab != 3) return;
final max = _pengumumanRowCount();
if (index < 0 || index >= max) return;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_pengumumanFocusNode(index).requestFocus();
_requestFocusAndReveal(
_pengumumanFocusNode(index),
_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',
value: _useUnsplash,
onChanged: (val) {
final previous = _useUnsplash;
setState(() => _useUnsplash = val);
_queueTampilanAutoSave();
if (!previous && val) {
_focusTampilanRow(useUnsplashRow + 1);
} else if (previous && !val) {
_focusTampilanRow(useUnsplashRow);
}
},
trueLabel: 'Aktif',
falseLabel: 'Nonaktif',
@@ -2363,7 +2390,9 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
),
),
SizedBox(height: 12 * s),
_TvEditableTextTile(
_scrollAware(
controller: _pengumumanScrollController,
child: _TvEditableTextTile(
scale: s,
label: 'Isi Text Slide',
focusNode: _pengumumanFocusNode(
@@ -2394,6 +2423,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
);
},
),
),
SizedBox(height: 10 * s),
_buildPengumumanActionButton(
rowIndex: textSlideDeleteRows[idx],
@@ -2579,7 +2609,9 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
),
),
SizedBox(height: 12 * s),
_TvEditableTextTile(
_scrollAware(
controller: _pengumumanScrollController,
child: _TvEditableTextTile(
scale: s,
label: 'Teks Berjalan',
focusNode: _pengumumanFocusNode(
@@ -2609,9 +2641,12 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
);
},
),
),
SizedBox(height: 12 * s),
SizedBox(
width: 180 * s,
child: _scrollAware(
controller: _pengumumanScrollController,
child: _TvEditableTextTile(
scale: s,
label: 'Durasi (detik)',
@@ -2643,6 +2678,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
},
),
),
),
SizedBox(height: 10 * s),
_buildPengumumanActionButton(
rowIndex: runningTextDeleteRows[idx],
@@ -2881,17 +2917,26 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
}
final key = event.logicalKey;
if (key == LogicalKeyboardKey.arrowLeft) {
onMoveLeft?.call();
if (onMoveLeft != null) {
onMoveLeft();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
}
if (key == LogicalKeyboardKey.arrowUp) {
onMoveUp?.call();
if (onMoveUp != null) {
onMoveUp();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
}
if (key == LogicalKeyboardKey.arrowDown) {
onMoveDown?.call();
if (onMoveDown != null) {
onMoveDown();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
}
if (key == LogicalKeyboardKey.arrowRight) {
return KeyEventResult.handled;
}
@@ -4046,15 +4091,14 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
final denominator = max - min;
final progress = denominator <= 0 ? 0.0 : ((value - min) / denominator);
return _scrollAware(
controller: _scrollControllerForTab(_selectedTab),
child: _buildTvAdjustTile(
return _buildTvAdjustTile(
s: s,
focusNode: focusNode,
label: label,
valueLabel: valueLabel,
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,
onMoveUp: onMoveUp,
onMoveDown: onMoveDown,
@@ -4091,7 +4135,6 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
});
onValueChanged?.call();
},
),
);
}
@@ -4109,7 +4152,9 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
required VoidCallback onDecrement,
ValueChanged<double>? onProgressChanged,
}) {
return _TvAdjustTile(
return _scrollAware(
controller: _scrollControllerForTab(_selectedTab),
child: _TvAdjustTile(
scale: s,
focusNode: focusNode,
label: label,
@@ -4122,6 +4167,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
onIncrement: onIncrement,
onDecrement: onDecrement,
onProgressChanged: onProgressChanged,
),
);
}
@@ -4972,29 +5018,60 @@ class _TvAdjustTileState extends State<_TvAdjustTile> {
}
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);
return KeyEventResult.handled;
}
if (!_isEditing && key == LogicalKeyboardKey.arrowUp) {
widget.onMoveUp?.call();
if (widget.onMoveUp != null) {
widget.onMoveUp!.call();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
}
if (!_isEditing && key == LogicalKeyboardKey.arrowDown) {
widget.onMoveDown?.call();
if (widget.onMoveDown != null) {
widget.onMoveDown!.call();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
}
if (!_isEditing && key == LogicalKeyboardKey.arrowLeft) {
widget.onMoveLeft?.call();
if (widget.onMoveLeft != null) {
widget.onMoveLeft!.call();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
}
if (!_isEditing && key == LogicalKeyboardKey.arrowRight) {
return KeyEventResult.handled;
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) {
widget.onDecrement();
return KeyEventResult.handled;
@@ -5300,27 +5377,40 @@ class _TvEditableTextTileState extends State<_TvEditableTextTile> {
}
final key = event.logicalKey;
final isActivateKey = key == LogicalKeyboardKey.enter ||
key == LogicalKeyboardKey.select ||
key == LogicalKeyboardKey.numpadEnter ||
key == LogicalKeyboardKey.space ||
key == LogicalKeyboardKey.gameButtonA;
if (!_isEditing &&
(key == LogicalKeyboardKey.enter ||
key == LogicalKeyboardKey.select)) {
isActivateKey) {
_startEditing();
return KeyEventResult.handled;
}
if (!_isEditing && key == LogicalKeyboardKey.arrowLeft) {
widget.onMoveLeft?.call();
if (widget.onMoveLeft != null) {
widget.onMoveLeft!.call();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
}
if (!_isEditing && key == LogicalKeyboardKey.arrowUp) {
widget.onMoveUp?.call();
if (widget.onMoveUp != null) {
widget.onMoveUp!.call();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
}
if (!_isEditing && key == LogicalKeyboardKey.arrowDown) {
widget.onMoveDown?.call();
if (widget.onMoveDown != null) {
widget.onMoveDown!.call();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
}
if (_isEditing && key == LogicalKeyboardKey.escape) {
_finishEditing();
@@ -5329,8 +5419,7 @@ class _TvEditableTextTileState extends State<_TvEditableTextTile> {
if (_isEditing &&
widget.maxLines == 1 &&
(key == LogicalKeyboardKey.enter ||
key == LogicalKeyboardKey.select)) {
isActivateKey) {
_finishEditing();
return KeyEventResult.handled;
}

View File

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