WIP ⑦ 밝은 화면 (나) 2: 보기 · 재생 · 나머지 화면 · 캡처 시험 넓힘 (검토 대기)

관리자 지시 10/21 (⑦ (나) 둘째 이후):
- 보기 화면 · 재생 막대는 밝은 화면에서도 어두운 띠 (그림 · 동영상 위 - 사진 보기 앱처럼, 내 판단).
  그 띠 위 강조 (켜진 단추 · 쪽 슬라이더 · 썸네일 테두리 · 재생 위치) 는 어두운 화면의 청록, 검은 바탕의 흐린 글자도 어두운 값
- 시간줄: 재생 위치 선 · 고른 칸 글자 (밝은 화면), AI 보라 (aiColor) 는 밝은 화면에서 진하게,
  다운로드 진행 막대 글자는 밝은 화면에서 진한 글자 + 흰 테두리
- 캡처 시험: 보기 화면 캡처 (windows_reader_shot_test) 를 새로 · 662 · i18n (앱 전체는 설정 화면 밝기) · check · magnet 에도 JJ_SHOT_THEME
- 시험 흔들림: 보관함 2단계 시험 정리 때 폴더를 잠깐 잡고 있으면 (errno 32) 몇 번 더 지움
- 확인: 어두운 화면 캡처를 808300b 와 픽셀로 견줘 (같은 커밋 두 번 찍은 흔들림 밖) 차이 0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xplore_p3b
RyuJongJin 2026-10-21 17:15:21 +09:00
parent 808300b69e
commit 0a9a2d0af5
12 changed files with 109 additions and 13 deletions

View File

