fix(tv-picker): native Android TV image picker for branded/slideshow + bump 1.0.10+11
This commit is contained in:
@@ -1,5 +1,217 @@
|
|||||||
package com.jamshalat.jamshalat_masjid_screen
|
package com.jamshalat.jamshalat_masjid_screen
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.provider.DocumentsContract
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.provider.OpenableColumns
|
||||||
|
import android.webkit.MimeTypeMap
|
||||||
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
import io.flutter.plugin.common.MethodCall
|
||||||
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
class MainActivity : FlutterActivity()
|
class MainActivity : FlutterActivity() {
|
||||||
|
private companion object {
|
||||||
|
const val CHANNEL_NAME = "jamshalat/tv_media_picker"
|
||||||
|
const val METHOD_PICK_IMAGES = "pickImages"
|
||||||
|
const val METHOD_LIST_HANDLERS = "listPickers"
|
||||||
|
const val REQUEST_PICK_IMAGES = 49011
|
||||||
|
}
|
||||||
|
|
||||||
|
private var pendingResult: MethodChannel.Result? = null
|
||||||
|
|
||||||
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
|
super.configureFlutterEngine(flutterEngine)
|
||||||
|
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL_NAME)
|
||||||
|
.setMethodCallHandler { call, result ->
|
||||||
|
when (call.method) {
|
||||||
|
METHOD_PICK_IMAGES -> handlePickImages(call, result)
|
||||||
|
METHOD_LIST_HANDLERS -> result.success(listPickers())
|
||||||
|
else -> result.notImplemented()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handlePickImages(call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
if (pendingResult != null) {
|
||||||
|
result.error("BUSY", "Media picker request is already running.", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val allowMultiple = call.argument<Boolean>("allowMultiple") ?: false
|
||||||
|
val intent = createPickIntent(allowMultiple)
|
||||||
|
if (intent == null) {
|
||||||
|
result.error(
|
||||||
|
"NO_PICKER",
|
||||||
|
"No compatible file manager/document picker found.",
|
||||||
|
listPickers(),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingResult = result
|
||||||
|
startActivityForResult(intent, REQUEST_PICK_IMAGES)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
if (requestCode != REQUEST_PICK_IMAGES) return
|
||||||
|
|
||||||
|
val result = pendingResult ?: return
|
||||||
|
pendingResult = null
|
||||||
|
|
||||||
|
if (resultCode != Activity.RESULT_OK) {
|
||||||
|
result.success(emptyList<String>())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val uris = mutableListOf<Uri>()
|
||||||
|
data?.data?.let { uris.add(it) }
|
||||||
|
data?.clipData?.let { clip ->
|
||||||
|
for (index in 0 until clip.itemCount) {
|
||||||
|
clip.getItemAt(index).uri?.let { uris.add(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val uniqueUris = uris.distinctBy { it.toString() }
|
||||||
|
if (uniqueUris.isEmpty()) {
|
||||||
|
result.success(emptyList<String>())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val copiedPaths = mutableListOf<String>()
|
||||||
|
for (uri in uniqueUris) {
|
||||||
|
copyUriToCache(uri)?.let { copiedPaths.add(it) }
|
||||||
|
}
|
||||||
|
result.success(copiedPaths)
|
||||||
|
} catch (error: Exception) {
|
||||||
|
result.error("PICK_FAILED", error.message, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createPickIntent(allowMultiple: Boolean): Intent? {
|
||||||
|
val openDocumentIntent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
type = "image/*"
|
||||||
|
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple)
|
||||||
|
putExtra(Intent.EXTRA_LOCAL_ONLY, true)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
putExtra(
|
||||||
|
DocumentsContract.EXTRA_INITIAL_URI,
|
||||||
|
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (openDocumentIntent.resolveActivity(packageManager) != null) {
|
||||||
|
return openDocumentIntent
|
||||||
|
}
|
||||||
|
|
||||||
|
val getContentIntent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
type = "image/*"
|
||||||
|
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple)
|
||||||
|
}
|
||||||
|
if (getContentIntent.resolveActivity(packageManager) != null) {
|
||||||
|
return Intent.createChooser(getContentIntent, "Pilih gambar")
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun listPickers(): List<Map<String, String>> {
|
||||||
|
val result = LinkedHashMap<String, Map<String, String>>()
|
||||||
|
val intents = listOf(
|
||||||
|
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
type = "image/*"
|
||||||
|
},
|
||||||
|
Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
type = "image/*"
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
for (intent in intents) {
|
||||||
|
val resolved = packageManager.queryIntentActivities(intent, 0)
|
||||||
|
for (info in resolved) {
|
||||||
|
val packageName = info.activityInfo.packageName ?: continue
|
||||||
|
if (result.containsKey(packageName)) continue
|
||||||
|
val label = info.loadLabel(packageManager)?.toString()?.trim().orEmpty()
|
||||||
|
result[packageName] = mapOf(
|
||||||
|
"packageName" to packageName,
|
||||||
|
"label" to if (label.isEmpty()) packageName else label,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.values.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun copyUriToCache(uri: Uri): String? {
|
||||||
|
if (uri.scheme == "content") {
|
||||||
|
try {
|
||||||
|
contentResolver.takePersistableUriPermission(
|
||||||
|
uri,
|
||||||
|
Intent.FLAG_GRANT_READ_URI_PERMISSION,
|
||||||
|
)
|
||||||
|
} catch (_: SecurityException) {
|
||||||
|
// Non-persistable provider, ignore.
|
||||||
|
} catch (_: UnsupportedOperationException) {
|
||||||
|
// Provider doesn't support persisted grants.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val inputStream = contentResolver.openInputStream(uri) ?: return null
|
||||||
|
inputStream.use { input ->
|
||||||
|
val ext = resolveExtension(uri)
|
||||||
|
val folder = File(cacheDir, "picked_images")
|
||||||
|
if (!folder.exists()) {
|
||||||
|
folder.mkdirs()
|
||||||
|
}
|
||||||
|
|
||||||
|
val fileName = "img_${System.currentTimeMillis()}_${Random.nextInt(1000, 9999)}.$ext"
|
||||||
|
val outputFile = File(folder, fileName)
|
||||||
|
FileOutputStream(outputFile).use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
return outputFile.absolutePath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveExtension(uri: Uri): String {
|
||||||
|
val mimeType = contentResolver.getType(uri)
|
||||||
|
val extFromMime = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)
|
||||||
|
if (!extFromMime.isNullOrBlank()) {
|
||||||
|
return extFromMime
|
||||||
|
}
|
||||||
|
|
||||||
|
val displayName = queryDisplayName(uri)
|
||||||
|
if (!displayName.isNullOrBlank()) {
|
||||||
|
val dot = displayName.lastIndexOf('.')
|
||||||
|
if (dot in 1 until displayName.length - 1) {
|
||||||
|
return displayName.substring(dot + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "jpg"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun queryDisplayName(uri: Uri): String? {
|
||||||
|
contentResolver.query(
|
||||||
|
uri,
|
||||||
|
arrayOf(OpenableColumns.DISPLAY_NAME),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
)?.use { cursor ->
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
return cursor.getString(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
82
lib/data/services/tv_media_picker_service.dart
Normal file
82
lib/data/services/tv_media_picker_service.dart
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
class TvPickerHandler {
|
||||||
|
final String packageName;
|
||||||
|
final String label;
|
||||||
|
|
||||||
|
const TvPickerHandler({
|
||||||
|
required this.packageName,
|
||||||
|
required this.label,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class TvMediaPickerUnavailable implements Exception {
|
||||||
|
final String message;
|
||||||
|
final List<TvPickerHandler> handlers;
|
||||||
|
|
||||||
|
const TvMediaPickerUnavailable({
|
||||||
|
required this.message,
|
||||||
|
required this.handlers,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class TvMediaPickerService {
|
||||||
|
TvMediaPickerService._();
|
||||||
|
static final TvMediaPickerService instance = TvMediaPickerService._();
|
||||||
|
|
||||||
|
static const MethodChannel _channel =
|
||||||
|
MethodChannel('jamshalat/tv_media_picker');
|
||||||
|
|
||||||
|
Future<List<String>> pickImages({
|
||||||
|
required bool allowMultiple,
|
||||||
|
}) async {
|
||||||
|
if (!Platform.isAndroid) return const [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
final raw = await _channel.invokeMethod<List<dynamic>>(
|
||||||
|
'pickImages',
|
||||||
|
{'allowMultiple': allowMultiple},
|
||||||
|
);
|
||||||
|
if (raw == null) return const [];
|
||||||
|
return raw
|
||||||
|
.map((item) => item?.toString() ?? '')
|
||||||
|
.where((path) => path.isNotEmpty)
|
||||||
|
.toList(growable: false);
|
||||||
|
} on PlatformException catch (error) {
|
||||||
|
if (error.code == 'NO_PICKER') {
|
||||||
|
throw TvMediaPickerUnavailable(
|
||||||
|
message: error.message ??
|
||||||
|
'Tidak ada aplikasi pemilih file yang kompatibel di perangkat.',
|
||||||
|
handlers: _parseHandlers(error.details),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<TvPickerHandler>> listPickers() async {
|
||||||
|
if (!Platform.isAndroid) return const [];
|
||||||
|
final raw = await _channel.invokeMethod<List<dynamic>>('listPickers');
|
||||||
|
return _parseHandlers(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<TvPickerHandler> _parseHandlers(dynamic raw) {
|
||||||
|
if (raw is! List) return const [];
|
||||||
|
final handlers = <TvPickerHandler>[];
|
||||||
|
for (final item in raw) {
|
||||||
|
if (item is! Map) continue;
|
||||||
|
final packageName = item['packageName']?.toString() ?? '';
|
||||||
|
final label = item['label']?.toString() ?? '';
|
||||||
|
if (packageName.isEmpty) continue;
|
||||||
|
handlers.add(
|
||||||
|
TvPickerHandler(
|
||||||
|
packageName: packageName,
|
||||||
|
label: label.isEmpty ? packageName : label,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return handlers;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,19 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:math';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:hugeicons/hugeicons.dart';
|
import 'package:hugeicons/hugeicons.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
import '../../core/sacred_tokens.dart';
|
import '../../core/sacred_tokens.dart';
|
||||||
import '../../providers.dart';
|
import '../../providers.dart';
|
||||||
import '../../data/services/sync_service.dart';
|
import '../../data/services/sync_service.dart';
|
||||||
import '../../data/services/myquran_service.dart';
|
import '../../data/services/myquran_service.dart';
|
||||||
import '../../data/services/sound_service.dart';
|
import '../../data/services/sound_service.dart';
|
||||||
|
import '../../data/services/tv_media_picker_service.dart';
|
||||||
import '../../data/services/update_service.dart';
|
import '../../data/services/update_service.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
@@ -344,20 +347,31 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _pickBrandedImage() async {
|
||||||
|
try {
|
||||||
|
final pickedPaths = await _pickImagePaths(allowMultiple: false);
|
||||||
|
if (pickedPaths.isEmpty) return;
|
||||||
|
setState(() => _brandedBgImage = pickedPaths.first);
|
||||||
|
_queueTampilanAutoSave(
|
||||||
|
message: 'Foto latar otomatis tersimpan',
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
_showStatusBadge(
|
||||||
|
'Gagal membuka pemilih file. Pastikan file manager tersedia di perangkat.',
|
||||||
|
isError: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _pickSlideshowImages() async {
|
Future<void> _pickSlideshowImages() async {
|
||||||
try {
|
try {
|
||||||
final res = await FilePicker.platform.pickFiles(
|
final pickedPaths = await _pickImagePaths(allowMultiple: true);
|
||||||
type: FileType.image,
|
if (pickedPaths.isEmpty) return;
|
||||||
allowMultiple: true,
|
|
||||||
);
|
|
||||||
if (res == null) return;
|
|
||||||
|
|
||||||
var hasNewImage = false;
|
var hasNewImage = false;
|
||||||
setState(() {
|
setState(() {
|
||||||
for (final path in res.paths) {
|
for (final path in pickedPaths) {
|
||||||
if (path != null &&
|
if (File(path).existsSync() && !_slideshowImages.contains(path)) {
|
||||||
File(path).existsSync() &&
|
|
||||||
!_slideshowImages.contains(path)) {
|
|
||||||
_slideshowImages.add(path);
|
_slideshowImages.add(path);
|
||||||
hasNewImage = true;
|
hasNewImage = true;
|
||||||
}
|
}
|
||||||
@@ -378,6 +392,88 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<String>> _pickImagePaths({
|
||||||
|
required bool allowMultiple,
|
||||||
|
}) async {
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
try {
|
||||||
|
return await TvMediaPickerService.instance.pickImages(
|
||||||
|
allowMultiple: allowMultiple,
|
||||||
|
);
|
||||||
|
} on TvMediaPickerUnavailable catch (error) {
|
||||||
|
if (!mounted) return const [];
|
||||||
|
final detected = error.handlers.map((handler) => handler.label).join(', ');
|
||||||
|
final supported = [
|
||||||
|
'File Commander',
|
||||||
|
'X-plore',
|
||||||
|
'Cx File Explorer',
|
||||||
|
'Files by Google',
|
||||||
|
].join(', ');
|
||||||
|
final message = detected.isNotEmpty
|
||||||
|
? 'Pemilih TV tidak tersedia. Aplikasi terdeteksi: $detected. Gunakan salah satu yang mendukung pemilih dokumen: $supported.'
|
||||||
|
: 'Tidak ada pemilih file Android TV yang kompatibel. Instal salah satu: $supported.';
|
||||||
|
_showStatusBadge(message, isError: true);
|
||||||
|
return const [];
|
||||||
|
} on MissingPluginException {
|
||||||
|
// Fallback below if native Android channel is not available.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final picked = await FilePicker.platform.pickFiles(
|
||||||
|
type: FileType.image,
|
||||||
|
allowMultiple: allowMultiple,
|
||||||
|
withData: true,
|
||||||
|
withReadStream: true,
|
||||||
|
);
|
||||||
|
if (picked == null) return const [];
|
||||||
|
|
||||||
|
final resolvedPaths = <String>[];
|
||||||
|
for (final file in picked.files) {
|
||||||
|
if (file.path != null && File(file.path!).existsSync()) {
|
||||||
|
resolvedPaths.add(file.path!);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final persisted = await _persistPickedImageFile(file);
|
||||||
|
if (persisted != null) {
|
||||||
|
resolvedPaths.add(persisted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resolvedPaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> _persistPickedImageFile(PlatformFile file) async {
|
||||||
|
final hasBytes = file.bytes != null;
|
||||||
|
final stream = file.readStream;
|
||||||
|
if (!hasBytes && stream == null) return null;
|
||||||
|
|
||||||
|
final supportDir = await getApplicationSupportDirectory();
|
||||||
|
final mediaDir = Directory('${supportDir.path}/picked_images');
|
||||||
|
await mediaDir.create(recursive: true);
|
||||||
|
|
||||||
|
final ext = _extractImageExtension(file.name);
|
||||||
|
final target = File(
|
||||||
|
'${mediaDir.path}/img_${DateTime.now().millisecondsSinceEpoch}_${1000 + Random().nextInt(9000)}.$ext',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (file.bytes != null) {
|
||||||
|
await target.writeAsBytes(file.bytes!, flush: true);
|
||||||
|
return target.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
final sink = target.openWrite();
|
||||||
|
await stream!.pipe(sink);
|
||||||
|
await sink.close();
|
||||||
|
return target.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _extractImageExtension(String name) {
|
||||||
|
final dot = name.lastIndexOf('.');
|
||||||
|
if (dot > 0 && dot < name.length - 1) {
|
||||||
|
return name.substring(dot + 1).toLowerCase();
|
||||||
|
}
|
||||||
|
return 'jpg';
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _savePengumuman({
|
Future<void> _savePengumuman({
|
||||||
String message = 'Pengaturan pengumuman otomatis tersimpan',
|
String message = 'Pengaturan pengumuman otomatis tersimpan',
|
||||||
}) async {
|
}) async {
|
||||||
@@ -1986,28 +2082,9 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
|||||||
_buildTampilanActionButton(
|
_buildTampilanActionButton(
|
||||||
rowIndex: pickBrandedBgRow,
|
rowIndex: pickBrandedBgRow,
|
||||||
s: s,
|
s: s,
|
||||||
onActivate: () async {
|
onActivate: _pickBrandedImage,
|
||||||
final res = await FilePicker.platform.pickFiles(type: FileType.image);
|
|
||||||
final selectedPath = res?.files.single.path;
|
|
||||||
if (selectedPath != null && File(selectedPath).existsSync()) {
|
|
||||||
setState(() => _brandedBgImage = selectedPath);
|
|
||||||
_queueTampilanAutoSave(
|
|
||||||
message: 'Foto latar otomatis tersimpan',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
onPressed: () async {
|
onPressed: _pickBrandedImage,
|
||||||
final res = await FilePicker.platform.pickFiles(type: FileType.image);
|
|
||||||
final selectedPath = res?.files.single.path;
|
|
||||||
if (selectedPath != null &&
|
|
||||||
File(selectedPath).existsSync()) {
|
|
||||||
setState(() => _brandedBgImage = selectedPath);
|
|
||||||
_queueTampilanAutoSave(
|
|
||||||
message: 'Foto latar otomatis tersimpan',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon: HugeIcon(icon: HugeIcons.strokeRoundedImage01, color: SacredColors.onPrimary, size: 20 * s),
|
icon: HugeIcon(icon: HugeIcons.strokeRoundedImage01, color: SacredColors.onPrimary, size: 20 * s),
|
||||||
label: Text('PILIH FOTO MASJID', style: TextStyle(fontSize: 16 * s)),
|
label: Text('PILIH FOTO MASJID', style: TextStyle(fontSize: 16 * s)),
|
||||||
style: _tvElevatedActionStyle(
|
style: _tvElevatedActionStyle(
|
||||||
|
|||||||
@@ -48,10 +48,8 @@ class _UnsplashBackgroundState extends ConsumerState<UnsplashBackground> {
|
|||||||
Future<void> runFetch() async {
|
Future<void> runFetch() async {
|
||||||
if (!ref.read(settingsProvider).useUnsplashBackground) return;
|
if (!ref.read(settingsProvider).useUnsplashBackground) return;
|
||||||
final requestId = ++_fetchNonce;
|
final requestId = ++_fetchNonce;
|
||||||
final randomPage = 1 + _rng.nextInt(10);
|
|
||||||
final urls = await UnsplashService.instance.fetchLandscapeBackgrounds(
|
final urls = await UnsplashService.instance.fetchLandscapeBackgrounds(
|
||||||
keyword,
|
keyword,
|
||||||
page: randomPage,
|
|
||||||
);
|
);
|
||||||
if (!mounted || requestId != _fetchNonce) return;
|
if (!mounted || requestId != _fetchNonce) return;
|
||||||
if (urls.isNotEmpty) {
|
if (urls.isNotEmpty) {
|
||||||
|
|||||||
@@ -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.9+10
|
version: 1.0.10+11
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.0.0 <4.0.0'
|
sdk: '>=3.0.0 <4.0.0'
|
||||||
|
|||||||
Reference in New Issue
Block a user