121 lines
4.3 KiB
Dart
121 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';
|
|
import 'jj_title_bar.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: jjTitleBar(
|
|
context,
|
|
title: trf('{0} · 그림 · 동영상 {1}개', [widget.title, items.length]),
|
|
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)),
|
|
),
|
|
),
|
|
]),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|