diff --git a/integration_test/windows_662_shot_test.dart b/integration_test/windows_662_shot_test.dart index 080f16e..5781cf7 100644 --- a/integration_test/windows_662_shot_test.dart +++ b/integration_test/windows_662_shot_test.dart @@ -21,6 +21,8 @@ import 'package:path/path.dart' as p; /// 설정은 메모리에만 (저장하지 않음), 즐겨찾기는 시험 폴더 (JJ_SHOT_TMP). void main() { 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 key = GlobalKey(); diff --git a/integration_test/windows_check_shot_test.dart b/integration_test/windows_check_shot_test.dart index a16fca4..ffde860 100644 --- a/integration_test/windows_check_shot_test.dart +++ b/integration_test/windows_check_shot_test.dart @@ -27,6 +27,8 @@ import 'support/temp_dir.dart'; /// 시험 폴더 · 메모리 저장소만 쓴다 (사용자 설정 · 비밀번호는 건드리지 않음). 휴지통에 넣은 시험 항목은 끝에 $I 로 골라 지운다. void main() { 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'); // 시험 폴더: 항목이 아주 많은 시스템 임시 폴더 대신 (JJ_SHOT_TMP, 없으면 시스템 임시 폴더) Directory tempDir(String prefix) { diff --git a/integration_test/windows_i18n_shot_test.dart b/integration_test/windows_i18n_shot_test.dart index 60e3c37..c73972e 100644 --- a/integration_test/windows_i18n_shot_test.dart +++ b/integration_test/windows_i18n_shot_test.dart @@ -6,6 +6,7 @@ import 'package:flutter/gestures.dart' show kSecondaryButton; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_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/i18n_controller.dart'; import 'package:jj_mkvmaker/core/models.dart'; @@ -20,6 +21,8 @@ import 'package:path/path.dart' as p; /// 라이선스 화면 · 글 칸의 복사 · 붙여넣기 메뉴 · 뒤로 버튼 이름. 설정은 메모리에만 (저장하지 않음). void main() { 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 navKey = GlobalKey(); final shotKey = GlobalKey(); @@ -47,6 +50,7 @@ void main() { t.view.physicalSize = const Size(1280, 800); t.view.devicePixelRatio = 1; 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(() => t.pumpWidget(RepaintBoundary(key: shotKey, child: JjMkvMakerApp(controller: c, navigatorKey: navKey)))); await settle(t); @@ -88,6 +92,7 @@ void main() { t.view.devicePixelRatio = 1; addTearDown(t.view.reset); final c = AppController(PlatformServices.create()); + if (Platform.environment['JJ_SHOT_THEME'] == 'light') c.settings.uiBrightness = 'light'; for (var i = 0; i < checked; i++) { final v = VideoItem(p.join(Directory.systemTemp.path, 'jj_i18n_none', 'v$i.mp4')); c.videos.add(v); diff --git a/integration_test/windows_magnet_shot_test.dart b/integration_test/windows_magnet_shot_test.dart index 8e22e26..81b7565 100644 --- a/integration_test/windows_magnet_shot_test.dart +++ b/integration_test/windows_magnet_shot_test.dart @@ -43,6 +43,8 @@ class _Quiet implements DownloadBackend { /// 실행: flutter test integration_test\windows_magnet_shot_test.dart -d windows (JJ_SHOT_DIR 에 PNG) void main() { 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'); const h = 'magnet:?xt=urn:btih:0123456789abcdef0123456789abcdef0123456'; for (final lang in ['ko', 'en', 'ja']) { diff --git a/integration_test/windows_reader_shot_test.dart b/integration_test/windows_reader_shot_test.dart new file mode 100644 index 0000000..3c9c3ff --- /dev/null +++ b/integration_test/windows_reader_shot_test.dart @@ -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 = []; + 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.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(); + }); +} diff --git a/lib/ui/ai_dialog.dart b/lib/ui/ai_dialog.dart index 3eb4694..e49270a 100644 --- a/lib/ui/ai_dialog.dart +++ b/lib/ui/ai_dialog.dart @@ -117,7 +117,7 @@ String aiStartNotice(List videos, AiOptions o, {required bool thenBui } /// 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 (int, int)? aiEstimateMinutes(Duration total, int translateLanguages) { diff --git a/lib/ui/audio_now_playing.dart b/lib/ui/audio_now_playing.dart index a52fe88..def9040 100644 --- a/lib/ui/audio_now_playing.dart +++ b/lib/ui/audio_now_playing.dart @@ -50,7 +50,8 @@ class AudioNowPlaying extends StatelessWidget { valueListenable: state, builder: (_, s, _) => Text( '${_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, child: DecoratedBox( 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)), ), ); } diff --git a/lib/ui/downloads_page.dart b/lib/ui/downloads_page.dart index 1dd5ea0..8fa08a4 100644 --- a/lib/ui/downloads_page.dart +++ b/lib/ui/downloads_page.dart @@ -443,8 +443,9 @@ class DownloadProgressBar extends StatelessWidget { fontSize: height * 0.62, height: 1, 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)], ), ), ), diff --git a/lib/ui/player_page.dart b/lib/ui/player_page.dart index 995f902..050b36d 100644 --- a/lib/ui/player_page.dart +++ b/lib/ui/player_page.dart @@ -439,7 +439,8 @@ class _PlayerPageState extends State { icon: Icon(i), ); 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), child: Column(mainAxisSize: MainAxisSize.min, children: [ Row(children: [ @@ -447,7 +448,14 @@ class _PlayerPageState extends State { style: const TextStyle(fontSize: 12, color: Colors.white70, fontFamily: 'Consolas')), Expanded( 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( value: pos, max: total <= 0 ? 1 : total, diff --git a/lib/ui/reader_page.dart b/lib/ui/reader_page.dart index 0d42080..4822304 100644 --- a/lib/ui/reader_page.dart +++ b/lib/ui/reader_page.dart @@ -804,7 +804,8 @@ class _ReaderPageState extends State { Widget _barButton(_Act a, {bool compact = false}) => IconButton( tooltip: a.tip, - color: a.on ? JjColors.accent : Colors.white, + // 막대는 밝은 화면에서도 어두운 띠 - 강조는 어두운 화면의 청록 (밝은 화면의 진한 청록은 어두운 띠 위에서 안 보임) + color: a.on ? JjPalette.dark.accent : Colors.white, disabledColor: Colors.white38, icon: a.icon, visualDensity: compact ? VisualDensity.compact : null, @@ -1005,7 +1006,7 @@ class _ReaderPageState extends State { child: Container( margin: const EdgeInsets.all(3), 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( future: _thumb(i), builder: (_, snap) { @@ -1048,6 +1049,8 @@ class _ReaderPageState extends State { max: (count - 1).toDouble(), divisions: count - 1, onChanged: (v) => _pages.jumpToPage(v.round()), + // ⑦ 어두운 막대 위 - 밝은 화면에서도 어두운 화면의 청록 (null 이면 테마 그대로) + activeColor: JjColors.isLight ? JjPalette.dark.accent : null, ), ), ), diff --git a/lib/ui/timeline.dart b/lib/ui/timeline.dart index 17f98d7..1999bd8 100644 --- a/lib/ui/timeline.dart +++ b/lib/ui/timeline.dart @@ -151,7 +151,8 @@ class _SubtitleTimelineState extends State { top: 0, bottom: 0, 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 { style: TextStyle( fontSize: 11, height: 1.2, - color: sel ? Colors.black : JjColors.text), + color: sel ? (JjColors.isLight ? JjColors.onAccent : Colors.black) : JjColors.text), ), ), ), diff --git a/test/vault_page_test.dart b/test/vault_page_test.dart index a399aa7..ad5d033 100644 --- a/test/vault_page_test.dart +++ b/test/vault_page_test.dart @@ -29,11 +29,20 @@ void main() { Directory(p.join(tmp.path, 'out')).createSync(); vaultIterationsForTest = 1000; }); - tearDown(() { + tearDown(() async { vaultDirOf = was; vaultPickFolder = wasPick; 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.delayed(const Duration(milliseconds: 250)); + } + } }); AppController ctl() {