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 entries; /// 그림 확장자 (환경 설정 > 보기) final List imageExts; /// 동영상 썸네일 (없으면 아이콘) final Future Function(FileEntry e) videoThumb; final Future 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 pick(List all, List imageExts) => [for (final e in all) if (!e.isDir && (imageExts.contains(e.ext) || isVideoFile(e.path))) e]; @override State createState() => _ExplorerGridPageState(); } class _ExplorerGridPageState extends State { /// 칸 크기 (밀어서 키우기 · 줄이기 대신 단추) 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( 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)), ), ), ]), ), ), ); } }