WIP X-plore 맞추기 (관리자 지시 10/20): 폴더 길게 누르기 [사진 슬라이드쇼] - 그 폴더의 그림 (지금 정렬 · 숨긴 것 빼고) 을 보기 화면으로 열고 [계속 보기] (117) 를 켠 채 시작, 그림이 없으면 알림 · 시험 (검토 대기)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xplore_view
RyuJongJin 2026-10-20 21:23:40 +09:00
parent 6b4743c9e9
commit dc77998e63
6 changed files with 69 additions and 3 deletions

View File

@ -1521,6 +1521,7 @@
"사용자가 취소했습니다.": "Cancelled by the user.",
"사용할 수 있는 자막 사이트가 없습니다.": "No subtitle site is available.",
"사진": "Photos",
"사진 슬라이드쇼": "Photo slideshow",
"사진에는 '사진' 모델이 더 낫지만, 이 기기 (그래픽카드 없음) 에서는 한 장에 약 20분 걸립니다.": "The 'Photos' model is better for photos, but on this device (no graphics card) it takes about 20 minutes per image.",
"사진용": "Photos",
"삭제": "Delete",
@ -2103,6 +2104,7 @@
"이 폴더에 현재 페이지 추가": "Add the current page to this folder",
"이 폴더에는 고를 파일이 없습니다": "There are no files to choose in this folder",
"이 폴더에는 그림 · 동영상이 없습니다": "No pictures or videos in this folder",
"이 폴더에는 그림이 없습니다": "There are no images in this folder",
"이 폴더에만: {0}": "This folder only: {0}",
"이 폴더의 동영상 재생": "Play videos in this folder",
"이 폴더의 파일": "Files in this folder",
@ -2734,6 +2736,7 @@
"폴더는 보여도 그 안의 동영상 · 파일은 이 권한이 있어야 보입니다. 허용 화면에서 JJ_MKVMaker 를 켜고 돌아오세요.": "Folders are shown, but the videos and files inside need this permission. Turn on JJ_MKVMaker on the permission screen and come back.",
"폴더를 만들 수 없습니다: {0}": "Cannot create the folder: {0}",
"폴더를 읽는 중…": "Reading folders…",
"폴더를 읽을 수 없습니다: {0}": "Can't read the folder: {0}",
"폴더를 자기 안으로 복사 · 이동할 수 없습니다": "Cannot copy or move a folder into itself",
"폴더를 자기 안으로 복사할 수 없습니다": "Cannot copy a folder into itself",
"폴더를 자기 안으로 옮길 수 없습니다": "Cannot move a folder into itself",

View File

@ -1521,6 +1521,7 @@
"사용자가 취소했습니다.": "ユーザーがキャンセルしました。",
"사용할 수 있는 자막 사이트가 없습니다.": "使用できる字幕サイトがありません。",
"사진": "写真",
"사진 슬라이드쇼": "写真スライドショー",
"사진에는 '사진' 모델이 더 낫지만, 이 기기 (그래픽카드 없음) 에서는 한 장에 약 20분 걸립니다.": "写真には「写真」モデルが適していますが、この端末 (グラフィックカードなし) では 1 枚に約 20 分かかります。",
"사진용": "写真用",
"삭제": "削除",
@ -2103,6 +2104,7 @@
"이 폴더에 현재 페이지 추가": "このフォルダーに現在のページを追加",
"이 폴더에는 고를 파일이 없습니다": "このフォルダーには選べるファイルがありません",
"이 폴더에는 그림 · 동영상이 없습니다": "このフォルダーには画像・動画がありません",
"이 폴더에는 그림이 없습니다": "このフォルダーには画像がありません",
"이 폴더에만: {0}": "このフォルダーだけ: {0}",
"이 폴더의 동영상 재생": "このフォルダーの動画を再生",
"이 폴더의 파일": "このフォルダーのファイル",
@ -2734,6 +2736,7 @@
"폴더는 보여도 그 안의 동영상 · 파일은 이 권한이 있어야 보입니다. 허용 화면에서 JJ_MKVMaker 를 켜고 돌아오세요.": "フォルダーは見えても、中の動画・ファイルはこの権限が必要です。許可画面で JJ_MKVMaker をオンにして戻ってください。",
"폴더를 만들 수 없습니다: {0}": "フォルダーを作成できません: {0}",
"폴더를 읽는 중…": "フォルダーを読み込み中…",
"폴더를 읽을 수 없습니다: {0}": "フォルダーを読み込めません: {0}",
"폴더를 자기 안으로 복사 · 이동할 수 없습니다": "フォルダーを自分自身の中にコピー · 移動できません",
"폴더를 자기 안으로 복사할 수 없습니다": "フォルダーを自分自身の中にコピーできません",
"폴더를 자기 안으로 옮길 수 없습니다": "フォルダーを自分自身の中に移動できません",

