설정 A 분야 6: 보기 화면 (ReaderSettings) - 키 9 · zipComicV2 표시
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>modular
parent
506803ca65
commit
6054daf17a
|
|
@ -30,7 +30,7 @@ void main() {
|
|||
(File(p.join(tmp.path, 'p$i.png'))..writeAsBytesSync(img.encodePng(img.Image(width: 300, height: 200)))).path,
|
||||
];
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings.readerGallery = gallery;
|
||||
c.settings.reader.gallery = gallery;
|
||||
Future<void> wait(int ms) async {
|
||||
for (var i = 0; i < ms ~/ 100; i++) {
|
||||
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 100)));
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import '../services/app_shell.dart' show appIconOf;
|
|||
import '../core/remote_idle.dart';
|
||||
import 'settings/records.dart';
|
||||
import 'settings/settings_group.dart';
|
||||
import 'settings/reader_settings.dart';
|
||||
import 'settings/ai_image_settings.dart';
|
||||
import 'settings/copy_settings.dart';
|
||||
import 'settings/tor_settings.dart';
|
||||
|
|
@ -24,6 +25,7 @@ import 'settings/explorer_settings.dart';
|
|||
export 'settings/records.dart';
|
||||
export 'settings/settings_group.dart';
|
||||
export 'settings/settings_store.dart';
|
||||
export 'settings/reader_settings.dart';
|
||||
export 'settings/ai_image_settings.dart';
|
||||
export 'settings/copy_settings.dart';
|
||||
export 'settings/tor_settings.dart';
|
||||
|
|
@ -254,30 +256,13 @@ class AppSettings {
|
|||
/// 그림 보기 (만화 보기) 대상 확장자 (파일 탐색기에서 두 번 누르면 보기로 연다)
|
||||
List<String> imageExts = [...defaultImageExtensions];
|
||||
|
||||
/// 보기: 'page' 한 쪽 맞추기 · 'width' 좌우 맞추기 (세로로 밀어 봄)
|
||||
String readerFit = 'page';
|
||||
|
||||
/// 보기: 오른쪽에서 왼쪽으로 넘김 (일본 만화)
|
||||
bool readerRtl = false;
|
||||
|
||||
/// 보기: 밝기 (-0.7 어둡게 ~ 0.7 밝게)
|
||||
double readerBrightness = 0;
|
||||
|
||||
/// 30 · 101: 보기 화면에서 위 · 아래 시스템 막대 (시계 · 알림 · 뒤로) 를 그대로 보이기 (기본: 보임 - 기기의 평소 동작, 화면 가득은 버튼 · 설정으로)
|
||||
bool readerSystemBars = true;
|
||||
|
||||
/// 117: 보기 화면 "계속 보기" 간격 (초, 1 ~ 60)
|
||||
int readerAutoSeconds = 3;
|
||||
|
||||
/// X-plore 맞추기 (관리자 지시 10/20): 그림 보기 화면 아래 갤러리 줄 (썸네일, 누르면 그 장으로). 처음 끔
|
||||
bool readerGallery = false;
|
||||
|
||||
/// X-plore 맞추기 (관리자 지시 10/20): 보기 화면에서 볼륨 키로 넘기기 (아래 = 다음 장, 위 = 이전 장, Android). 처음 끔.
|
||||
/// 보기 화면에서만 가로챈다 (동영상 · 다른 화면의 볼륨은 그대로)
|
||||
bool readerVolumeKeys = false;
|
||||
|
||||
/// 119: 한 장에 두 쪽이 붙은 그림을 반씩 나눠 보기 - 'auto' 가로가 세로보다 긴 그림만 · 'on' 늘 · 'off' 안 함
|
||||
String readerSplit = 'auto';
|
||||
|
||||
/// 99 (Windows): 시작 메뉴에 등록 - 알림이 JJ_MKVMaker 로 보이고 눌러서 열린다
|
||||
bool startMenuShortcut = true;
|
||||
|
|
@ -322,8 +307,6 @@ class AppSettings {
|
|||
/// 내장 플레이어 자막 글자 크기 배율 (0.5 ~ 2.5, 기본 1)
|
||||
double subtitleScale = 1.0;
|
||||
|
||||
/// ZIP · CBZ 를 두 번 누르면 안의 그림을 만화처럼 보기 (끄면 목록). 기본은 목록 (사용자 지시)
|
||||
bool zipComic = false;
|
||||
|
||||
/// WebDAV 서버 (파일 탐색기 · Rsync 화면 위쪽 "SD 카드" 옆 탭). 비밀번호는 이 설정 파일에만.
|
||||
List<DavServer> webdavServers = [];
|
||||
|
|
@ -434,8 +417,11 @@ class AppSettings {
|
|||
/// AI 그림 · 해상도 올리기 (123 · 121)
|
||||
final aiImage = AiImageSettings();
|
||||
|
||||
/// 보기 화면 (그림 · 만화 · PDF · 문서)
|
||||
final reader = ReaderSettings();
|
||||
|
||||
/// 설정 분야들 (설정 A) - 저마다 자기 키를 읽고 쓴다. toJson · fromJson 은 이 목록을 돌며 합치기만 한다
|
||||
List<SettingsGroup> get groups => [explorer, network, tor, copy, aiImage];
|
||||
List<SettingsGroup> get groups => [explorer, network, tor, copy, aiImage, reader];
|
||||
|
||||
/// [plainSecrets]: 안전 저장소를 쓸 수 없을 때만 (예전 파일에 있던 비밀 값을 잃지 않게) 예전처럼 파일에 둔다
|
||||
Map<String, Object?> toJson({bool plainSecrets = false}) {
|
||||
|
|
@ -513,14 +499,6 @@ class AppSettings {
|
|||
'navOrder': navOrder,
|
||||
'swipeNav': swipeNav,
|
||||
'imageExts': imageExts,
|
||||
'readerFit': readerFit,
|
||||
'readerRtl': readerRtl,
|
||||
'readerBrightness': readerBrightness,
|
||||
'readerSystemBars': readerSystemBars,
|
||||
'readerAutoSeconds': readerAutoSeconds,
|
||||
'readerGallery': readerGallery,
|
||||
'readerVolumeKeys': readerVolumeKeys,
|
||||
'readerSplit': readerSplit,
|
||||
'startMenuShortcut': startMenuShortcut,
|
||||
'swipeWrap': swipeWrap,
|
||||
'browserSwipeHinted': browserSwipeHinted,
|
||||
|
|
@ -535,9 +513,6 @@ class AppSettings {
|
|||
// 116: 예전 판의 표시 ('allFilesHintShown') 는 권한이 없어진 것을 몰라 다시 띄우지 못했다 → 새 이름으로 한 번 더
|
||||
'allFilesHint2': allFilesHintShown,
|
||||
'subtitleScale': subtitleScale,
|
||||
'zipComic': zipComic,
|
||||
// 예전 기본값 (만화 보기 켜짐) 은 지시와 반대였으므로 한 번 꺼진 상태 (목록) 로
|
||||
'zipComicV2': true,
|
||||
'webdavServers': [
|
||||
for (final x in webdavServers) {...x.toJson(), if (plainSecrets) 'password': x.password},
|
||||
],
|
||||
|
|
@ -668,14 +643,6 @@ class AppSettings {
|
|||
..imageExts = j['imageExts'] is List
|
||||
? [for (final x in j['imageExts'] as List) '$x'.toLowerCase()]
|
||||
: [...defaultImageExtensions]
|
||||
..readerFit = j['readerFit'] == 'width' ? 'width' : 'page'
|
||||
..readerRtl = j['readerRtl'] as bool? ?? false
|
||||
..readerBrightness = ((j['readerBrightness'] as num?)?.toDouble() ?? 0).clamp(-0.7, 0.7)
|
||||
..readerSystemBars = j['readerSystemBars'] != false
|
||||
..readerAutoSeconds = ((j['readerAutoSeconds'] as num?)?.toInt() ?? 3).clamp(1, 60)
|
||||
..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'
|
||||
..startMenuShortcut = j['startMenuShortcut'] != false
|
||||
..swipeWrap = j['swipeWrap'] != false
|
||||
..browserSwipeHinted = j['browserSwipeHinted'] == true
|
||||
|
|
@ -691,7 +658,6 @@ class AppSettings {
|
|||
..davExternalOpen = const ['fetch', 'url'].contains(j['davExternalOpen']) ? j['davExternalOpen'] as String : 'ask'
|
||||
..allFilesHintShown = j['allFilesHint2'] == true
|
||||
..subtitleScale = ((j['subtitleScale'] as num?)?.toDouble() ?? 1.0).clamp(0.5, 2.5)
|
||||
..zipComic = j['zipComic'] == true && j['zipComicV2'] == true
|
||||
..webdavServers = [
|
||||
for (final x in (j['webdavServers'] as List?) ?? const [])
|
||||
if (x is Map) DavServer.fromJson(x),
|
||||
|
|
@ -803,7 +769,7 @@ class AppSettings {
|
|||
// 53 · 28: 앱 안 브라우저 쿠키를 쓰던 사람에게 넘기는 사이트가 넓어졌음을 한 번 알린다
|
||||
if (j['cookieScopeV2'] != true && j.isNotEmpty && (j['ytCookiesBrowser'] ?? internalBrowserCookies) == internalBrowserCookies)
|
||||
'cookieScope',
|
||||
if (j['zipComicV2'] != true && j['zipComic'] != false && j.containsKey('imageExts') && !s.zipComic) 'zipComic',
|
||||
if (j['zipComicV2'] != true && j['zipComic'] != false && j.containsKey('imageExts') && !s.reader.zipComic) 'zipComic',
|
||||
if (j['backgroundV2'] != true && j['runInBackground'] == false && s.runInBackground && Platform.isAndroid)
|
||||
'background',
|
||||
// 193: 그 설정이 생기기 전 판 (설정 파일은 있는데 그 항목이 없음 - 예: 10.03) 에서 올라온 사람이 바로 겪는 동작 변화.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
|
||||
import 'settings_group.dart';
|
||||
|
||||
/// 약속 (설정 A): 환경 설정 > 보기 화면 (그림 · 만화 · PDF · 문서) - 자기 키 (이름 · 처음 값 · 읽기 · 쓰기) 를 스스로 갖는다.
|
||||
/// 설정 하나를 더할 때는 이 파일만 고친다. 저장 파일의 키 이름은 예전 그대로 (평평한 키)
|
||||
class ReaderSettings implements SettingsGroup {
|
||||
/// 보기: 'page' 한 쪽 맞추기 · 'width' 좌우 맞추기 (세로로 밀어 봄)
|
||||
String fit = 'page';
|
||||
|
||||
/// 보기: 오른쪽에서 왼쪽으로 넘김 (일본 만화)
|
||||
bool rtl = false;
|
||||
|
||||
/// 보기: 밝기 (-0.7 어둡게 ~ 0.7 밝게)
|
||||
double brightness = 0;
|
||||
|
||||
/// 30 · 101: 보기 화면에서 위 · 아래 시스템 막대 (시계 · 알림 · 뒤로) 를 그대로 보이기 (기본: 보임 - 기기의 평소 동작, 화면 가득은 버튼 · 설정으로)
|
||||
bool systemBars = true;
|
||||
|
||||
/// 117: 보기 화면 "계속 보기" 간격 (초, 1 ~ 60)
|
||||
int autoSeconds = 3;
|
||||
|
||||
/// X-plore 맞추기 (관리자 지시 10/20): 그림 보기 화면 아래 갤러리 줄 (썸네일, 누르면 그 장으로). 처음 끔
|
||||
bool gallery = false;
|
||||
|
||||
/// X-plore 맞추기 (관리자 지시 10/20): 보기 화면에서 볼륨 키로 넘기기 (아래 = 다음 장, 위 = 이전 장, Android). 처음 끔.
|
||||
/// 보기 화면에서만 가로챈다 (동영상 · 다른 화면의 볼륨은 그대로)
|
||||
bool volumeKeys = false;
|
||||
|
||||
/// 119: 한 장에 두 쪽이 붙은 그림을 반씩 나눠 보기 - 'auto' 가로가 세로보다 긴 그림만 · 'on' 늘 · 'off' 안 함
|
||||
String split = 'auto';
|
||||
|
||||
/// ZIP · CBZ 를 두 번 누르면 안의 그림을 만화처럼 보기 (끄면 목록). 기본은 목록 (사용자 지시)
|
||||
bool zipComic = false;
|
||||
|
||||
@override
|
||||
void write(Map<String, Object?> j, {required bool plainSecrets}) {
|
||||
j['readerFit'] = fit;
|
||||
j['readerRtl'] = rtl;
|
||||
j['readerBrightness'] = brightness;
|
||||
j['readerSystemBars'] = systemBars;
|
||||
j['readerAutoSeconds'] = autoSeconds;
|
||||
j['readerGallery'] = gallery;
|
||||
j['readerVolumeKeys'] = volumeKeys;
|
||||
j['readerSplit'] = split;
|
||||
j['zipComic'] = zipComic;
|
||||
// 예전 기본값 (만화 보기 켜짐) 은 지시와 반대였으므로 한 번 꺼진 상태 (목록) 로
|
||||
j['zipComicV2'] = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void read(Map<String, dynamic> j) {
|
||||
fit = j['readerFit'] == 'width' ? 'width' : 'page';
|
||||
rtl = j['readerRtl'] as bool? ?? false;
|
||||
brightness = ((j['readerBrightness'] as num?)?.toDouble() ?? 0).clamp(-0.7, 0.7);
|
||||
systemBars = j['readerSystemBars'] != false;
|
||||
autoSeconds = ((j['readerAutoSeconds'] as num?)?.toInt() ?? 3).clamp(1, 60);
|
||||
gallery = j['readerGallery'] as bool? ?? false;
|
||||
volumeKeys = j['readerVolumeKeys'] as bool? ?? false;
|
||||
split = const ['auto', 'on', 'off'].contains(j['readerSplit']) ? j['readerSplit'] as String : 'auto';
|
||||
zipComic = j['zipComic'] == true && j['zipComicV2'] == true;
|
||||
}
|
||||
}
|
||||
|
|
@ -44,8 +44,8 @@ class ViewerSettings extends StatelessWidget {
|
|||
),
|
||||
SwitchListTile(
|
||||
secondary: const Icon(Icons.auto_stories_outlined),
|
||||
value: s.zipComic,
|
||||
onChanged: (v) => c.updateSettings((x) => x.zipComic = v),
|
||||
value: s.reader.zipComic,
|
||||
onChanged: (v) => c.updateSettings((x) => x.reader.zipComic = v),
|
||||
title: Text(tr('ZIP · CBZ 를 만화로 보기')),
|
||||
subtitle: Text(tr('켜면 두 번 눌렀을 때 안의 그림을 바로 넘겨 봅니다. 끄면 목록 (골라서 풀기) 을 엽니다.')),
|
||||
),
|
||||
|
|
@ -55,20 +55,20 @@ class ViewerSettings extends StatelessWidget {
|
|||
title: Text(tr('두 쪽 나눠 보기')),
|
||||
subtitle: Text(tr('한 장에 두 쪽이 붙은 그림을 반씩 넘겨 봅니다 (읽는 방향의 쪽부터)')),
|
||||
trailing: DropdownButton<String>(
|
||||
value: s.readerSplit,
|
||||
value: s.reader.split,
|
||||
items: [
|
||||
DropdownMenuItem(value: 'auto', child: Text(tr('자동 (가로로 긴 그림만)'))),
|
||||
DropdownMenuItem(value: 'on', child: Text(tr('켜기'))),
|
||||
DropdownMenuItem(value: 'off', child: Text(tr('끄기'))),
|
||||
],
|
||||
onChanged: (v) => c.updateSettings((x) => x.readerSplit = v!),
|
||||
onChanged: (v) => c.updateSettings((x) => x.reader.split = v!),
|
||||
),
|
||||
),
|
||||
// X-plore 맞추기 (관리자 지시 10/20): 아래 갤러리 줄 (보기 화면 위 막대에서도 켜고 끔)
|
||||
SwitchListTile(
|
||||
secondary: const Icon(Icons.view_carousel_outlined),
|
||||
value: s.readerGallery,
|
||||
onChanged: (v) => c.updateSettings((x) => x.readerGallery = v),
|
||||
value: s.reader.gallery,
|
||||
onChanged: (v) => c.updateSettings((x) => x.reader.gallery = v),
|
||||
title: Text(tr('그림 보기 아래 갤러리 줄')),
|
||||
subtitle: Text(tr('그림을 볼 때 아래에 작은 그림 줄을 보입니다 (누르면 그 장으로)')),
|
||||
),
|
||||
|
|
@ -76,8 +76,8 @@ class ViewerSettings extends StatelessWidget {
|
|||
if (Platform.isAndroid)
|
||||
SwitchListTile(
|
||||
secondary: const Icon(Icons.volume_down_outlined),
|
||||
value: s.readerVolumeKeys,
|
||||
onChanged: (v) => c.updateSettings((x) => x.readerVolumeKeys = v),
|
||||
value: s.reader.volumeKeys,
|
||||
onChanged: (v) => c.updateSettings((x) => x.reader.volumeKeys = v),
|
||||
title: Text(tr('볼륨 키로 넘기기')),
|
||||
subtitle: Text(tr('보기 화면에서 볼륨 아래 = 다음 장, 위 = 이전 장 (동영상 · 다른 화면의 볼륨은 그대로)')),
|
||||
),
|
||||
|
|
@ -87,9 +87,9 @@ class ViewerSettings extends StatelessWidget {
|
|||
title: Text(tr('계속 보기 간격')),
|
||||
subtitle: Text(tr('보기 화면의 [계속 보기] 가 다음 장으로 넘기는 간격')),
|
||||
trailing: DropdownButton<int>(
|
||||
value: s.readerAutoSeconds,
|
||||
value: s.reader.autoSeconds,
|
||||
items: [for (var n = 1; n <= 60; n++) DropdownMenuItem(value: n, child: Text(trf('{0}초', [n])))],
|
||||
onChanged: (v) => c.updateSettings((x) => x.readerAutoSeconds = v!),
|
||||
onChanged: (v) => c.updateSettings((x) => x.reader.autoSeconds = v!),
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -254,7 +254,7 @@ class ExplorerOpener {
|
|||
|
||||
Future<void> openZipFile(BuildContext context, String path, ExplorerPane? pane) async {
|
||||
try {
|
||||
if (ctl.c.settings.zipComic && await openZipComic(context, ctl.c, path)) return;
|
||||
if (ctl.c.settings.reader.zipComic && await openZipComic(context, ctl.c, path)) return;
|
||||
} catch (e) {
|
||||
ctl.host.snack(trf('열 수 없습니다: {0}', [e]));
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -65,9 +65,9 @@ List<ReaderBarItem> readerBarItems(BuildContext context, AppController c, Reader
|
|||
final pictures = src is ImageFilesSource || src is ZipImagesSource;
|
||||
final aiOn = c.settings.components.contains('aiimage');
|
||||
final playing = st.playing;
|
||||
final secs = s.readerAutoSeconds;
|
||||
final secs = s.reader.autoSeconds;
|
||||
ReaderBarItem one(ReaderAct a) => ReaderBarItem(48, [a]);
|
||||
final split = switch (s.readerSplit) {
|
||||
final split = switch (s.reader.split) {
|
||||
'on' => tr('두 쪽 나눠 보기: 켜기 (누르면 끄기)'),
|
||||
'off' => tr('두 쪽 나눠 보기: 끄기 (누르면 자동)'),
|
||||
_ => tr('두 쪽 나눠 보기: 자동 - 두 쪽 스캔으로 보이는 그림만 (누르면 켜기)'),
|
||||
|
|
@ -94,30 +94,30 @@ List<ReaderBarItem> readerBarItems(BuildContext context, AppController c, Reader
|
|||
if (src is ImageFilesSource) one(ReaderAct(const Icon(Icons.info_outline), tr('사진 정보 (Exif)'), on.photoInfo)),
|
||||
// X-plore 맞추기 (관리자 지시 10/20): 아래 갤러리 줄 켜고 끄기 (그림 원본만)
|
||||
if (pictures)
|
||||
one(ReaderAct(const Icon(Icons.view_carousel_outlined), s.readerGallery ? tr('아래 갤러리 줄 숨기기') : tr('아래 갤러리 줄 보이기'),
|
||||
on.toggleGallery, on: s.readerGallery)),
|
||||
one(ReaderAct(const Icon(Icons.view_carousel_outlined), s.reader.gallery ? tr('아래 갤러리 줄 숨기기') : tr('아래 갤러리 줄 보이기'),
|
||||
on.toggleGallery, on: s.reader.gallery)),
|
||||
// 119 · 120
|
||||
one(ReaderAct(const Icon(Icons.vertical_split_outlined), split, on.cycleSplit, on: s.readerSplit != 'off')),
|
||||
one(ReaderAct(const Icon(Icons.vertical_split_outlined), split, on.cycleSplit, on: s.reader.split != 'off')),
|
||||
one(ReaderAct(const Icon(Icons.rotate_right), tr('90° 회전 (시계 방향)'), () => on.rotate(1), on: st.turns != 0)),
|
||||
one(ReaderAct(const Icon(Icons.rotate_left), tr('반대로 90° 회전'), () => on.rotate(3), on: st.turns != 0)),
|
||||
one(ReaderAct(const RotatedBox(quarterTurns: 1, child: Icon(Icons.flip)), tr('상하 반전'), on.toggleFlip, on: st.flipV)),
|
||||
one(ReaderAct(const Icon(Icons.fit_screen_outlined), tr('한 쪽 맞추기'), () => on.updateSettings((x) => x.readerFit = 'page'),
|
||||
on: s.readerFit == 'page')),
|
||||
one(ReaderAct(const Icon(Icons.fit_screen_outlined), tr('한 쪽 맞추기'), () => on.updateSettings((x) => x.reader.fit = 'page'),
|
||||
on: s.reader.fit == 'page')),
|
||||
one(ReaderAct(const Icon(Icons.width_full_outlined), tr('좌우 맞추기 (세로로 밀어 봄)'),
|
||||
() => on.updateSettings((x) => x.readerFit = 'width'), on: s.readerFit == 'width')),
|
||||
() => on.updateSettings((x) => x.reader.fit = 'width'), on: s.reader.fit == 'width')),
|
||||
one(ReaderAct(const Icon(Icons.swap_horiz),
|
||||
s.readerRtl ? tr('넘기는 방향: 오른쪽 → 왼쪽 (만화)') : tr('넘기는 방향: 왼쪽 → 오른쪽'),
|
||||
() => on.updateSettings((x) => x.readerRtl = !x.readerRtl), on: s.readerRtl)),
|
||||
s.reader.rtl ? tr('넘기는 방향: 오른쪽 → 왼쪽 (만화)') : tr('넘기는 방향: 왼쪽 → 오른쪽'),
|
||||
() => on.updateSettings((x) => x.reader.rtl = !x.reader.rtl), on: s.reader.rtl)),
|
||||
one(ReaderAct(const Icon(Icons.brightness_low), tr('어둡게'),
|
||||
() => on.updateSettings((x) => x.readerBrightness = (x.readerBrightness - 0.1).clamp(-0.7, 0.7)))),
|
||||
() => on.updateSettings((x) => x.reader.brightness = (x.reader.brightness - 0.1).clamp(-0.7, 0.7)))),
|
||||
one(ReaderAct(const Icon(Icons.brightness_high), tr('밝게'),
|
||||
() => on.updateSettings((x) => x.readerBrightness = (x.readerBrightness + 0.1).clamp(-0.7, 0.7)))),
|
||||
() => on.updateSettings((x) => x.reader.brightness = (x.reader.brightness + 0.1).clamp(-0.7, 0.7)))),
|
||||
// 30: 시계 · 알림 · 뒤로 막대를 그대로 / 화면 가득
|
||||
one(ReaderAct(Icon(s.readerSystemBars ? Icons.fullscreen : Icons.fullscreen_exit),
|
||||
s.readerSystemBars ? tr('화면 가득 (시스템 막대 숨기기)') : tr('시스템 막대 보이기 (시계 · 알림 · 뒤로)'), () async {
|
||||
await on.updateSettings((x) => x.readerSystemBars = !x.readerSystemBars);
|
||||
one(ReaderAct(Icon(s.reader.systemBars ? Icons.fullscreen : Icons.fullscreen_exit),
|
||||
s.reader.systemBars ? tr('화면 가득 (시스템 막대 숨기기)') : tr('시스템 막대 보이기 (시계 · 알림 · 뒤로)'), () async {
|
||||
await on.updateSettings((x) => x.reader.systemBars = !x.reader.systemBars);
|
||||
on.applySystemBars();
|
||||
}, on: s.readerSystemBars)),
|
||||
}, on: s.reader.systemBars)),
|
||||
// AI 는 막대가 좁으면 먼저 ⋮ 로 (늘 쓰는 버튼이 막대에 남게)
|
||||
// 121: AI 해상도 올리기 (이 장 · 모든 장) - 새 파일로 저장, 원본 · ZIP 은 그대로
|
||||
if (aiOn && src is! PdfSource) ...[
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ class _ReaderPageState extends State<ReaderPage> {
|
|||
|
||||
/// 보기 화면은 위 · 아래 시스템 막대까지 그림으로 (30: 설정으로 막대를 그대로 둘 수 있다)
|
||||
void _applySystemBars() => SystemChrome.setEnabledSystemUIMode(
|
||||
c.settings.readerSystemBars ? SystemUiMode.edgeToEdge : SystemUiMode.immersiveSticky);
|
||||
c.settings.reader.systemBars ? SystemUiMode.edgeToEdge : SystemUiMode.immersiveSticky);
|
||||
|
||||
// ───────── 쪽 나누기 (119) · 회전 · 반전 (120) ─────────
|
||||
|
||||
|
|
@ -139,14 +139,14 @@ class _ReaderPageState extends State<ReaderPage> {
|
|||
bool _flipV = false;
|
||||
|
||||
/// 이 장을 반씩 나눠 볼지. 자동은 두 쪽 스캔일 가능성이 높을 때만 ([autoSplitsPage])
|
||||
bool _splits(int page) => splitsPage(c.settings.readerSplit, page, _dims,
|
||||
bool _splits(int page) => splitsPage(c.settings.reader.split, page, _dims,
|
||||
turns: _turns,
|
||||
// 관리자 지시 (10/20) · 내 판단: HWP · HWPX 문서의 가로 쪽 (가로 표 등) 은 만화 두 쪽 스캔이 아니라 나누지 않는다
|
||||
bundle: src is! ImageFilesSource && src is! HwpSource);
|
||||
|
||||
/// 223: 나눠 본 장에서 바로 한 장으로 (설정을 "끄기" 로)
|
||||
Future<void> _splitOff() async {
|
||||
await c.updateSettings((x) => x.readerSplit = 'off');
|
||||
await c.updateSettings((x) => x.reader.split = 'off');
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_resetZoom();
|
||||
|
|
@ -177,7 +177,7 @@ class _ReaderPageState extends State<ReaderPage> {
|
|||
unawaited(ReaderImageCache.imageSize(r).then((size) {
|
||||
if (!mounted || size == null || v != _version || _dims.containsKey(page)) return;
|
||||
_dims[page] = size;
|
||||
if (c.settings.readerSplit == 'auto') setState(_rebuildPages);
|
||||
if (c.settings.reader.split == 'auto') setState(_rebuildPages);
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
@ -188,7 +188,7 @@ class _ReaderPageState extends State<ReaderPage> {
|
|||
});
|
||||
|
||||
Future<void> _cycleSplit() async {
|
||||
await c.updateSettings((x) => x.readerSplit = switch (x.readerSplit) { 'auto' => 'on', 'on' => 'off', _ => 'auto' });
|
||||
await c.updateSettings((x) => x.reader.split = switch (x.reader.split) { 'auto' => 'on', 'on' => 'off', _ => 'auto' });
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_resetZoom();
|
||||
|
|
@ -198,7 +198,7 @@ class _ReaderPageState extends State<ReaderPage> {
|
|||
|
||||
// ───────── 계속 보기 (117) ─────────
|
||||
|
||||
late final _show = ReaderSlideshow(seconds: () => c.settings.readerAutoSeconds, onTick: _tick);
|
||||
late final _show = ReaderSlideshow(seconds: () => c.settings.reader.autoSeconds, onTick: _tick);
|
||||
bool get _playing => _show.playing;
|
||||
|
||||
/// 계속 보기를 멈추게 한 누름은 장 넘기기 · 막대 숨기기로 쓰지 않는다
|
||||
|
|
@ -235,7 +235,7 @@ class _ReaderPageState extends State<ReaderPage> {
|
|||
}
|
||||
|
||||
Future<void> _setSeconds(int d) async {
|
||||
await c.updateSettings((x) => x.readerAutoSeconds = (x.readerAutoSeconds + d).clamp(1, 60));
|
||||
await c.updateSettings((x) => x.reader.autoSeconds = (x.reader.autoSeconds + d).clamp(1, 60));
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
if (_playing) _play(true); // 새 간격으로 (지금 장이 보이면 처음부터 다시)
|
||||
|
|
@ -311,7 +311,7 @@ class _ReaderPageState extends State<ReaderPage> {
|
|||
|
||||
/// X-plore 맞추기 (관리자 지시 10/20): 볼륨 키로 넘기기 (설정을 켰고 Android 일 때만 - 처리한 키는 시스템 볼륨을 바꾸지 않는다)
|
||||
KeyEventResult _onKey(FocusNode _, KeyEvent e) {
|
||||
final step = volumeKeyStep(e, enabled: c.settings.readerVolumeKeys && ReaderPage.volumeKeysSupported());
|
||||
final step = volumeKeyStep(e, enabled: c.settings.reader.volumeKeys && ReaderPage.volumeKeysSupported());
|
||||
if (step == null) return KeyEventResult.ignored;
|
||||
if (step != 0) _go(step);
|
||||
return KeyEventResult.handled;
|
||||
|
|
@ -332,7 +332,7 @@ class _ReaderPageState extends State<ReaderPage> {
|
|||
_swallowTap = false;
|
||||
return;
|
||||
}
|
||||
final step = tapZone(d.localPosition.dx, width, rtl: c.settings.readerRtl);
|
||||
final step = tapZone(d.localPosition.dx, width, rtl: c.settings.reader.rtl);
|
||||
if (step != 0) {
|
||||
_go(step);
|
||||
} else {
|
||||
|
|
@ -357,7 +357,7 @@ class _ReaderPageState extends State<ReaderPage> {
|
|||
imageKey: '$_version:${slot.page}:${slot.part}:${_retry[slot.page] ?? 0}',
|
||||
image: _images.sharp(i) ?? (slot.part >= 0 ? _images.half(slot.page, _width) : _load(slot.page)),
|
||||
zoom: _zoom.of(i), zoomed: _zoom.zoomed, holdPage: _zoom.holdPage,
|
||||
turns: _turns, flipV: _flipV, rtl: c.settings.readerRtl, fit: c.settings.readerFit, box: box,
|
||||
turns: _turns, flipV: _flipV, rtl: c.settings.reader.rtl, fit: c.settings.reader.fit, box: box,
|
||||
onShown: () => _markShown(i), onZoomed: _setZoomed, onZoomEnd: () => _onZoomEnd(i),
|
||||
// 관리자 지시 (10/20, HWP 2단계): [다시 시도] - 그 장의 기억을 지우고 다시 부른다
|
||||
onRetry: () => setState(() {
|
||||
|
|
@ -430,7 +430,7 @@ class _ReaderPageState extends State<ReaderPage> {
|
|||
}
|
||||
|
||||
Future<void> _toggleGallery() async {
|
||||
await c.updateSettings((x) => x.readerGallery = !x.readerGallery);
|
||||
await c.updateSettings((x) => x.reader.gallery = !x.reader.gallery);
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _scrollGallery(animate: false));
|
||||
|
|
@ -449,9 +449,9 @@ class _ReaderPageState extends State<ReaderPage> {
|
|||
final n = src.length;
|
||||
return ReaderBottomBar(
|
||||
pages: n, slots: _v.length, index: _index, label: slotLabel(_cur),
|
||||
pageName: n == 0 ? '' : src.pageName(_cur.page), rtl: c.settings.readerRtl, onSlide: _pages.jumpToPage,
|
||||
gallery: _pictures && c.settings.readerGallery && n > 0
|
||||
? ReaderGalleryStrip(controller: _gallery, count: n, current: _cur.page, rtl: c.settings.readerRtl,
|
||||
pageName: n == 0 ? '' : src.pageName(_cur.page), rtl: c.settings.reader.rtl, onSlide: _pages.jumpToPage,
|
||||
gallery: _pictures && c.settings.reader.gallery && n > 0
|
||||
? ReaderGalleryStrip(controller: _gallery, count: n, current: _cur.page, rtl: c.settings.reader.rtl,
|
||||
thumb: _images.thumb, onTap: _goPage)
|
||||
: null,
|
||||
);
|
||||
|
|
@ -478,9 +478,9 @@ class _ReaderPageState extends State<ReaderPage> {
|
|||
version: _version,
|
||||
count: _v.length,
|
||||
controller: _pages,
|
||||
rtl: s.readerRtl,
|
||||
rtl: s.reader.rtl,
|
||||
holdPage: _zoom.holdPage,
|
||||
brightness: s.readerBrightness,
|
||||
brightness: s.reader.brightness,
|
||||
onWidth: (w) => _width = w,
|
||||
onPointerDown: _onPointerDown,
|
||||
onPointerUp: () => _finger(-1),
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ void main() {
|
|||
final s = AppSettings();
|
||||
expect([
|
||||
s.startMenuShortcut, s.copy.syncStopToast, s.swipeWrap, s.migrationNotice, s.allowInnerToOuter,
|
||||
s.rememberPasswords, s.recycleOnDelete, s.readerSystemBars,
|
||||
s.rememberPasswords, s.recycleOnDelete, s.reader.systemBars,
|
||||
], everyElement(isTrue));
|
||||
expect(s.cleanupPrechecked, ['work', 'download']);
|
||||
expect(s.tripleTapAction, 'move');
|
||||
|
|
@ -37,7 +37,7 @@ void main() {
|
|||
expect([s.webPauseOnLeave, s.autoCheckUpdates, s.youtubeAdSkip, s.youtubeAdHide, s.runInBackground, s.swipeNav],
|
||||
everyElement(isTrue));
|
||||
// 탐색기 처음 배치 = 두 창 (규칙 7 · 관리자 결정 3, 10/22)
|
||||
expect([s.copy.liveSyncOnStart, s.copy.liveSyncIntervalSec, s.explorer.layout, s.zipComic], ['auto', 30, 'dual', false]);
|
||||
expect([s.copy.liveSyncOnStart, s.copy.liveSyncIntervalSec, s.explorer.layout, s.reader.zipComic], ['auto', 30, 'dual', false]);
|
||||
expect([AppSettings.uiScaleMin, AppSettings.uiScaleMax], [0.5, 2.5]);
|
||||
// 바꾼 값은 남는다
|
||||
final b = AppSettings.fromJson((AppSettings()
|
||||
|
|
|
|||
|
|
@ -84,15 +84,15 @@ void main() {
|
|||
|
||||
test('117 · 119 설정: 처음 값 3초 · 자동, 1 ~ 60 초', () {
|
||||
final s = AppSettings.fromJson({});
|
||||
expect((s.readerAutoSeconds, s.readerSplit), (3, 'auto'));
|
||||
expect((s.reader.autoSeconds, s.reader.split), (3, 'auto'));
|
||||
final b = AppSettings.fromJson((AppSettings()
|
||||
..readerAutoSeconds = 12
|
||||
..readerSplit = 'on')
|
||||
..reader.autoSeconds = 12
|
||||
..reader.split = 'on')
|
||||
.toJson());
|
||||
expect((b.readerAutoSeconds, b.readerSplit), (12, 'on'));
|
||||
expect(AppSettings.fromJson({'readerAutoSeconds': 500}).readerAutoSeconds, 60);
|
||||
expect(AppSettings.fromJson({'readerAutoSeconds': 0}).readerAutoSeconds, 1);
|
||||
expect(AppSettings.fromJson({'readerSplit': 'x'}).readerSplit, 'auto');
|
||||
expect((b.reader.autoSeconds, b.reader.split), (12, 'on'));
|
||||
expect(AppSettings.fromJson({'readerAutoSeconds': 500}).reader.autoSeconds, 60);
|
||||
expect(AppSettings.fromJson({'readerAutoSeconds': 0}).reader.autoSeconds, 1);
|
||||
expect(AppSettings.fromJson({'readerSplit': 'x'}).reader.split, 'auto');
|
||||
});
|
||||
|
||||
testWidgets('119: 자동이면 가로로 긴 그림만 반씩 (12-1 · 12-2), 표지 (세로) 는 통째 · 반쪽은 두 배 너비로 읽음', (t) async {
|
||||
|
|
@ -113,17 +113,17 @@ void main() {
|
|||
// 버튼: 자동 → 켜기 (모든 장) → 끄기 → 자동
|
||||
await t.tap(find.byTooltip('두 쪽 나눠 보기: 자동 - 두 쪽 스캔으로 보이는 그림만 (누르면 켜기)'));
|
||||
await t.pumpAndSettle();
|
||||
expect(c.settings.readerSplit, 'on');
|
||||
expect(c.settings.reader.split, 'on');
|
||||
expect(find.text('2-2 / 3장'), findsOneWidget, reason: '보던 쪽 그대로');
|
||||
await tapAt(t, 850);
|
||||
expect(find.text('3-1 / 3장'), findsOneWidget);
|
||||
await t.tap(find.byTooltip('두 쪽 나눠 보기: 켜기 (누르면 끄기)'));
|
||||
await t.pumpAndSettle();
|
||||
expect(c.settings.readerSplit, 'off');
|
||||
expect(c.settings.reader.split, 'off');
|
||||
expect(find.text('3 / 3'), findsOneWidget);
|
||||
await t.tap(find.byTooltip('두 쪽 나눠 보기: 끄기 (누르면 자동)'));
|
||||
await t.pumpAndSettle();
|
||||
expect(c.settings.readerSplit, 'auto');
|
||||
expect(c.settings.reader.split, 'auto');
|
||||
});
|
||||
|
||||
test('223: 자동 나누기 판단 - 묶음 (ZIP · CBZ · PDF) 의 가로 그림은 나눔, 폴더는 다른 그림 대부분이 세로일 때만', () {
|
||||
|
|
@ -154,12 +154,12 @@ void main() {
|
|||
expect(find.text('1-1 / 1장'), findsOneWidget);
|
||||
await t.tap(find.byTooltip('두 쪽 나눔 끄기 (한 장으로 보기)'));
|
||||
await t.pumpAndSettle();
|
||||
expect(c.settings.readerSplit, 'off');
|
||||
expect(c.settings.reader.split, 'off');
|
||||
expect(find.text('1 / 1'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('119: 오른쪽 → 왼쪽으로 읽으면 오른쪽 반부터', (t) async {
|
||||
final c = controller()..settings.readerRtl = true;
|
||||
final c = controller()..settings.reader.rtl = true;
|
||||
final src = await source(t, [(200, 100)]);
|
||||
await open(t, c, src);
|
||||
expect(find.text('1-1 / 1장'), findsOneWidget);
|
||||
|
|
@ -199,13 +199,13 @@ void main() {
|
|||
await open(t, c, src);
|
||||
await t.tap(find.byTooltip('계속 보기 간격 늘리기 (지금 3초)'));
|
||||
await t.pumpAndSettle();
|
||||
expect(c.settings.readerAutoSeconds, 4);
|
||||
expect(c.settings.reader.autoSeconds, 4);
|
||||
expect(find.text('4초'), findsOneWidget);
|
||||
await t.tap(find.byTooltip('계속 보기 간격 줄이기 (지금 4초)'));
|
||||
await t.pumpAndSettle();
|
||||
await t.tap(find.byTooltip('계속 보기 간격 줄이기 (지금 3초)'));
|
||||
await t.pumpAndSettle();
|
||||
expect(c.settings.readerAutoSeconds, 2);
|
||||
expect(c.settings.reader.autoSeconds, 2);
|
||||
|
||||
await t.tap(find.byTooltip('계속 보기 (2초마다 다음 장)'));
|
||||
await t.pump();
|
||||
|
|
@ -298,7 +298,7 @@ void main() {
|
|||
}
|
||||
await t.tap(find.text('넘기는 방향: 왼쪽 → 오른쪽'));
|
||||
await t.pumpAndSettle();
|
||||
expect(c.settings.readerRtl, isTrue);
|
||||
expect(c.settings.reader.rtl, isTrue);
|
||||
// 넓으면 메뉴 없이 모두
|
||||
await open(t, c, src, width: 1200);
|
||||
expect(find.byTooltip('더 보기'), findsNothing);
|
||||
|
|
|
|||
|
|
@ -58,12 +58,12 @@ void main() {
|
|||
test('보기 설정 저장 · 읽기', () {
|
||||
final s = AppSettings()
|
||||
..imageExts = ['png', 'webp']
|
||||
..readerFit = 'width'
|
||||
..readerRtl = true
|
||||
..readerBrightness = 0.3
|
||||
..zipComic = false;
|
||||
..reader.fit = 'width'
|
||||
..reader.rtl = true
|
||||
..reader.brightness = 0.3
|
||||
..reader.zipComic = false;
|
||||
final b = AppSettings.fromJson(s.toJson());
|
||||
expect([b.imageExts, b.readerFit, b.readerRtl, b.readerBrightness, b.zipComic], [
|
||||
expect([b.imageExts, b.reader.fit, b.reader.rtl, b.reader.brightness, b.reader.zipComic], [
|
||||
['png', 'webp'], 'width', true, 0.3, false,
|
||||
]);
|
||||
expect(AppSettings.fromJson({}).imageExts, defaultImageExtensions);
|
||||
|
|
@ -140,15 +140,15 @@ void main() {
|
|||
// 만화 (오른쪽 → 왼쪽): 왼쪽을 누르면 다음
|
||||
await tester.tap(find.byTooltip('넘기는 방향: 왼쪽 → 오른쪽'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(c.settings.readerRtl, isTrue);
|
||||
expect(c.settings.reader.rtl, isTrue);
|
||||
await tapAt(50);
|
||||
expect(find.text('3 / 3'), findsOneWidget);
|
||||
// 맞추기 · 밝기
|
||||
await tester.tap(find.byTooltip('좌우 맞추기 (세로로 밀어 봄)'));
|
||||
await tester.tap(find.byTooltip('밝게'));
|
||||
await tester.pump();
|
||||
expect(c.settings.readerFit, 'width');
|
||||
expect(c.settings.readerBrightness, closeTo(0.1, 1e-9));
|
||||
expect(c.settings.reader.fit, 'width');
|
||||
expect(c.settings.reader.brightness, closeTo(0.1, 1e-9));
|
||||
});
|
||||
|
||||
testWidgets('30: 두 손가락 확대 → 밀어도 장이 넘어가지 않음 · 더 크게 다시 그림 · 시스템 막대 보이기 설정', (tester) async {
|
||||
|
|
@ -181,20 +181,20 @@ void main() {
|
|||
await tester.pumpAndSettle();
|
||||
expect(find.text('1 / 2'), findsOneWidget);
|
||||
// 101: 기본은 시스템 막대 보임, 버튼으로 화면 가득 (설정에 남음)
|
||||
expect(c.settings.readerSystemBars, isTrue);
|
||||
expect(AppSettings.fromJson({}).readerSystemBars, isTrue);
|
||||
expect(c.settings.reader.systemBars, isTrue);
|
||||
expect(AppSettings.fromJson({}).reader.systemBars, isTrue);
|
||||
await tester.tap(find.byTooltip('화면 가득 (시스템 막대 숨기기)'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(c.settings.readerSystemBars, isFalse);
|
||||
expect(AppSettings.fromJson(c.settings.toJson()).readerSystemBars, isFalse);
|
||||
expect(c.settings.reader.systemBars, isFalse);
|
||||
expect(AppSettings.fromJson(c.settings.toJson()).reader.systemBars, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('X-plore 맞추기: 아래 갤러리 줄 - 처음은 끔, 켜면 썸네일 · 누르면 그 장으로, 위 막대에서 끄기', (tester) async {
|
||||
tester.view.physicalSize = const Size(1400, 1200);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
expect(AppSettings().readerGallery, isFalse, reason: '처음 값은 지금 동작 그대로');
|
||||
expect(AppSettings.fromJson({'readerGallery': true}).readerGallery, isTrue);
|
||||
expect(AppSettings().reader.gallery, isFalse, reason: '처음 값은 지금 동작 그대로');
|
||||
expect(AppSettings.fromJson({'readerGallery': true}).reader.gallery, isTrue);
|
||||
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,
|
||||
];
|
||||
|
|
@ -204,7 +204,7 @@ void main() {
|
|||
expect(find.byKey(const ValueKey('thumb0')), findsNothing, reason: '처음은 끔');
|
||||
await tester.tap(find.byTooltip('아래 갤러리 줄 보이기'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(c.settings.readerGallery, isTrue);
|
||||
expect(c.settings.reader.gallery, isTrue);
|
||||
expect(find.byKey(const ValueKey('thumb2')), findsOneWidget);
|
||||
await tester.tap(find.byKey(const ValueKey('thumb2')));
|
||||
await tester.pumpAndSettle();
|
||||
|
|
@ -222,7 +222,7 @@ void main() {
|
|||
final was = ReaderPage.volumeKeysSupported;
|
||||
addTearDown(() => ReaderPage.volumeKeysSupported = was);
|
||||
ReaderPage.volumeKeysSupported = () => true;
|
||||
expect(AppSettings().readerVolumeKeys, isFalse);
|
||||
expect(AppSettings().reader.volumeKeys, 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,
|
||||
];
|
||||
|
|
@ -234,7 +234,7 @@ void main() {
|
|||
await tester.pumpAndSettle();
|
||||
expect(find.text('1 / 3'), findsOneWidget);
|
||||
// 켬
|
||||
c.settings.readerVolumeKeys = true;
|
||||
c.settings.reader.volumeKeys = true;
|
||||
expect(await tester.sendKeyEvent(LogicalKeyboardKey.audioVolumeDown), isTrue, reason: '처리함 - 시스템 볼륨은 바뀌지 않음');
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('2 / 3'), findsOneWidget);
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ void main() {
|
|||
expect(old.screenOrientation, 'auto');
|
||||
// 규칙 7 · 결정 3 (10/22): 처음 배치가 두 창이 되어 예전 'dual' 은 그대로 (옮겼다는 알림 없음)
|
||||
expect(old.explorer.layout, 'dual');
|
||||
expect(old.zipComic, isFalse);
|
||||
expect(old.reader.zipComic, isFalse);
|
||||
expect(old.migrated, containsAll(['orientation', 'zipComic']));
|
||||
expect(old.migrated, isNot(contains('explorerLayout')));
|
||||
// 다시 저장 · 읽기: 이제 사용자가 고른 값으로 보고 그대로 · 알림 없음
|
||||
|
|
|
|||
Loading…
Reference in New Issue