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);