@ -21,6 +21,8 @@ import 'package:path/path.dart' as p;
/// 설정은 메모리에만 (저장하지 않음), 즐겨찾기는 시험 폴더 (JJ_SHOT_TMP). /// 설정은 메모리에만 (저장하지 않음), 즐겨찾기는 시험 폴더 (JJ_SHOT_TMP).
void main() { void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized(); IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// ⑦ 밝은 화면 캡처: JJ_SHOT_THEME=light (JJ_SHOT_DIR 를 따로 두어 어두운 것과 견줌)
if (Platform.environment['JJ_SHOT_THEME'] == 'light') JjColors.use(Brightness.light);
final out = Platform.environment['JJ_SHOT_DIR'] ?? p.join(Directory.systemTemp.path, 'jj_shots_662'); final out = Platform.environment['JJ_SHOT_DIR'] ?? p.join(Directory.systemTemp.path, 'jj_shots_662');
final key = GlobalKey(); final key = GlobalKey();

View File

@ -27,6 +27,8 @@ import 'support/temp_dir.dart';
/// 시험 폴더 · 메모리 저장소만 쓴다 (사용자 설정 · 비밀번호는 건드리지 않음). 휴지통에 넣은 시험 항목은 끝에 $I 로 골라 지운다. /// 시험 폴더 · 메모리 저장소만 쓴다 (사용자 설정 · 비밀번호는 건드리지 않음). 휴지통에 넣은 시험 항목은 끝에 $I 로 골라 지운다.
void main() { void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized(); IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// ⑦ 밝은 화면 캡처: JJ_SHOT_THEME=light (JJ_SHOT_DIR 를 따로 두어 어두운 것과 견줌)
if (Platform.environment['JJ_SHOT_THEME'] == 'light') JjColors.use(Brightness.light);
final out = Platform.environment['JJ_SHOT_DIR'] ?? p.join(Directory.systemTemp.path, 'jj_shots'); final out = Platform.environment['JJ_SHOT_DIR'] ?? p.join(Directory.systemTemp.path, 'jj_shots');
// 시험 폴더: 항목이 아주 많은 시스템 임시 폴더 대신 (JJ_SHOT_TMP, 없으면 시스템 임시 폴더) // 시험 폴더: 항목이 아주 많은 시스템 임시 폴더 대신 (JJ_SHOT_TMP, 없으면 시스템 임시 폴더)
Directory tempDir(String prefix) { Directory tempDir(String prefix) {

View File

@ -6,6 +6,7 @@ import 'package:flutter/gestures.dart' show kSecondaryButton;
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart'; import 'package:integration_test/integration_test.dart';
import 'package:jj_mkvmaker/ui/theme.dart';
import 'package:jj_mkvmaker/app/app_controller.dart'; import 'package:jj_mkvmaker/app/app_controller.dart';
import 'package:jj_mkvmaker/app/i18n_controller.dart'; import 'package:jj_mkvmaker/app/i18n_controller.dart';
import 'package:jj_mkvmaker/core/models.dart'; import 'package:jj_mkvmaker/core/models.dart';
@ -20,6 +21,8 @@ import 'package:path/path.dart' as p;
/// 라이선스 화면 · 글 칸의 복사 · 붙여넣기 메뉴 · 뒤로 버튼 이름. 설정은 메모리에만 (저장하지 않음). /// 라이선스 화면 · 글 칸의 복사 · 붙여넣기 메뉴 · 뒤로 버튼 이름. 설정은 메모리에만 (저장하지 않음).
void main() { void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized(); IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// ⑦ 밝은 화면 캡처: JJ_SHOT_THEME=light (JJ_SHOT_DIR 를 따로 두어 어두운 것과 견줌)
if (Platform.environment['JJ_SHOT_THEME'] == 'light') JjColors.use(Brightness.light);
final out = Platform.environment['JJ_SHOT_DIR'] ?? p.join(Directory.systemTemp.path, 'jj_shots_i18n'); final out = Platform.environment['JJ_SHOT_DIR'] ?? p.join(Directory.systemTemp.path, 'jj_shots_i18n');
final navKey = GlobalKey<NavigatorState>(); final navKey = GlobalKey<NavigatorState>();
final shotKey = GlobalKey(); final shotKey = GlobalKey();
@ -47,6 +50,7 @@ void main() {
t.view.physicalSize = const Size(1280, 800); t.view.physicalSize = const Size(1280, 800);
t.view.devicePixelRatio = 1; t.view.devicePixelRatio = 1;
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
if (Platform.environment['JJ_SHOT_THEME'] == 'light') c.settings.uiBrightness = 'light';
await t.runAsync(() => i18n.apply(lang, save: false)); await t.runAsync(() => i18n.apply(lang, save: false));
await t.runAsync(() => t.pumpWidget(RepaintBoundary(key: shotKey, child: JjMkvMakerApp(controller: c, navigatorKey: navKey)))); await t.runAsync(() => t.pumpWidget(RepaintBoundary(key: shotKey, child: JjMkvMakerApp(controller: c, navigatorKey: navKey))));
await settle(t); await settle(t);
@ -88,6 +92,7 @@ void main() {
t.view.devicePixelRatio = 1; t.view.devicePixelRatio = 1;
addTearDown(t.view.reset); addTearDown(t.view.reset);
final c = AppController(PlatformServices.create()); final c = AppController(PlatformServices.create());
if (Platform.environment['JJ_SHOT_THEME'] == 'light') c.settings.uiBrightness = 'light';
for (var i = 0; i < checked; i++) { for (var i = 0; i < checked; i++) {
final v = VideoItem(p.join(Directory.systemTemp.path, 'jj_i18n_none', 'v$i.mp4')); final v = VideoItem(p.join(Directory.systemTemp.path, 'jj_i18n_none', 'v$i.mp4'));
c.videos.add(v); c.videos.add(v);

View File

@ -43,6 +43,8 @@ class _Quiet implements DownloadBackend {
/// 실행: flutter test integration_test\windows_magnet_shot_test.dart -d windows (JJ_SHOT_DIR 에 PNG) /// 실행: flutter test integration_test\windows_magnet_shot_test.dart -d windows (JJ_SHOT_DIR 에 PNG)
void main() { void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized(); IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// ⑦ 밝은 화면 캡처: JJ_SHOT_THEME=light (JJ_SHOT_DIR 를 따로 두어 어두운 것과 견줌)
if (Platform.environment['JJ_SHOT_THEME'] == 'light') JjColors.use(Brightness.light);
final out = Platform.environment['JJ_SHOT_DIR'] ?? p.join(Directory.systemTemp.path, 'jj_shots_magnet'); final out = Platform.environment['JJ_SHOT_DIR'] ?? p.join(Directory.systemTemp.path, 'jj_shots_magnet');
const h = 'magnet:?xt=urn:btih:0123456789abcdef0123456789abcdef0123456'; const h = 'magnet:?xt=urn:btih:0123456789abcdef0123456789abcdef0123456';
for (final lang in ['ko', 'en', 'ja']) { for (final lang in ['ko', 'en', 'ja']) {

View File

@ -0,0 +1,62 @@
import 'dart:io';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:image/image.dart' as img;
import 'package:integration_test/integration_test.dart';
import 'package:jj_mkvmaker/app/app_controller.dart';
import 'package:jj_mkvmaker/core/reader_sources.dart';
import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart';
import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart';
import 'package:jj_mkvmaker/services/platform_services.dart';
import 'package:jj_mkvmaker/ui/reader_page.dart';
import 'package:jj_mkvmaker/ui/theme.dart';
import 'package:path/path.dart' as p;
/// ⑦ 밝은 화면 (관리자 지시 10/21): 보기 화면 (그림 · 위아래 막대 · 썸네일 줄) 을 PNG 로 (JJ_SHOT_DIR, JJ_SHOT_THEME=light).
/// 시험 그림은 임시 폴더에만 만들고 끝나면 지운다
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
if (Platform.environment['JJ_SHOT_THEME'] == 'light') JjColors.use(Brightness.light);
testWidgets('보기 화면 그림', (t) async {
final out = Platform.environment['JJ_SHOT_DIR'] ?? p.join(Directory.systemTemp.path, 'jj_shots_reader');
Directory(out).createSync(recursive: true);
final tmp = Directory.systemTemp.createTempSync('jj_readershot_');
addTearDown(() => tmp.deleteSync(recursive: true));
final paths = <String>[];
for (var i = 0; i < 4; i++) {
final im = img.Image(width: 600, height: 400);
img.fill(im, color: img.ColorRgb8(40 + i * 50, 120, 200 - i * 40));
img.drawString(im, 'page ${i + 1}', font: img.arial48, x: 200, y: 170);
final f = File(p.join(tmp.path, 'p${i + 1}.png'))..writeAsBytesSync(img.encodePng(im));
paths.add(f.path);
}
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
final key = GlobalKey();
for (final (name, size) in [('960', const Size(960, 600)), ('412', const Size(412, 800))]) {
t.view.physicalSize = size;
t.view.devicePixelRatio = 1;
await t.runAsync(() => t.pumpWidget(RepaintBoundary(
key: key,
child: MaterialApp(
theme: buildTheme(),
debugShowCheckedModeBanner: false,
home: ReaderPage(c: c, source: ImageFilesSource(paths, tempDir: tmp.path, title: 'shot'), start: 1),
),
)));
for (var i = 0; i < 20; i++) {
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 100)));
await t.pump(const Duration(milliseconds: 50));
}
final boundary = key.currentContext!.findRenderObject()! as RenderRepaintBoundary;
final image = await t.runAsync(() => boundary.toImage());
final bytes = await t.runAsync(() => image!.toByteData(format: ui.ImageByteFormat.png));
File(p.join(out, 'reader_$name.png')).writeAsBytesSync(bytes!.buffer.asUint8List());
await t.pumpWidget(const SizedBox());
}
t.view.reset();
});
}

View File

@ -117,7 +117,7 @@ String aiStartNotice(List<VideoItem> videos, AiOptions o, {required bool thenBui
} }
/// 138: AI 작업 (자막 · 자막 & MKV) 의 색 - [MKV 만들기] (청록) 와 헷갈리지 않게 /// 138: AI 작업 (자막 · 자막 & MKV) 의 색 - [MKV 만들기] (청록) 와 헷갈리지 않게
const aiColor = Color(0xFFB388FF); Color get aiColor => JjColors.isLight ? const Color(0xFF6A3FC8) : const Color(0xFFB388FF); // ⑦ 밝은 화면은 진한 보라 (대비 4.5:1)
/// 138: AI 작업 시간 어림 (분): 영상 1시간 ≈ 음성인식 15~30분 + 번역할 언어마다 10~20분. 길이를 모르면 null /// 138: AI 작업 시간 어림 (분): 영상 1시간 ≈ 음성인식 15~30분 + 번역할 언어마다 10~20분. 길이를 모르면 null
(int, int)? aiEstimateMinutes(Duration total, int translateLanguages) { (int, int)? aiEstimateMinutes(Duration total, int translateLanguages) {

View File

@ -50,7 +50,8 @@ class AudioNowPlaying extends StatelessWidget {
valueListenable: state, valueListenable: state,
builder: (_, s, _) => Text( builder: (_, s, _) => Text(
'${_t(s.position)} / ${_t(s.duration)}', '${_t(s.position)} / ${_t(s.duration)}',
style: TextStyle(color: JjColors.textDim, fontSize: 14, fontFeatures: [FontFeature.tabularFigures()]), // 검은 바탕 위 - 밝은 화면에서도 어두운 화면의 흐린 글자색
style: TextStyle(color: JjPalette.dark.textDim, fontSize: 14, fontFeatures: [FontFeature.tabularFigures()]),
), ),
), ),
]), ]),
@ -65,7 +66,7 @@ class _Note extends StatelessWidget {
aspectRatio: 1, aspectRatio: 1,
child: DecoratedBox( child: DecoratedBox(
decoration: BoxDecoration(color: Color(0xFF1E1E1E), borderRadius: BorderRadius.all(Radius.circular(12))), decoration: BoxDecoration(color: Color(0xFF1E1E1E), borderRadius: BorderRadius.all(Radius.circular(12))),
child: Center(child: Icon(Icons.music_note, size: 96, color: JjColors.textDim)), child: Center(child: Icon(Icons.music_note, size: 96, color: JjPalette.dark.textDim)),
), ),
); );
} }

View File

@ -443,8 +443,9 @@ class DownloadProgressBar extends StatelessWidget {
fontSize: height * 0.62, fontSize: height * 0.62,
height: 1, height: 1,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Colors.white, // ⑦ 밝은 화면: 옅은 막대 위에서도 읽히게 진한 글자 + 흰 테두리
shadows: const [Shadow(blurRadius: 2, color: Colors.black)], color: JjColors.isLight ? JjColors.text : Colors.white,
shadows: JjColors.isLight ? const [Shadow(blurRadius: 3, color: Colors.white)] : const [Shadow(blurRadius: 2, color: Colors.black)],
), ),
), ),
), ),