View File

@ -1521,6 +1521,7 @@
"사용자가 취소했습니다.": "用户已取消。",
"사용할 수 있는 자막 사이트가 없습니다.": "没有可用的字幕网站。",
"사진": "照片",
"사진 슬라이드쇼": "照片幻灯片",
"사진에는 '사진' 모델이 더 낫지만, 이 기기 (그래픽카드 없음) 에서는 한 장에 약 20분 걸립니다.": "照片更适合使用“照片”模型,但在本设备 (无显卡) 上每张约需 20 分钟。",
"사진용": "照片",
"삭제": "删除",
@ -2103,6 +2104,7 @@
"이 폴더에 현재 페이지 추가": "将当前页面添加到此文件夹",
"이 폴더에는 고를 파일이 없습니다": "此文件夹中没有可选择的文件",
"이 폴더에는 그림 · 동영상이 없습니다": "此文件夹中没有图片或视频",
"이 폴더에는 그림이 없습니다": "此文件夹中没有图片",
"이 폴더에만: {0}": "仅此文件夹: {0}",
"이 폴더의 동영상 재생": "播放此文件夹中的视频",
"이 폴더의 파일": "此文件夹中的文件",
@ -2734,6 +2736,7 @@
"폴더는 보여도 그 안의 동영상 · 파일은 이 권한이 있어야 보입니다. 허용 화면에서 JJ_MKVMaker 를 켜고 돌아오세요.": "虽然能看到文件夹,但其中的视频和文件需要此权限。请在权限界面开启 JJ_MKVMaker 后返回。",
"폴더를 만들 수 없습니다: {0}": "无法创建文件夹: {0}",
"폴더를 읽는 중…": "正在读取文件夹…",
"폴더를 읽을 수 없습니다: {0}": "无法读取文件夹: {0}",
"폴더를 자기 안으로 복사 · 이동할 수 없습니다": "不能将文件夹复制或移动到其自身内部",
"폴더를 자기 안으로 복사할 수 없습니다": "不能将文件夹复制到其自身内部",
"폴더를 자기 안으로 옮길 수 없습니다": "不能将文件夹移动到其自身内部",

View File

@ -1765,6 +1765,28 @@ class _ExplorerPageState extends State<ExplorerPage> {
await openReader(context, c, ImageFilesSource(images, tempDir: temp, title: vBasename(dir)), start: start);
}
/// X-plore 맞추기 (관리자 지시 10/20): 폴더의 그림 (지금 정렬 순서) 을 보기 화면으로 열고 [계속 보기] 를 켠 채 시작
Future<void> _slideshow(_Pane pane, String dir) async {
List<FileEntry> list;
try {
list = pane.cache[dir] ?? _sorted(await listEntries(dir, showHidden: c.settings.explorerShowHidden), dir);
} catch (e) {
_snack(trf('폴더를 읽을 수 없습니다: {0}', [e]));
return;
}
final images = [
for (final e in list)
if (!e.isDir && c.settings.imageExts.contains(extOf(e.path)) && (_showAppHidden || !_appHidden(e.path))) e.path,
];
if (images.isEmpty) {
_snack(tr('이 폴더에는 그림이 없습니다'));
return;
}
final temp = await c.services.storage.tempDirectory();
if (!mounted) return;
await openReader(context, c, ImageFilesSource(images, tempDir: temp, title: vBasename(dir)), autoPlay: true);
}
/// 문서 → PDF (진행 창) → 보기
Future<void> _viewDoc(String path) async {
final temp = await c.services.storage.tempDirectory();
@ -1999,6 +2021,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
if (!e.isDir && !dav) ('hash', Icons.tag, tr('해시 (MD5 · SHA-256)')),
if (e.isDir && dual) ('openOther', Icons.vertical_split_outlined, tr('다른 창에서 열기')),
if (e.isDir) ('playFolder', Icons.play_circle_outline, tr('이 폴더의 동영상 재생')),
// X-plore 맞추기 (관리자 지시 10/20): 이 폴더의 그림을 [계속 보기] 로
if (e.isDir && viewer) ('slideshow', Icons.slideshow_outlined, tr('사진 슬라이드쇼')),
// X-plore 비교 g: 그림 · 동영상을 썸네일 격자로
if (e.isDir && !dav) ('grid', Icons.grid_view, tr('격자로 보기 (그림 · 동영상)')),
if (video || audio) ('playInternal', Icons.play_circle_outline, tr('내장 플레이어로 재생')),
@ -2075,6 +2099,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
setState(() => _active = _panes.indexOf(other));
case 'playFolder':
await _playFolder(e.path);
case 'slideshow':
await _slideshow(pane, e.path);
case 'playInternal':
await _play([e.path], internal: true);
case 'grid':

View File

@ -15,9 +15,9 @@ import 'ai_upscale_ui.dart';
import 'theme.dart';
/// 그림 · 만화 · PDF 보기를 연다. [start] 처음 볼 장. 닫으면 [source] 를 정리한다.
Future<void> openReader(BuildContext context, AppController c, ReaderSource source, {int start = 0}) async {
Future<void> openReader(BuildContext context, AppController c, ReaderSource source, {int start = 0, bool autoPlay = false}) async {
await Navigator.of(context).push(MaterialPageRoute<void>(
builder: (_) => ReaderPage(c: c, source: source, start: start),
builder: (_) => ReaderPage(c: c, source: source, start: start, autoPlay: autoPlay),
));
await source.dispose();
}
@ -45,7 +45,10 @@ class ReaderPage extends StatefulWidget {
final AppController c;
final ReaderSource source;
final int start;
const ReaderPage({super.key, required this.c, required this.source, this.start = 0});
/// X-plore 맞추기 (관리자 지시 10/20): 폴더 [사진 슬라이드쇼] - [계속 보기] (117) 를 켠 채 연다
final bool autoPlay;
const ReaderPage({super.key, required this.c, required this.source, this.start = 0, this.autoPlay = false});
/// 계속 보기 동안 화면 켜 두기 (시험에서 바꿈)
static Future<void> Function(bool on) keepScreenOn = (on) => on ? WakelockPlus.enable() : WakelockPlus.disable();
@ -81,6 +84,7 @@ class _ReaderPageState extends State<ReaderPage> {
_applySystemBars();
// 224: 앱으로 돌아올 때 그사이 밖에서 지운 그림을 목록에서 뺀다
_life = AppLifecycleListener(onResume: () => unawaited(dropMissingFiles()));
if (widget.autoPlay) WidgetsBinding.instance.addPostFrameCallback((_) => mounted ? _play(true) : null);
}
late final AppLifecycleListener _life;

View File

@ -24,6 +24,8 @@ import 'package:jj_mkvmaker/ui/explorer_grid.dart';
import 'package:jj_mkvmaker/ui/text_viewer.dart';
import 'package:jj_mkvmaker/ui/storage_page.dart';
import 'package:jj_mkvmaker/ui/path_label.dart';
import 'package:image/image.dart' as img;
import 'package:jj_mkvmaker/ui/reader_page.dart' show ReaderPage;
import 'package:path/path.dart' as p;
import 'support/fake_recycle_bin.dart';
import 'package:jj_mkvmaker/platform/windows/recycle_bin.dart' show recycleBinForTest;
@ -167,6 +169,31 @@ void main() {
await tester.pump(const Duration(seconds: 2));
});
testWidgets('X-plore 맞추기 (관리자 지시 10/20): 폴더 [사진 슬라이드쇼] - 그 폴더 그림을 [계속 보기] 로 열기 · 그림이 없으면 알림', (tester) async {
for (final n in ['b.png', 'a.png']) {
File(p.join(left, 'sub', n)).writeAsBytesSync(img.encodePng(img.Image(width: 20, height: 30)));
}
Directory(p.join(left, 'empty')).createSync();
await open(tester);
await tester.longPress(find.text('sub'));
await calm(tester);
await act(tester, () => tester.tap(find.text('사진 슬라이드쇼')));
await settle(tester, () => find.byType(ReaderPage).evaluate().isNotEmpty);
expect(find.text('1 / 2'), findsOneWidget);
expect(find.text('a.png'), findsOneWidget, reason: '지금 정렬 순서 (이름) 의 첫 그림부터');
expect(find.byTooltip('계속 보기 멈추기'), findsOneWidget, reason: '[계속 보기] 를 켠 채 시작');
await act(tester, () async => tester.state<NavigatorState>(find.byType(Navigator).first).pop());
await calm(tester);
// 그림이 없는 폴더
await tester.longPress(find.text('empty'));
await calm(tester);
await act(tester, () => tester.tap(find.text('사진 슬라이드쇼')));
await settle(tester, () => find.text('이 폴더에는 그림이 없습니다').evaluate().isNotEmpty);
expect(find.byType(ReaderPage), findsNothing);
ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars();
await calm(tester);
});
testWidgets('X-plore P3: 길게 누르기 [해시] (MD5 · SHA-256) · 폴더 [폴더 속성] · 빠른 보기 [16진]', (tester) async {
await open(tester);
// 해시: doc.txt = "hello"