/// 약속: 다운로드 상자와 인코딩 막대 (코덱 · 크기 · 화질 · 보정) - 바꾸기는 AppController 가. library; import 'package:flutter/material.dart'; import '../../app/app_controller.dart'; import '../../app/download_manager.dart'; import '../../core/encode_options.dart'; import '../app_actions.dart'; import '../downloads_page.dart'; import '../encode_notice.dart'; import '../theme.dart'; import '../video_adjust_dialog.dart'; import '../../l10n/tr.dart'; /// 상단 "다운로딩" 상자: 누르면 다운로드 목록 class HomeDownloadBox extends StatelessWidget { final DownloadManager d; const HomeDownloadBox({super.key, required this.d}); @override Widget build(BuildContext context) => ListenableBuilder( listenable: d, builder: (context, _) { final n = d.downloadingCount; final p = d.overallProgress; return InkWell( borderRadius: BorderRadius.circular(6), onTap: () => DownloadsPage.open(Navigator.of(context), d), child: Container( width: 150, padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( border: Border.all(color: n > 0 ? JjColors.accent : JjColors.border), borderRadius: BorderRadius.circular(6), ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Icon(Icons.downloading, size: 16, color: n > 0 ? JjColors.accent : JjColors.textDim), const SizedBox(width: 6), Text( n > 0 ? trf('다운로딩 {0}{1}', [n, p == null ? '' : ' · ${(p * 100).toStringAsFixed(0)}%']) : trf('다운로드 {0}', [d.tasks.length]), style: const TextStyle(fontSize: 12)), ]), if (n > 0) ...[ const SizedBox(height: 4), ClipRRect( borderRadius: BorderRadius.circular(2), child: LinearProgressIndicator( value: p, minHeight: 4, backgroundColor: JjColors.panelHigh), ), ], ], ), ), ); }, ); } // ───────── 화면 크기 · 코덱 · 화질 ───────── class EncodeBar extends StatelessWidget { final AppController c; const EncodeBar({super.key, required this.c}); @override Widget build(BuildContext context) { final s = c.encode; // 32: MKV 를 만드는 동안만 (AI 자막 · 번역 중에는 다음 MKV 설정을 바꿀 수 있다) final locked = c.buildingMkv; final up = c.upscaleCount; Widget label(String t) => Padding( padding: const EdgeInsets.only(right: 6), child: Text(t, style: TextStyle(fontSize: 12, color: JjColors.textDim)), ); // 253: 글자가 크면 고르기 상자가 화면보다 넓어져 넘쳤다 - 제 폭 (가장 긴 항목) 을 쓰되 화면 폭을 넘지 않게 (글은 …) final maxDrop = (MediaQuery.sizeOf(context).width - 40).clamp(120.0, double.infinity); Widget drop(T value, List items, String Function(T) text, ValueChanged onChanged, {bool Function(T)? enabled}) => ConstrainedBox( constraints: BoxConstraints(maxWidth: maxDrop), child: IntrinsicWidth( child: DropdownButton( value: value, isDense: true, isExpanded: true, underline: const SizedBox(), style: TextStyle(fontSize: 13, color: JjColors.text), items: [ for (final i in items) DropdownMenuItem( value: i, enabled: enabled?.call(i) ?? true, child: Text( text(i) + ((enabled?.call(i) ?? true) ? '' : tr(' (사용 불가)')), maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: (enabled?.call(i) ?? true) ? null : JjColors.textDim), ), ), ], onChanged: locked ? null : (v) => onChanged(v as T), ), ), ); final compact = isCompact(context); final row = Row(children: [ label(tr('화면 크기')), drop(s.resolution, ResolutionChoice.values, (r) => r.label, (r) { c.setResolution(r); showEncodeNotice(context, c); }), const SizedBox(width: 20), label(tr('코덱')), drop(s.codec, VideoCodecChoice.values, (v) => v.label, c.setCodec, enabled: c.isCodecAvailable), const SizedBox(width: 20), if (s.reencode) ...[ label(tr('화질')), drop(s.quality, QualityChoice.values, (q) => q.label, c.setQuality), const SizedBox(width: 20), ], // 화면 비율 (가로 · 세로) · 회전 · 색 보정: 미리보기를 보며 고르는 창 Tooltip( message: s.adjusts ? '${tr('화면 · 색 보정')}: ${s.adjustSummary}' : tr('화면 비율 (가로 · 세로) · 회전 · 밝기 · 대비 · 채도 · 색온도'), child: (s.adjusts ? FilledButton.tonalIcon : OutlinedButton.icon)( onPressed: locked ? null : () => showVideoAdjust(context, c), icon: const Icon(Icons.tune, size: 18), label: Text(tr('화면 · 색 보정')), ), ), const SizedBox(width: 12), Expanded( // 31: "⚠ 원본보다 커서" 경고는 화면 너비와 상관없이 늘 보이게 맨 앞에 (넓은 화면에서 한 줄이 넘치면 설명 · 보정 요약 쪽이 잘린다) child: Text( '${s.reencode && up > 0 ? '${trf('⚠ {0}개는 원본보다 커서 화질 향상 없이 용량만 늘어납니다', [up])} · ' : ''}' '${s.adjusts ? s.adjustSummary : !s.reencode ? tr('영상·음성을 그대로 복사합니다 (빠름, 화질 손실 없음)') : tr('영상을 다시 인코딩합니다 (시간이 오래 걸림, 음성은 그대로)')}', overflow: compact ? null : TextOverflow.ellipsis, style: TextStyle( fontSize: 12, color: up > 0 && s.reencode ? JjColors.danger : JjColors.textDim), ), ), ]); // 241 시험에서 찾음: 폭 720 ~ 840 (노트 10+ 가로 등) 에서 한 줄이 넘쳤다 - 960 보다 좁으면 폰처럼 줄을 바꿔 보인다 (내 판단) // 256: 낮은 화면 (폰 가로) 은 여러 줄로 감기면 목록이 한 줄만 보였다 - 한 줄로 옆으로 밀게. // 31: 설명 · "⚠ 원본보다 커서" 경고는 맨 앞에 (밀지 않아도 보이게) if (!compact && MediaQuery.sizeOf(context).height < 500) { final text = [for (final w in row.children) if (w is Expanded) w.child]; return Container( width: double.infinity, height: 44, color: JjColors.panel, padding: const EdgeInsets.symmetric(horizontal: 8), child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row(children: [ for (final t in text) ...[ConstrainedBox(constraints: const BoxConstraints(maxWidth: 360), child: t), const SizedBox(width: 16)], for (final w in row.children) if (w is! Expanded) w, ]), ), ); } if (compact || MediaQuery.sizeOf(context).width < 960) { return Container( width: double.infinity, color: JjColors.panel, padding: const EdgeInsets.fromLTRB(8, 4, 8, 6), child: Wrap( crossAxisAlignment: WrapCrossAlignment.center, runSpacing: 6, // 31: 설명 · "⚠ 원본보다 커서 용량만 늘어납니다" 경고는 빼지 않고 한 줄을 다 써서 보여 준다 children: [ for (final w in row.children) w is Expanded ? SizedBox(width: double.infinity, child: w.child) : w, ], ), ); } return Container( height: 44, color: JjColors.panel, padding: const EdgeInsets.symmetric(horizontal: 16), child: row, ); } } // ───────── 왼쪽: 동영상 목록 ─────────