View File

@ -439,7 +439,8 @@ class _PlayerPageState extends State<PlayerPage> {
icon: Icon(i), icon: Icon(i),
); );
return Container( return Container(
color: (_full || _popup) ? Colors.black.withValues(alpha: 0.6) : JjColors.panel, // ⑦ 재생 막대는 밝은 화면에서도 어두운 띠 (흰 단추 · 시각 글자가 보이게)
color: (_full || _popup) ? Colors.black.withValues(alpha: 0.6) : JjPalette.dark.panel,
padding: const EdgeInsets.fromLTRB(8, 0, 8, 4), padding: const EdgeInsets.fromLTRB(8, 0, 8, 4),
child: Column(mainAxisSize: MainAxisSize.min, children: [ child: Column(mainAxisSize: MainAxisSize.min, children: [
Row(children: [ Row(children: [
@ -447,7 +448,14 @@ class _PlayerPageState extends State<PlayerPage> {
style: const TextStyle(fontSize: 12, color: Colors.white70, fontFamily: 'Consolas')), style: const TextStyle(fontSize: 12, color: Colors.white70, fontFamily: 'Consolas')),
Expanded( Expanded(
child: SliderTheme( child: SliderTheme(
data: const SliderThemeData(trackHeight: 3, overlayShape: RoundSliderOverlayShape(overlayRadius: 10)), data: JjColors.isLight
? SliderThemeData(
trackHeight: 3,
overlayShape: const RoundSliderOverlayShape(overlayRadius: 10),
activeTrackColor: JjPalette.dark.accent,
thumbColor: JjPalette.dark.accent,
inactiveTrackColor: Colors.white24)
: const SliderThemeData(trackHeight: 3, overlayShape: RoundSliderOverlayShape(overlayRadius: 10)),
child: Slider( child: Slider(
value: pos, value: pos,
max: total <= 0 ? 1 : total, max: total <= 0 ? 1 : total,

View File

@ -804,7 +804,8 @@ class _ReaderPageState extends State<ReaderPage> {
Widget _barButton(_Act a, {bool compact = false}) => IconButton( Widget _barButton(_Act a, {bool compact = false}) => IconButton(
tooltip: a.tip, tooltip: a.tip,
color: a.on ? JjColors.accent : Colors.white, // 막대는 밝은 화면에서도 어두운 띠 - 강조는 어두운 화면의 청록 (밝은 화면의 진한 청록은 어두운 띠 위에서 안 보임)
color: a.on ? JjPalette.dark.accent : Colors.white,
disabledColor: Colors.white38, disabledColor: Colors.white38,
icon: a.icon, icon: a.icon,
visualDensity: compact ? VisualDensity.compact : null, visualDensity: compact ? VisualDensity.compact : null,
@ -1005,7 +1006,7 @@ class _ReaderPageState extends State<ReaderPage> {
child: Container( child: Container(
margin: const EdgeInsets.all(3), margin: const EdgeInsets.all(3),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all(color: i == _cur.page ? JjColors.accent : Colors.white24, width: i == _cur.page ? 2 : 1)), border: Border.all(color: i == _cur.page ? JjPalette.dark.accent : Colors.white24, width: i == _cur.page ? 2 : 1)),
child: FutureBuilder<ReaderImage>( child: FutureBuilder<ReaderImage>(
future: _thumb(i), future: _thumb(i),
builder: (_, snap) { builder: (_, snap) {
@ -1048,6 +1049,8 @@ class _ReaderPageState extends State<ReaderPage> {
max: (count - 1).toDouble(), max: (count - 1).toDouble(),
divisions: count - 1, divisions: count - 1,
onChanged: (v) => _pages.jumpToPage(v.round()), onChanged: (v) => _pages.jumpToPage(v.round()),
// ⑦ 어두운 막대 위 - 밝은 화면에서도 어두운 화면의 청록 (null 이면 테마 그대로)
activeColor: JjColors.isLight ? JjPalette.dark.accent : null,
), ),
), ),
), ),

View File

@ -151,7 +151,8 @@ class _SubtitleTimelineState extends State<SubtitleTimeline> {
top: 0, top: 0,
bottom: 0, bottom: 0,
child: IgnorePointer( child: IgnorePointer(
child: Container(width: 2, color: Colors.white), // ⑦ 밝은 화면: 흰 선은 밝은 바탕에서 안 보여 글자색으로
child: Container(width: 2, color: JjColors.isLight ? JjColors.text : Colors.white),
), ),
); );
}, },
@ -254,7 +255,7 @@ class _SubtitleTimelineState extends State<SubtitleTimeline> {
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11,
height: 1.2, height: 1.2,
color: sel ? Colors.black : JjColors.text), color: sel ? (JjColors.isLight ? JjColors.onAccent : Colors.black) : JjColors.text),
), ),
), ),
), ),

View File

@ -29,11 +29,20 @@ void main() {
Directory(p.join(tmp.path, 'out')).createSync(); Directory(p.join(tmp.path, 'out')).createSync();
vaultIterationsForTest = 1000; vaultIterationsForTest = 1000;
}); });
tearDown(() { tearDown(() async {
vaultDirOf = was; vaultDirOf = was;
vaultPickFolder = wasPick; vaultPickFolder = wasPick;
MasterLock.instance = null; MasterLock.instance = null;
tmp.deleteSync(recursive: true); // Windows 는 막 쓴 파일을 백신 · 색인이 잠깐 잡고 있을 수 있다 (errno 32) - 몇 번 더
for (var i = 0; ; i++) {
try {
tmp.deleteSync(recursive: true);
break;
} on FileSystemException {
if (i >= 20) rethrow;
await Future<void>.delayed(const Duration(milliseconds: 250));
}
}
}); });
AppController ctl() { AppController ctl() {