124 lines
4.5 KiB
Dart
124 lines
4.5 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:path/path.dart' as p;
|
|
|
|
import '../app/app_controller.dart';
|
|
import '../core/file_ops.dart' show FileEntry;
|
|
import '../core/lan_scan.dart' show activeMeteredOf;
|
|
import '../core/playlist.dart' show isVideoFile;
|
|
import '../core/remote_stream.dart';
|
|
import '../core/remote_thumbs.dart';
|
|
import 'theme.dart';
|
|
|
|
/// X-plore 비교 5-1 ② (관리자 지시 10/21): 원격 썸네일을 쓰는 쪽 (설정 · 망 · FFmpeg) - 앱에 하나
|
|
Future<RemoteThumbs>? _hub;
|
|
|
|
Future<RemoteThumbs> remoteThumbsOf(AppController c) => _hub ??= () async {
|
|
final dir = Directory(p.join(await c.services.storage.tempDirectory(), 'jj_rthumbs'));
|
|
return RemoteThumbs(
|
|
dir: dir,
|
|
maxBytes: () => c.settings.explorerRemoteThumbCacheMb * 1024 * 1024,
|
|
allowed: () => remoteThumbsAllowedNow(c),
|
|
make: (path, out) => _make(c, path, out),
|
|
);
|
|
}();
|
|
|
|
/// 시험: 다음에 새로 만든다
|
|
void resetRemoteThumbsForTest() => _hub = null;
|
|
|
|
/// 원격 그림은 이보다 크면 받지 않는다 (썸네일 하나에 큰 파일을 통째로 받지 않게)
|
|
const remoteThumbImageMax = 30 * 1024 * 1024;
|
|
|
|
const _images = {'jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'};
|
|
|
|
/// 원격 썸네일을 받을 종류인지 (그림 - [remoteThumbImageMax] 아래 · 동영상)
|
|
bool remoteThumbKind(FileEntry e) => !e.isDir && (isVideoFile(e.path) || (_images.contains(e.ext) && e.size <= remoteThumbImageMax));
|
|
|
|
DateTime? _meteredAt;
|
|
bool _metered = true;
|
|
|
|
/// 지금 받아도 되는지: 'off' 아니오 · 'always' 예 · 'wifi' (처음 값) 는 데이터 요금제가 아닌 망에서만 (10초 동안 기억)
|
|
Future<bool> remoteThumbsAllowedNow(AppController c) async {
|
|
switch (c.settings.explorerRemoteThumbs) {
|
|
case 'off':
|
|
return false;
|
|
case 'always':
|
|
return true;
|
|
}
|
|
final now = DateTime.now();
|
|
if (_meteredAt == null || now.difference(_meteredAt!) > const Duration(seconds: 10)) {
|
|
_metered = await activeMeteredOf();
|
|
_meteredAt = now;
|
|
}
|
|
return !_metered;
|
|
}
|
|
|
|
/// 시험: 망 판단 기억을 지운다
|
|
void resetRemoteThumbNetForTest() => _meteredAt = null;
|
|
|
|
/// FFmpeg 가 이 기기 안 스트리밍 다리 (127.0.0.1, Range) 에서 필요한 부분만 읽어 가로 160 jpg 로. 비밀번호는 명령 줄에 넣지 않는다.
|
|
/// MKV 만들기 등 작업 중에는 만들지 않는다 (FFmpeg 를 같이 씀)
|
|
Future<bool> _make(AppController c, String path, String out) async {
|
|
if (c.busy) return false;
|
|
final url = (await RemoteStreamBridge.get()).url(path);
|
|
for (final at in isVideoFile(path) ? ['5', '0'] : [null]) {
|
|
await c.services.mediaTool.runFfmpeg([
|
|
'-hide_banner', '-loglevel', 'error', if (at != null) ...['-ss', at], '-i', url, //
|
|
'-frames:v', '1', '-vf', 'scale=160:-2', '-y', out,
|
|
]);
|
|
if (await File(out).exists() && await File(out).length() > 0) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// 목록 줄의 원격 썸네일 (보이는 동안만 달라고 한다 - 화면에서 사라지면 줄 세운 것은 시작하지 않음)
|
|
class RemoteThumb extends StatefulWidget {
|
|
final AppController c;
|
|
final FileEntry entry;
|
|
final Widget fallback;
|
|
const RemoteThumb({super.key, required this.c, required this.entry, required this.fallback});
|
|
|
|
@override
|
|
State<RemoteThumb> createState() => _RemoteThumbState();
|
|
}
|
|
|
|
class _RemoteThumbState extends State<RemoteThumb> {
|
|
File? _file;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
unawaited(_load());
|
|
}
|
|
|
|
Future<void> _load() async {
|
|
final e = widget.entry;
|
|
final key = RemoteThumbs.keyOf(e.path, e.size, e.modified);
|
|
final hub = await remoteThumbsOf(widget.c);
|
|
final had = hub.cached(key);
|
|
if (had != null) {
|
|
if (mounted) setState(() => _file = had);
|
|
return;
|
|
}
|
|
final f = await hub.request(e.path, key, wanted: () => mounted);
|
|
if (mounted && f != null) setState(() => _file = f);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final f = _file;
|
|
if (f == null) return widget.fallback;
|
|
return ClipRRect(
|
|
borderRadius: BorderRadius.circular(3),
|
|
child: Stack(alignment: Alignment.center, children: [
|
|
Image.file(f, width: 56, height: 44, fit: BoxFit.cover, errorBuilder: (_, _, _) => widget.fallback),
|
|
if (isVideoFile(widget.entry.path)) const Icon(Icons.play_arrow, size: 18, color: Colors.white70),
|
|
// 원격에서 온 것 표시 (작게)
|
|
Positioned(right: 1, bottom: 1, child: Icon(Icons.cloud_outlined, size: 10, color: JjColors.textDim)),
|
|
]),
|
|
);
|
|
}
|
|
}
|