fix(tv-picker): native Android TV image picker for branded/slideshow + bump 1.0.10+11
This commit is contained in:
@@ -1,16 +1,19 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:hugeicons/hugeicons.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../../core/sacred_tokens.dart';
|
||||
import '../../providers.dart';
|
||||
import '../../data/services/sync_service.dart';
|
||||
import '../../data/services/myquran_service.dart';
|
||||
import '../../data/services/sound_service.dart';
|
||||
import '../../data/services/tv_media_picker_service.dart';
|
||||
import '../../data/services/update_service.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
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 {
|
||||
try {
|
||||
final res = await FilePicker.platform.pickFiles(
|
||||
type: FileType.image,
|
||||
allowMultiple: true,
|
||||
);
|
||||
if (res == null) return;
|
||||
|
||||
final pickedPaths = await _pickImagePaths(allowMultiple: true);
|
||||
if (pickedPaths.isEmpty) return;
|
||||
var hasNewImage = false;
|
||||
setState(() {
|
||||
for (final path in res.paths) {
|
||||
if (path != null &&
|
||||
File(path).existsSync() &&
|
||||
!_slideshowImages.contains(path)) {
|
||||
for (final path in pickedPaths) {
|
||||
if (File(path).existsSync() && !_slideshowImages.contains(path)) {
|
||||
_slideshowImages.add(path);
|
||||
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({
|
||||
String message = 'Pengaturan pengumuman otomatis tersimpan',
|
||||
}) async {
|
||||
@@ -1986,28 +2082,9 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
||||
_buildTampilanActionButton(
|
||||
rowIndex: pickBrandedBgRow,
|
||||
s: s,
|
||||
onActivate: () async {
|
||||
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',
|
||||
);
|
||||
}
|
||||
},
|
||||
onActivate: _pickBrandedImage,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
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',
|
||||
);
|
||||
}
|
||||
},
|
||||
onPressed: _pickBrandedImage,
|
||||
icon: HugeIcon(icon: HugeIcons.strokeRoundedImage01, color: SacredColors.onPrimary, size: 20 * s),
|
||||
label: Text('PILIH FOTO MASJID', style: TextStyle(fontSize: 16 * s)),
|
||||
style: _tvElevatedActionStyle(
|
||||
|
||||
Reference in New Issue
Block a user