JJ_MKVMaker/lib/ui/explorer_grid.dart

119 lines
4.3 KiB
Dart

import 'dart:io';
import 'package:flutter/material.dart';
import '../core/file_ops.dart';
import '../core/playlist.dart' show isVideoFile;
import '../l10n/tr.dart';
import 'theme.dart';
/// X-plore 비교 g: 격자 보기 - 지금 폴더의 그림 · 동영상을 썸네일 격자로 (X-plore 의 그림 보기 방식).
/// 누르면 열기 (그림 보기 · 재생), 길게 누르면 탐색기에서 그 자리로. 돌려주는 값: "그 자리로" 고른 경로 (없으면 null)
class ExplorerGridPage extends StatefulWidget {
final String title;
final List<FileEntry> entries;
/// 그림 확장자 (환경 설정 > 보기)
final List<String> imageExts;
/// 동영상 썸네일 (없으면 아이콘)
final Future<File?> Function(FileEntry e) videoThumb;
final Future<void> Function(FileEntry e) onOpen;
const ExplorerGridPage({
super.key,
required this.title,
required this.entries,
required this.imageExts,
required this.videoThumb,
required this.onOpen,
});
/// 격자에 넣을 것 (그림 · 동영상)
static List<FileEntry> pick(List<FileEntry> all, List<String> imageExts) =>
[for (final e in all) if (!e.isDir && (imageExts.contains(e.ext) || isVideoFile(e.path))) e];
@override
State<ExplorerGridPage> createState() => _ExplorerGridPageState();
}
class _ExplorerGridPageState extends State<ExplorerGridPage> {
/// 칸 크기 (밀어서 키우기 · 줄이기 대신 단추)
double _cell = 140;
@override
Widget build(BuildContext context) {
final items = ExplorerGridPage.pick(widget.entries, widget.imageExts);
return Scaffold(
appBar: AppBar(
title: Text(trf('{0} · 그림 · 동영상 {1}개', [widget.title, items.length]), maxLines: 1, overflow: TextOverflow.ellipsis),
actions: [
IconButton(
tooltip: tr('작게'),
icon: const Icon(Icons.zoom_out),
onPressed: _cell > 80 ? () => setState(() => _cell -= 30) : null,
),
IconButton(
tooltip: tr('크게'),
icon: const Icon(Icons.zoom_in),
onPressed: _cell < 320 ? () => setState(() => _cell += 30) : null,
),
],
),
body: items.isEmpty
? Center(child: Text(tr('이 폴더에는 그림 · 동영상이 없습니다')))
: GridView.builder(
padding: const EdgeInsets.all(6),
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(maxCrossAxisExtent: _cell, mainAxisSpacing: 6, crossAxisSpacing: 6),
itemCount: items.length,
itemBuilder: (_, i) => _tile(items[i]),
),
);
}
Widget _tile(FileEntry e) {
final video = isVideoFile(e.path);
final dpr = MediaQuery.devicePixelRatioOf(context);
final px = (_cell * dpr).round();
Widget picture;
if (video) {
picture = FutureBuilder<File?>(
future: widget.videoThumb(e),
builder: (_, s) => s.data == null
? Center(child: Icon(Icons.movie_outlined, size: 40, color: JjColors.textDim))
: Image.file(s.data!, fit: BoxFit.cover, cacheWidth: px),
);
} else {
picture = Image.file(File(e.path),
fit: BoxFit.cover,
cacheWidth: px,
errorBuilder: (_, _, _) => Center(child: Icon(Icons.broken_image_outlined, size: 40, color: JjColors.textDim)));
}
return Tooltip(
message: e.name,
child: Material(
color: JjColors.panelHigh,
borderRadius: BorderRadius.circular(6),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () => widget.onOpen(e),
onLongPress: () => Navigator.pop(context, e.path),
child: Stack(fit: StackFit.expand, children: [
picture,
if (video) const Align(alignment: Alignment.center, child: Icon(Icons.play_circle_outline, color: Colors.white70, size: 32)),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
color: Colors.black54,
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
child: Text(e.name, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 11, color: Colors.white)),
),
),
]),
),
),
);
}
}