WIP X-plore 맞추기 (관리자 지시 10/20): 보기 화면 볼륨 키로 넘기기 (아래 = 다음 장, 위 = 이전 장, Android) - 처음 끔 (환경 설정 > 보기), 켰을 때만 보기 화면에서 가로채고 (처리한 키는 시스템 볼륨을 바꾸지 않음) 동영상 · 다른 화면은 그대로 · 시험 (검토 대기)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>xplore_view
parent
99de4f4bc9
commit
ed24463192
|
|
@ -1411,6 +1411,7 @@
|
||||||
"보기": "View",
|
"보기": "View",
|
||||||
"보기 {0}초 · Ctrl+휠 확대/축소": "View {0} s · Ctrl+wheel to zoom",
|
"보기 {0}초 · Ctrl+휠 확대/축소": "View {0} s · Ctrl+wheel to zoom",
|
||||||
"보기 화면에 [품질 올리기]: AI 그림 (그림 → 그림, 약하게) 으로 다듬은 뒤 해상도 올리기를 이어서. 새 파일 (이름_q) 로 저장하며 AI 그림 모델이 필요합니다": "Shows [Improve quality] in the viewer: refines with AI image (image → image, light) and then upscales. Saves a new file (name_q); needs the AI image model",
|
"보기 화면에 [품질 올리기]: AI 그림 (그림 → 그림, 약하게) 으로 다듬은 뒤 해상도 올리기를 이어서. 새 파일 (이름_q) 로 저장하며 AI 그림 모델이 필요합니다": "Shows [Improve quality] in the viewer: refines with AI image (image → image, light) and then upscales. Saves a new file (name_q); needs the AI image model",
|
||||||
|
"보기 화면에서 볼륨 아래 = 다음 장, 위 = 이전 장 (동영상 · 다른 화면의 볼륨은 그대로)": "In the viewer, volume down = next, volume up = previous (videos and other screens keep normal volume keys)",
|
||||||
"보기 화면의 [계속 보기] 가 다음 장으로 넘기는 간격": "How often [Slideshow] in the viewer turns to the next page",
|
"보기 화면의 [계속 보기] 가 다음 장으로 넘기는 간격": "How often [Slideshow] in the viewer turns to the next page",
|
||||||
"보내지 못했습니다": "Could not send",
|
"보내지 못했습니다": "Could not send",
|
||||||
"보낼 것 {0} / 있는 것 {1}": "sending {0} / existing {1}",
|
"보낼 것 {0} / 있는 것 {1}": "sending {0} / existing {1}",
|
||||||
|
|
@ -1461,6 +1462,7 @@
|
||||||
"복사했습니다.": "copied.",
|
"복사했습니다.": "copied.",
|
||||||
"볼 그림이 없습니다.": "No images to show.",
|
"볼 그림이 없습니다.": "No images to show.",
|
||||||
"볼 수 없는 영상입니다 (삭제 · 지역 제한).": "This video is unavailable (deleted · region restricted).",
|
"볼 수 없는 영상입니다 (삭제 · 지역 제한).": "This video is unavailable (deleted · region restricted).",
|
||||||
|
"볼륨 키로 넘기기": "Turn pages with volume keys",
|
||||||
"분석": "Analyze",
|
"분석": "Analyze",
|
||||||
"분석 실패: {0} - {1}": "Analysis failed: {0} - {1}",
|
"분석 실패: {0} - {1}": "Analysis failed: {0} - {1}",
|
||||||
"분석에 실패했던 동영상 {0}개를 다시 분석했습니다": "Re-analyzed {0} videos that had failed",
|
"분석에 실패했던 동영상 {0}개를 다시 분석했습니다": "Re-analyzed {0} videos that had failed",
|
||||||
|
|
|
||||||
|
|
@ -1411,6 +1411,7 @@
|
||||||
"보기": "表示",
|
"보기": "表示",
|
||||||
"보기 {0}초 · Ctrl+휠 확대/축소": "表示 {0}秒・Ctrl+ホイールで拡大/縮小",
|
"보기 {0}초 · Ctrl+휠 확대/축소": "表示 {0}秒・Ctrl+ホイールで拡大/縮小",
|
||||||
"보기 화면에 [품질 올리기]: AI 그림 (그림 → 그림, 약하게) 으로 다듬은 뒤 해상도 올리기를 이어서. 새 파일 (이름_q) 로 저장하며 AI 그림 모델이 필요합니다": "ビューアーに [画質を上げる]: AI 画像 (画像 → 画像、弱め) で整えてから解像度を上げます。新しいファイル (名前_q) に保存し、AI 画像モデルが必要です",
|
"보기 화면에 [품질 올리기]: AI 그림 (그림 → 그림, 약하게) 으로 다듬은 뒤 해상도 올리기를 이어서. 새 파일 (이름_q) 로 저장하며 AI 그림 모델이 필요합니다": "ビューアーに [画質を上げる]: AI 画像 (画像 → 画像、弱め) で整えてから解像度を上げます。新しいファイル (名前_q) に保存し、AI 画像モデルが必要です",
|
||||||
|
"보기 화면에서 볼륨 아래 = 다음 장, 위 = 이전 장 (동영상 · 다른 화면의 볼륨은 그대로)": "ビューアーで音量下 = 次、音量上 = 前 (動画・ほかの画面の音量はそのまま)",
|
||||||
"보기 화면의 [계속 보기] 가 다음 장으로 넘기는 간격": "表示画面の [連続表示] が次のページへ進む間隔",
|
"보기 화면의 [계속 보기] 가 다음 장으로 넘기는 간격": "表示画面の [連続表示] が次のページへ進む間隔",
|
||||||
"보내지 못했습니다": "送信できませんでした",
|
"보내지 못했습니다": "送信できませんでした",
|
||||||
"보낼 것 {0} / 있는 것 {1}": "送る方 {0} / 既存 {1}",
|
"보낼 것 {0} / 있는 것 {1}": "送る方 {0} / 既存 {1}",
|
||||||
|
|
@ -1461,6 +1462,7 @@
|
||||||
"복사했습니다.": "コピーしました。",
|
"복사했습니다.": "コピーしました。",
|
||||||
"볼 그림이 없습니다.": "表示する画像がありません。",
|
"볼 그림이 없습니다.": "表示する画像がありません。",
|
||||||
"볼 수 없는 영상입니다 (삭제 · 지역 제한).": "視聴できない動画です(削除・地域制限)。",
|
"볼 수 없는 영상입니다 (삭제 · 지역 제한).": "視聴できない動画です(削除・地域制限)。",
|
||||||
|
"볼륨 키로 넘기기": "音量キーでページ送り",
|
||||||
"분석": "分析",
|
"분석": "分析",
|
||||||
"분석 실패: {0} - {1}": "分析失敗: {0} - {1}",
|
"분석 실패: {0} - {1}": "分析失敗: {0} - {1}",
|
||||||
"분석에 실패했던 동영상 {0}개를 다시 분석했습니다": "分析に失敗していた動画 {0} 件を再分析しました",
|
"분석에 실패했던 동영상 {0}개를 다시 분석했습니다": "分析に失敗していた動画 {0} 件を再分析しました",
|
||||||
|
|
|
||||||
|
|
@ -1411,6 +1411,7 @@
|
||||||
"보기": "查看",
|
"보기": "查看",
|
||||||
"보기 {0}초 · Ctrl+휠 확대/축소": "显示 {0} 秒 · Ctrl+滚轮缩放",
|
"보기 {0}초 · Ctrl+휠 확대/축소": "显示 {0} 秒 · Ctrl+滚轮缩放",
|
||||||
"보기 화면에 [품질 올리기]: AI 그림 (그림 → 그림, 약하게) 으로 다듬은 뒤 해상도 올리기를 이어서. 새 파일 (이름_q) 로 저장하며 AI 그림 모델이 필요합니다": "在查看器中显示 [提升画质]: 先用 AI 图片 (图生图,轻度) 优化再提高分辨率。保存为新文件 (名称_q),需要 AI 图片模型",
|
"보기 화면에 [품질 올리기]: AI 그림 (그림 → 그림, 약하게) 으로 다듬은 뒤 해상도 올리기를 이어서. 새 파일 (이름_q) 로 저장하며 AI 그림 모델이 필요합니다": "在查看器中显示 [提升画质]: 先用 AI 图片 (图生图,轻度) 优化再提高分辨率。保存为新文件 (名称_q),需要 AI 图片模型",
|
||||||
|
"보기 화면에서 볼륨 아래 = 다음 장, 위 = 이전 장 (동영상 · 다른 화면의 볼륨은 그대로)": "在查看器中音量减 = 下一张, 音量加 = 上一张 (视频和其他界面的音量不变)",
|
||||||
"보기 화면의 [계속 보기] 가 다음 장으로 넘기는 간격": "浏览画面中 [连续浏览] 翻到下一页的间隔",
|
"보기 화면의 [계속 보기] 가 다음 장으로 넘기는 간격": "浏览画面中 [连续浏览] 翻到下一页的间隔",
|
||||||
"보내지 못했습니다": "无法发送",
|
"보내지 못했습니다": "无法发送",
|
||||||
"보낼 것 {0} / 있는 것 {1}": "要发送 {0} / 已有 {1}",
|
"보낼 것 {0} / 있는 것 {1}": "要发送 {0} / 已有 {1}",
|
||||||
|
|
@ -1461,6 +1462,7 @@
|
||||||
"복사했습니다.": "已复制。",
|
"복사했습니다.": "已复制。",
|
||||||
"볼 그림이 없습니다.": "没有可显示的图片。",
|
"볼 그림이 없습니다.": "没有可显示的图片。",
|
||||||
"볼 수 없는 영상입니다 (삭제 · 지역 제한).": "该视频无法观看(已删除 · 地区限制)。",
|
"볼 수 없는 영상입니다 (삭제 · 지역 제한).": "该视频无法观看(已删除 · 地区限制)。",
|
||||||
|
"볼륨 키로 넘기기": "用音量键翻页",
|
||||||
"분석": "分析",
|
"분석": "分析",
|
||||||
"분석 실패: {0} - {1}": "分析失败: {0} - {1}",
|
"분석 실패: {0} - {1}": "分析失败: {0} - {1}",
|
||||||
"분석에 실패했던 동영상 {0}개를 다시 분석했습니다": "已重新分析之前分析失败的 {0} 个视频",
|
"분석에 실패했던 동영상 {0}개를 다시 분석했습니다": "已重新分析之前分析失败的 {0} 个视频",
|
||||||
|
|
|
||||||
|
|
@ -558,6 +558,10 @@ class AppSettings {
|
||||||
/// X-plore 맞추기 (관리자 지시 10/20): 그림 보기 화면 아래 갤러리 줄 (썸네일, 누르면 그 장으로). 처음 끔
|
/// X-plore 맞추기 (관리자 지시 10/20): 그림 보기 화면 아래 갤러리 줄 (썸네일, 누르면 그 장으로). 처음 끔
|
||||||
bool readerGallery = false;
|
bool readerGallery = false;
|
||||||
|
|
||||||
|
/// X-plore 맞추기 (관리자 지시 10/20): 보기 화면에서 볼륨 키로 넘기기 (아래 = 다음 장, 위 = 이전 장, Android). 처음 끔.
|
||||||
|
/// 보기 화면에서만 가로챈다 (동영상 · 다른 화면의 볼륨은 그대로)
|
||||||
|
bool readerVolumeKeys = false;
|
||||||
|
|
||||||
/// 119: 한 장에 두 쪽이 붙은 그림을 반씩 나눠 보기 - 'auto' 가로가 세로보다 긴 그림만 · 'on' 늘 · 'off' 안 함
|
/// 119: 한 장에 두 쪽이 붙은 그림을 반씩 나눠 보기 - 'auto' 가로가 세로보다 긴 그림만 · 'on' 늘 · 'off' 안 함
|
||||||
String readerSplit = 'auto';
|
String readerSplit = 'auto';
|
||||||
|
|
||||||
|
|
@ -871,6 +875,7 @@ class AppSettings {
|
||||||
'readerSystemBars': readerSystemBars,
|
'readerSystemBars': readerSystemBars,
|
||||||
'readerAutoSeconds': readerAutoSeconds,
|
'readerAutoSeconds': readerAutoSeconds,
|
||||||
'readerGallery': readerGallery,
|
'readerGallery': readerGallery,
|
||||||
|
'readerVolumeKeys': readerVolumeKeys,
|
||||||
'readerSplit': readerSplit,
|
'readerSplit': readerSplit,
|
||||||
'startMenuShortcut': startMenuShortcut,
|
'startMenuShortcut': startMenuShortcut,
|
||||||
'syncStopToast': syncStopToast,
|
'syncStopToast': syncStopToast,
|
||||||
|
|
@ -1127,6 +1132,7 @@ class AppSettings {
|
||||||
..readerSystemBars = j['readerSystemBars'] != false
|
..readerSystemBars = j['readerSystemBars'] != false
|
||||||
..readerAutoSeconds = ((j['readerAutoSeconds'] as num?)?.toInt() ?? 3).clamp(1, 60)
|
..readerAutoSeconds = ((j['readerAutoSeconds'] as num?)?.toInt() ?? 3).clamp(1, 60)
|
||||||
..readerGallery = j['readerGallery'] as bool? ?? false
|
..readerGallery = j['readerGallery'] as bool? ?? false
|
||||||
|
..readerVolumeKeys = j['readerVolumeKeys'] as bool? ?? false
|
||||||
..readerSplit = const ['auto', 'on', 'off'].contains(j['readerSplit']) ? j['readerSplit'] as String : 'auto'
|
..readerSplit = const ['auto', 'on', 'off'].contains(j['readerSplit']) ? j['readerSplit'] as String : 'auto'
|
||||||
..startMenuShortcut = j['startMenuShortcut'] != false
|
..startMenuShortcut = j['startMenuShortcut'] != false
|
||||||
..syncStopToast = j['syncStopToast'] != false
|
..syncStopToast = j['syncStopToast'] != false
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,15 @@ class ViewerSettings extends StatelessWidget {
|
||||||
title: Text(tr('그림 보기 아래 갤러리 줄')),
|
title: Text(tr('그림 보기 아래 갤러리 줄')),
|
||||||
subtitle: Text(tr('그림을 볼 때 아래에 작은 그림 줄을 보입니다 (누르면 그 장으로)')),
|
subtitle: Text(tr('그림을 볼 때 아래에 작은 그림 줄을 보입니다 (누르면 그 장으로)')),
|
||||||
),
|
),
|
||||||
|
// X-plore 맞추기 (관리자 지시 10/20): 볼륨 키로 넘기기 (Android)
|
||||||
|
if (Platform.isAndroid)
|
||||||
|
SwitchListTile(
|
||||||
|
secondary: const Icon(Icons.volume_down_outlined),
|
||||||
|
value: s.readerVolumeKeys,
|
||||||
|
onChanged: (v) => c.updateSettings((x) => x.readerVolumeKeys = v),
|
||||||
|
title: Text(tr('볼륨 키로 넘기기')),
|
||||||
|
subtitle: Text(tr('보기 화면에서 볼륨 아래 = 다음 장, 위 = 이전 장 (동영상 · 다른 화면의 볼륨은 그대로)')),
|
||||||
|
),
|
||||||
// 117: 계속 보기 간격
|
// 117: 계속 보기 간격
|
||||||
SettingTile(
|
SettingTile(
|
||||||
leading: const Icon(Icons.slideshow_outlined),
|
leading: const Icon(Icons.slideshow_outlined),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:io' show Platform;
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
@ -50,6 +51,9 @@ class ReaderPage extends StatefulWidget {
|
||||||
final bool autoPlay;
|
final bool autoPlay;
|
||||||
const ReaderPage({super.key, required this.c, required this.source, this.start = 0, this.autoPlay = false});
|
const ReaderPage({super.key, required this.c, required this.source, this.start = 0, this.autoPlay = false});
|
||||||
|
|
||||||
|
/// 볼륨 키로 넘기기를 쓰는 기기인지 (Android - 시험에서 바꿈)
|
||||||
|
static bool Function() volumeKeysSupported = () => Platform.isAndroid;
|
||||||
|
|
||||||
/// 계속 보기 동안 화면 켜 두기 (시험에서 바꿈)
|
/// 계속 보기 동안 화면 켜 두기 (시험에서 바꿈)
|
||||||
static Future<void> Function(bool on) keepScreenOn = (on) => on ? WakelockPlus.enable() : WakelockPlus.disable();
|
static Future<void> Function(bool on) keepScreenOn = (on) => on ? WakelockPlus.enable() : WakelockPlus.disable();
|
||||||
|
|
||||||
|
|
@ -412,6 +416,15 @@ class _ReaderPageState extends State<ReaderPage> {
|
||||||
_jumpLater();
|
_jumpLater();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// X-plore 맞추기 (관리자 지시 10/20): 볼륨 키로 넘기기 (설정을 켰고 Android 일 때만 - 처리한 키는 시스템 볼륨을 바꾸지 않는다)
|
||||||
|
KeyEventResult _onKey(FocusNode _, KeyEvent e) {
|
||||||
|
if (!c.settings.readerVolumeKeys || !ReaderPage.volumeKeysSupported()) return KeyEventResult.ignored;
|
||||||
|
final k = e.logicalKey;
|
||||||
|
if (k != LogicalKeyboardKey.audioVolumeDown && k != LogicalKeyboardKey.audioVolumeUp) return KeyEventResult.ignored;
|
||||||
|
if (e is KeyDownEvent || e is KeyRepeatEvent) _go(k == LogicalKeyboardKey.audioVolumeDown ? 1 : -1);
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
|
||||||
void _go(int delta) {
|
void _go(int delta) {
|
||||||
final to = (_index + delta).clamp(0, math.max(0, _v.length - 1)).toInt();
|
final to = (_index + delta).clamp(0, math.max(0, _v.length - 1)).toInt();
|
||||||
if (to == _index) {
|
if (to == _index) {
|
||||||
|
|
@ -933,7 +946,10 @@ class _ReaderPageState extends State<ReaderPage> {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Scaffold(
|
child: Focus(
|
||||||
|
autofocus: true,
|
||||||
|
onKeyEvent: _onKey,
|
||||||
|
child: Scaffold(
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
body: LayoutBuilder(builder: (context, box) {
|
body: LayoutBuilder(builder: (context, box) {
|
||||||
// 화면 너비 (기기 픽셀) 에 맞춰 그림을 줄여 읽는다
|
// 화면 너비 (기기 픽셀) 에 맞춰 그림을 줄여 읽는다
|
||||||
|
|
@ -977,6 +993,7 @@ class _ReaderPageState extends State<ReaderPage> {
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
}),
|
}),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:archive/archive_io.dart';
|
import 'package:archive/archive_io.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:image/image.dart' as img;
|
import 'package:image/image.dart' as img;
|
||||||
import 'package:jj_mkvmaker/app/app_controller.dart';
|
import 'package:jj_mkvmaker/app/app_controller.dart';
|
||||||
|
|
@ -215,6 +215,41 @@ void main() {
|
||||||
await tester.pumpWidget(const SizedBox());
|
await tester.pumpWidget(const SizedBox());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('X-plore 맞추기: 볼륨 키로 넘기기 - 처음은 끔 (키를 가로채지 않음), 켜면 아래 = 다음 · 위 = 이전 (Android 만)', (tester) async {
|
||||||
|
tester.view.physicalSize = const Size(900, 1200);
|
||||||
|
tester.view.devicePixelRatio = 1;
|
||||||
|
addTearDown(tester.view.reset);
|
||||||
|
final was = ReaderPage.volumeKeysSupported;
|
||||||
|
addTearDown(() => ReaderPage.volumeKeysSupported = was);
|
||||||
|
ReaderPage.volumeKeysSupported = () => true;
|
||||||
|
expect(AppSettings().readerVolumeKeys, isFalse);
|
||||||
|
final files = [
|
||||||
|
for (final (i, n) in ['a.png', 'b.png', 'c.png'].indexed) (File(p.join(tmp.path, n))..writeAsBytesSync(_png(i * 80, 0, 0))).path,
|
||||||
|
];
|
||||||
|
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||||
|
await tester.pumpWidget(MaterialApp(home: ReaderPage(c: c, source: ImageFilesSource(files, tempDir: tmp.path, title: 'imgs'))));
|
||||||
|
await tester.pump();
|
||||||
|
// 끔: 볼륨 키를 가로채지 않는다 (시스템 볼륨이 바뀜)
|
||||||
|
expect(await tester.sendKeyEvent(LogicalKeyboardKey.audioVolumeDown), isFalse);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('1 / 3'), findsOneWidget);
|
||||||
|
// 켬
|
||||||
|
c.settings.readerVolumeKeys = true;
|
||||||
|
expect(await tester.sendKeyEvent(LogicalKeyboardKey.audioVolumeDown), isTrue, reason: '처리함 - 시스템 볼륨은 바뀌지 않음');
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('2 / 3'), findsOneWidget);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.audioVolumeDown);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('3 / 3'), findsOneWidget);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.audioVolumeUp);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('2 / 3'), findsOneWidget);
|
||||||
|
// Android 가 아니면 쓰지 않음
|
||||||
|
ReaderPage.volumeKeysSupported = () => false;
|
||||||
|
expect(await tester.sendKeyEvent(LogicalKeyboardKey.audioVolumeDown), isFalse);
|
||||||
|
await tester.pumpWidget(const SizedBox());
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('그림이 깨졌으면 화면 글 + [자세히], 파일을 고친 뒤 [다시 시도] 하면 열린다 (28 지적: 같은 키 · 같은 그림이라 Image 가 다시 읽지 않았다 - 장의 키를 올리고 그림 캐시도 비움)', (tester) async {
|
testWidgets('그림이 깨졌으면 화면 글 + [자세히], 파일을 고친 뒤 [다시 시도] 하면 열린다 (28 지적: 같은 키 · 같은 그림이라 Image 가 다시 읽지 않았다 - 장의 키를 올리고 그림 캐시도 비움)', (tester) async {
|
||||||
tester.view.physicalSize = const Size(900, 1200);
|
tester.view.physicalSize = const Size(900, 1200);
|
||||||
tester.view.devicePixelRatio = 1;
|
tester.view.devicePixelRatio = 1;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue