From 98b8437e877d419b764a376431ee4a83a9a919e4 Mon Sep 17 00:00:00 2001 From: dwindown Date: Sun, 5 Apr 2026 14:59:55 +0700 Subject: [PATCH] feat(tv-admin): fix action/focus flows, update app title, randomize unsplash, bump 1.0.9+10 --- android/app/src/main/AndroidManifest.xml | 2 +- ios/Runner/Info.plist | 4 +- lib/data/services/unsplash_service.dart | 16 ++- lib/features/admin/admin_screen.dart | 148 +++++++-------------- lib/features/home/unsplash_background.dart | 79 ++++++++--- lib/main.dart | 2 +- linux/runner/my_application.cc | 4 +- macos/Runner/Info.plist | 4 +- pubspec.yaml | 2 +- web/index.html | 4 +- web/manifest.json | 4 +- windows/runner/Runner.rc | 4 +- windows/runner/main.cpp | 2 +- 13 files changed, 144 insertions(+), 131 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 2376e72..89172bb 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -2,7 +2,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - Jamshalat Masjid Screen + JamShalat - Masjid Screen CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -15,7 +15,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - jamshalat_masjid_screen + JamShalat - Masjid Screen CFBundlePackageType APPL CFBundleShortVersionString diff --git a/lib/data/services/unsplash_service.dart b/lib/data/services/unsplash_service.dart index 6ab42bb..0cd9d8e 100644 --- a/lib/data/services/unsplash_service.dart +++ b/lib/data/services/unsplash_service.dart @@ -10,12 +10,22 @@ class UnsplashService { UnsplashService._(); /// Fetches a list of highly compressed landscape URLs based on the given keyword. - Future> fetchLandscapeBackgrounds(String keyword) async { + Future> fetchLandscapeBackgrounds( + String keyword, { + int page = 1, + }) async { // Trim keyword and default to 'mosque' if empty final query = keyword.trim().isEmpty ? 'mosque' : keyword.trim(); - + // Specifically requesting 'regular' size to fit 1080p elegantly while minimizing RAM overhead. - final url = Uri.parse('$_baseUrl/search/photos?query=$query&orientation=landscape&per_page=20'); + final url = Uri.parse('$_baseUrl/search/photos').replace( + queryParameters: { + 'query': query, + 'orientation': 'landscape', + 'per_page': '20', + 'page': page.clamp(1, 1000).toString(), + }, + ); try { final response = await http.get( diff --git a/lib/features/admin/admin_screen.dart b/lib/features/admin/admin_screen.dart index 90563e7..0097db1 100644 --- a/lib/features/admin/admin_screen.dart +++ b/lib/features/admin/admin_screen.dart @@ -344,6 +344,40 @@ class _AdminScreenState extends ConsumerState { ); } + Future _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 _savePengumuman({ String message = 'Pengaturan pengumuman otomatis tersimpan', }) async { @@ -1270,7 +1304,6 @@ class _AdminScreenState extends ConsumerState { count += 1; count += 1; count += _slideshowImages.length; - count += 1; return count; } @@ -1375,9 +1408,7 @@ class _AdminScreenState extends ConsumerState { _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 { 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 { 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 { 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 { 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 { 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 { 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 { _slideshowImages.length, (_) => row++, ); - final openPengumumanRow = row++; return FocusTraversalGroup( policy: WidgetOrderTraversalPolicy(), @@ -2050,41 +2088,9 @@ class _AdminScreenState extends ConsumerState { _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 { ], ), ), - 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), ], ), diff --git a/lib/features/home/unsplash_background.dart b/lib/features/home/unsplash_background.dart index 3cadec2..fb992c4 100644 --- a/lib/features/home/unsplash_background.dart +++ b/lib/features/home/unsplash_background.dart @@ -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 { + final Random _rng = Random(); List _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 { 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 _fetchImages(String keyword) async { - final urls = await UnsplashService.instance.fetchLandscapeBackgrounds(keyword); - if (urls.isNotEmpty && mounted) { - setState(() { - _urls = urls; - _currentIndex = 0; - }); + Future _fetchImages(String keyword, {bool immediate = false}) async { + _keywordDebounceTimer?.cancel(); + Future 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 { 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 { 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); diff --git a/lib/main.dart b/lib/main.dart index cb633f0..3a866a1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -125,7 +125,7 @@ class JamShalatApp extends ConsumerWidget { // textScaleProvider will be used selectively in child components. return MaterialApp( - title: 'Jam Shalat Digital', + title: 'JamShalat - Masjid Screen', debugShowCheckedModeBanner: false, theme: ThemeData( useMaterial3: true, diff --git a/linux/runner/my_application.cc b/linux/runner/my_application.cc index a89a973..ff96a53 100644 --- a/linux/runner/my_application.cc +++ b/linux/runner/my_application.cc @@ -45,11 +45,11 @@ static void my_application_activate(GApplication* application) { if (use_header_bar) { GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); gtk_widget_show(GTK_WIDGET(header_bar)); - gtk_header_bar_set_title(header_bar, "jamshalat_masjid_screen"); + gtk_header_bar_set_title(header_bar, "JamShalat - Masjid Screen"); gtk_header_bar_set_show_close_button(header_bar, TRUE); gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); } else { - gtk_window_set_title(window, "jamshalat_masjid_screen"); + gtk_window_set_title(window, "JamShalat - Masjid Screen"); } gtk_window_set_default_size(window, 1280, 720); diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist index 4789daa..ffab510 100644 --- a/macos/Runner/Info.plist +++ b/macos/Runner/Info.plist @@ -12,8 +12,10 @@ $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 + CFBundleDisplayName + JamShalat - Masjid Screen CFBundleName - $(PRODUCT_NAME) + JamShalat - Masjid Screen CFBundlePackageType APPL CFBundleShortVersionString diff --git a/pubspec.yaml b/pubspec.yaml index 4c3157a..3762756 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: jamshalat_masjid_screen description: Smart Digital Prayer Clock for Android TV Box publish_to: 'none' -version: 1.0.8+9 +version: 1.0.9+10 environment: sdk: '>=3.0.0 <4.0.0' diff --git a/web/index.html b/web/index.html index 3b7c541..df27f15 100644 --- a/web/index.html +++ b/web/index.html @@ -23,13 +23,13 @@ - + - jamshalat_masjid_screen + JamShalat - Masjid Screen diff --git a/web/manifest.json b/web/manifest.json index 42a3fc8..86f7c68 100644 --- a/web/manifest.json +++ b/web/manifest.json @@ -1,6 +1,6 @@ { - "name": "jamshalat_masjid_screen", - "short_name": "jamshalat_masjid_screen", + "name": "JamShalat - Masjid Screen", + "short_name": "JamShalat", "start_url": ".", "display": "standalone", "background_color": "#0175C2", diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc index 923354f..ccda7c8 100644 --- a/windows/runner/Runner.rc +++ b/windows/runner/Runner.rc @@ -90,12 +90,12 @@ BEGIN BLOCK "040904e4" BEGIN VALUE "CompanyName", "com.jamshalat" "\0" - VALUE "FileDescription", "jamshalat_masjid_screen" "\0" + VALUE "FileDescription", "JamShalat - Masjid Screen" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "InternalName", "jamshalat_masjid_screen" "\0" VALUE "LegalCopyright", "Copyright (C) 2026 com.jamshalat. All rights reserved." "\0" VALUE "OriginalFilename", "jamshalat_masjid_screen.exe" "\0" - VALUE "ProductName", "jamshalat_masjid_screen" "\0" + VALUE "ProductName", "JamShalat - Masjid Screen" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" END END diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp index c6ce34d..08f02c0 100644 --- a/windows/runner/main.cpp +++ b/windows/runner/main.cpp @@ -27,7 +27,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); - if (!window.Create(L"jamshalat_masjid_screen", origin, size)) { + if (!window.Create(L"JamShalat - Masjid Screen", origin, size)) { return EXIT_FAILURE; } window.SetQuitOnClose(true);