feat(tv-admin): fix action/focus flows, update app title, randomize unsplash, bump 1.0.9+10
This commit is contained in:
@@ -344,6 +344,40 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _pickSlideshowImages() async {
|
||||
try {
|
||||
final res = await FilePicker.platform.pickFiles(
|
||||
type: FileType.image,
|
||||
allowMultiple: true,
|
||||
);
|
||||
if (res == null) return;
|
||||
|
||||
var hasNewImage = false;
|
||||
setState(() {
|
||||
for (final path in res.paths) {
|
||||
if (path != null &&
|
||||
File(path).existsSync() &&
|
||||
!_slideshowImages.contains(path)) {
|
||||
_slideshowImages.add(path);
|
||||
hasNewImage = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (hasNewImage) {
|
||||
_queueTampilanAutoSave(
|
||||
message: 'Galeri slideshow otomatis tersimpan',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
_showStatusBadge(
|
||||
'Gagal membuka pemilih file. Pastikan file manager tersedia di perangkat.',
|
||||
isError: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _savePengumuman({
|
||||
String message = 'Pengaturan pengumuman otomatis tersimpan',
|
||||
}) async {
|
||||
@@ -1270,7 +1304,6 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
count += 1;
|
||||
count += 1;
|
||||
count += _slideshowImages.length;
|
||||
count += 1;
|
||||
return count;
|
||||
}
|
||||
|
||||
@@ -1375,9 +1408,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
_focusNavTab(index + 1);
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (key == LogicalKeyboardKey.arrowRight ||
|
||||
key == LogicalKeyboardKey.select ||
|
||||
key == LogicalKeyboardKey.enter) {
|
||||
if (key == LogicalKeyboardKey.arrowRight || _isActivateKey(key)) {
|
||||
_focusEntryForTab(index);
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
@@ -1410,7 +1441,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
if (key == LogicalKeyboardKey.arrowRight) {
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (key == LogicalKeyboardKey.select || key == LogicalKeyboardKey.enter) {
|
||||
if (_isActivateKey(key)) {
|
||||
onActivate();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
@@ -1453,7 +1484,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
if (key == LogicalKeyboardKey.arrowRight) {
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (key == LogicalKeyboardKey.select || key == LogicalKeyboardKey.enter) {
|
||||
if (_isActivateKey(key)) {
|
||||
onActivate();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
@@ -1485,7 +1516,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
if (key == LogicalKeyboardKey.arrowRight) {
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (key == LogicalKeyboardKey.select || key == LogicalKeyboardKey.enter) {
|
||||
if (_isActivateKey(key)) {
|
||||
onActivate();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
@@ -1517,7 +1548,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
if (key == LogicalKeyboardKey.arrowRight) {
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (key == LogicalKeyboardKey.select || key == LogicalKeyboardKey.enter) {
|
||||
if (_isActivateKey(key)) {
|
||||
onActivate();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
@@ -1548,7 +1579,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
if (key == LogicalKeyboardKey.arrowRight) {
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (key == LogicalKeyboardKey.select || key == LogicalKeyboardKey.enter) {
|
||||
if (_isActivateKey(key)) {
|
||||
onActivate();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
@@ -1579,13 +1610,21 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
if (key == LogicalKeyboardKey.arrowRight) {
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (key == LogicalKeyboardKey.select || key == LogicalKeyboardKey.enter) {
|
||||
if (_isActivateKey(key)) {
|
||||
onActivate();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
bool _isActivateKey(LogicalKeyboardKey key) {
|
||||
return key == LogicalKeyboardKey.enter ||
|
||||
key == LogicalKeyboardKey.select ||
|
||||
key == LogicalKeyboardKey.numpadEnter ||
|
||||
key == LogicalKeyboardKey.space ||
|
||||
key == LogicalKeyboardKey.gameButtonA;
|
||||
}
|
||||
|
||||
Widget _buildJumatTab(double s) {
|
||||
return FocusTraversalGroup(
|
||||
policy: WidgetOrderTraversalPolicy(),
|
||||
@@ -1743,7 +1782,6 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
_slideshowImages.length,
|
||||
(_) => row++,
|
||||
);
|
||||
final openPengumumanRow = row++;
|
||||
|
||||
return FocusTraversalGroup(
|
||||
policy: WidgetOrderTraversalPolicy(),
|
||||
@@ -2050,41 +2088,9 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
_buildTampilanActionButton(
|
||||
rowIndex: addSlideshowImageRow,
|
||||
s: s,
|
||||
onActivate: () async {
|
||||
final res = await FilePicker.platform.pickFiles(type: FileType.image, allowMultiple: true);
|
||||
if (res != null) {
|
||||
setState(() {
|
||||
for (var path in res.paths) {
|
||||
if (path != null &&
|
||||
File(path).existsSync() &&
|
||||
!_slideshowImages.contains(path)) {
|
||||
_slideshowImages.add(path);
|
||||
}
|
||||
}
|
||||
});
|
||||
_queueTampilanAutoSave(
|
||||
message: 'Galeri slideshow otomatis tersimpan',
|
||||
);
|
||||
}
|
||||
},
|
||||
onActivate: _pickSlideshowImages,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
final res = await FilePicker.platform.pickFiles(type: FileType.image, allowMultiple: true);
|
||||
if (res != null) {
|
||||
setState(() {
|
||||
for (var path in res.paths) {
|
||||
if (path != null &&
|
||||
File(path).existsSync() &&
|
||||
!_slideshowImages.contains(path)) {
|
||||
_slideshowImages.add(path);
|
||||
}
|
||||
}
|
||||
});
|
||||
_queueTampilanAutoSave(
|
||||
message: 'Galeri slideshow otomatis tersimpan',
|
||||
);
|
||||
}
|
||||
},
|
||||
onPressed: _pickSlideshowImages,
|
||||
icon: HugeIcon(icon: HugeIcons.strokeRoundedPlusSign, color: SacredColors.onPrimary, size: 18 * s),
|
||||
label: Text('TAMBAH FOTO', style: TextStyle(fontSize: 14 * s)),
|
||||
style: _tvElevatedActionStyle(
|
||||
@@ -2180,58 +2186,6 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 24 * s),
|
||||
_adminCard(
|
||||
s,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_sectionLabel('Pengumuman Dipisah ke Tab Sendiri', s),
|
||||
SizedBox(height: 8 * s),
|
||||
Text(
|
||||
'Text slide tengah dan running text bawah sekarang dipindahkan ke tab Pengumuman agar halaman Tampilan & Media lebih ringkas.',
|
||||
style: GoogleFonts.manrope(
|
||||
fontSize: 14 * s,
|
||||
color: SacredColors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16 * s),
|
||||
_buildTampilanActionButton(
|
||||
rowIndex: openPengumumanRow,
|
||||
s: s,
|
||||
onActivate: () {
|
||||
_setSelectedTab(3);
|
||||
_focusEntryForTab(3);
|
||||
},
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
_setSelectedTab(3);
|
||||
_focusEntryForTab(3);
|
||||
},
|
||||
icon: HugeIcon(
|
||||
icon: HugeIcons.strokeRoundedNotification03,
|
||||
color: SacredColors.onPrimary,
|
||||
size: 18 * s,
|
||||
),
|
||||
label: Text(
|
||||
'BUKA TAB PENGUMUMAN',
|
||||
style: TextStyle(fontSize: 14 * s),
|
||||
),
|
||||
style: _tvElevatedActionStyle(
|
||||
s: s,
|
||||
normalBackground: SacredColors.secondary,
|
||||
normalForeground: SacredColors.onPrimary,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 20 * s,
|
||||
vertical: 14 * s,
|
||||
),
|
||||
fontSize: 14 * s,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 40 * s),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
@@ -14,11 +15,15 @@ class UnsplashBackground extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class _UnsplashBackgroundState extends ConsumerState<UnsplashBackground> {
|
||||
final Random _rng = Random();
|
||||
List<String> _urls = [];
|
||||
int _currentIndex = 0;
|
||||
Timer? _rotationTimer;
|
||||
Timer? _keywordDebounceTimer;
|
||||
int _fetchNonce = 0;
|
||||
String? _lastKeyword;
|
||||
int? _lastRotationHours;
|
||||
bool? _lastUseUnsplash;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -30,18 +35,55 @@ class _UnsplashBackgroundState extends ConsumerState<UnsplashBackground> {
|
||||
final settings = ref.read(settingsProvider);
|
||||
_lastKeyword = settings.unsplashKeyword;
|
||||
_lastRotationHours = settings.unsplashRotationHours;
|
||||
_lastUseUnsplash = settings.useUnsplashBackground;
|
||||
|
||||
await _fetchImages(settings.unsplashKeyword);
|
||||
if (settings.useUnsplashBackground) {
|
||||
await _fetchImages(settings.unsplashKeyword, immediate: true);
|
||||
}
|
||||
_startTimer(settings.unsplashRotationHours);
|
||||
}
|
||||
|
||||
Future<void> _fetchImages(String keyword) async {
|
||||
final urls = await UnsplashService.instance.fetchLandscapeBackgrounds(keyword);
|
||||
if (urls.isNotEmpty && mounted) {
|
||||
setState(() {
|
||||
_urls = urls;
|
||||
_currentIndex = 0;
|
||||
});
|
||||
Future<void> _fetchImages(String keyword, {bool immediate = false}) async {
|
||||
_keywordDebounceTimer?.cancel();
|
||||
Future<void> runFetch() async {
|
||||
if (!ref.read(settingsProvider).useUnsplashBackground) return;
|
||||
final requestId = ++_fetchNonce;
|
||||
final randomPage = 1 + _rng.nextInt(10);
|
||||
final urls = await UnsplashService.instance.fetchLandscapeBackgrounds(
|
||||
keyword,
|
||||
page: randomPage,
|
||||
);
|
||||
if (!mounted || requestId != _fetchNonce) return;
|
||||
if (urls.isNotEmpty) {
|
||||
urls.shuffle(_rng);
|
||||
setState(() {
|
||||
_urls = urls;
|
||||
_currentIndex = _rng.nextInt(_urls.length);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (immediate) {
|
||||
await runFetch();
|
||||
return;
|
||||
}
|
||||
|
||||
_keywordDebounceTimer = Timer(
|
||||
const Duration(milliseconds: 1200),
|
||||
() {
|
||||
unawaited(runFetch());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _nextRandomImage() {
|
||||
if (_urls.isNotEmpty && mounted) {
|
||||
if (_urls.length <= 1) return;
|
||||
var nextIndex = _currentIndex;
|
||||
while (nextIndex == _currentIndex) {
|
||||
nextIndex = _rng.nextInt(_urls.length);
|
||||
}
|
||||
setState(() => _currentIndex = nextIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,17 +92,14 @@ class _UnsplashBackgroundState extends ConsumerState<UnsplashBackground> {
|
||||
if (hours <= 0) return;
|
||||
|
||||
_rotationTimer = Timer.periodic(Duration(hours: hours), (_) {
|
||||
if (_urls.isNotEmpty && mounted) {
|
||||
setState(() {
|
||||
_currentIndex = (_currentIndex + 1) % _urls.length;
|
||||
});
|
||||
}
|
||||
_nextRandomImage();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_rotationTimer?.cancel();
|
||||
_keywordDebounceTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -69,12 +108,20 @@ class _UnsplashBackgroundState extends ConsumerState<UnsplashBackground> {
|
||||
final settings = ref.watch(settingsProvider);
|
||||
|
||||
// Watch for config changes
|
||||
if (settings.useUnsplashBackground != _lastUseUnsplash) {
|
||||
_lastUseUnsplash = settings.useUnsplashBackground;
|
||||
if (settings.useUnsplashBackground && _urls.isEmpty) {
|
||||
_fetchImages(settings.unsplashKeyword, immediate: true);
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.unsplashKeyword != _lastKeyword) {
|
||||
_lastKeyword = settings.unsplashKeyword;
|
||||
// Re-fetch images organically
|
||||
_fetchImages(settings.unsplashKeyword);
|
||||
if (settings.useUnsplashBackground) {
|
||||
_fetchImages(settings.unsplashKeyword);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (settings.unsplashRotationHours != _lastRotationHours) {
|
||||
_lastRotationHours = settings.unsplashRotationHours;
|
||||
_startTimer(settings.unsplashRotationHours);
|
||||
|
||||
Reference in New Issue
Block a user