import 'dart:io'; import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:jj_mkvmaker/app/app_controller.dart'; import 'package:jj_mkvmaker/ui/app_actions.dart'; import 'package:jj_mkvmaker/app/subtitle_editor_controller.dart'; import 'package:jj_mkvmaker/core/app_manager.dart'; import 'package:jj_mkvmaker/core/dlna.dart'; import 'package:jj_mkvmaker/core/languages.dart'; import 'package:jj_mkvmaker/core/models.dart'; import 'package:jj_mkvmaker/core/srt.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/app_manager_page.dart'; import 'package:jj_mkvmaker/ui/dlna_page.dart'; import 'package:jj_mkvmaker/ui/subtitle_editor_page.dart'; import 'package:jj_mkvmaker/ui/theme.dart'; import 'package:jj_mkvmaker/ui/tor_page.dart'; import 'package:path/path.dart' as p; /// ⑦ 밝은 화면 (관리자 지시 10/21): 캡처가 없던 화면 (DLNA · 앱 관리자 · Tor · 자막 편집) 을 PNG 로 (JJ_SHOT_DIR, JJ_SHOT_THEME=light). /// 기기 · 네트워크 대신 가짜를 쓰고, 설정은 메모리에만 class _Apps implements AppManagerBackend { @override Future> list() async => [ for (final (i, n) in ['카메라', 'Chrome', 'JJ_MKVMaker', '메모'].indexed) InstalledApp( package: 'com.x.$i', label: n, version: '1.$i', apk: '', installed: DateTime(2026, 1, i + 1), updated: DateTime(2026, 2, i + 1), size: (i + 1) * 12000000, system: i == 0, splits: 0), ]; @override Future icon(String package) async => null; @override Future details(String package) async => true; @override Future uninstall(String package) async => true; @override Future backupDir() async => Directory.systemTemp.path; } 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_misc'); Directory(out).createSync(recursive: true); final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c.settings ..tor.noticeSeen = true ..components = [...c.settings.components, 'tor']; final key = GlobalKey(); Future shot(String name, Size size, Widget home, {Future Function()? then}) async { t.view.physicalSize = size; t.view.devicePixelRatio = 1; await t.runAsync(() => t.pumpWidget(MaterialApp( key: UniqueKey(), theme: buildTheme(), debugShowCheckedModeBanner: false, builder: (_, child) => AppScope(controller: c, child: RepaintBoundary(key: key, child: child!)), home: home, ))); for (var i = 0; i < 15; i++) { await t.runAsync(() => Future.delayed(const Duration(milliseconds: 100))); await t.pump(const Duration(milliseconds: 50)); } if (then != null) { await then(); for (var i = 0; i < 10; 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, 'misc_$name.png')).writeAsBytesSync(bytes!.buffer.asUint8List()); await t.pumpWidget(const SizedBox()); } const nas = DlnaServer('집 NAS', 'http://192.168.0.5/d.xml', 'http://192.168.0.5/ctl'); Widget dlna() => DlnaPage( c: c, discover: () async => [nas], browse: (s, id, start) async => ( entries: [ const DlnaEntry('v', '동영상', isContainer: true, childCount: 2), const DlnaEntry('x', '여행.mp4', upnpClass: 'object.item.videoItem'), ], total: 2 ), ); await shot('dlna_servers', const Size(960, 600), dlna()); await shot('dlna_folder', const Size(960, 600), dlna(), then: () => t.tap(find.text('집 NAS'))); await shot('apps_412', const Size(412, 800), AppManagerPage(backend: _Apps())); await shot('tor_960', const Size(960, 600), TorPage( c: c, startTor: () async {}, missingFeatures: () async => const [], applyProxy: () async => true, onLeft: () async {}, viewBuilder: (_, _) => const ColoredBox(color: Colors.white), )); final v = VideoItem(r'D:\m\영화.mkv'); final s = SubtitleEntry.external(path: r'D:\m\영화.ko.srt', language: languageOf('ko')); v.subtitles.add(s); final editor = SubtitleEditorController( parseSrt('1\n00:00:01,000 --> 00:00:03,000\n첫째\n\n2\n00:00:05,000 --> 00:00:07,000\n둘째\n\n3\n00:00:09,000 --> 00:00:11,000\n셋째\n'), saveCharset: 'UTF-8'); await shot('subtitle_960', const Size(960, 600), SubtitleEditorPage(app: c, video: v, entry: s, editor: editor)); t.view.reset(); }); }