W JjChip: 고르기 칩 21곳을 JjChip 하나로 + 여럿 고르기 목록은 withPicked 하나로 + 모니터 캡처 흔들림 고정
- lib/ui/jj_chip.dart: 아이콘 없는 칩은 Material 처음 꼴 (고르면 앞에 ✓), 아이콘 칩은 고르면 그 자리에 ✓ 만 (62ba78d 규칙 -
예전에는 화면 · 색 보정 비율 칩 한 곳만 손으로). multi 면 FilterChip. 화면 · 색 보정 · 압축 · 찾기 · 일정 · 저장 공간 · 보기 ·
정리 · 받기 설정 · 즐겨찾기 · 자막 찾기 · 복사 방식 · 원본 정리 등 21곳이 부르기만 한다. 탐색기 왼쪽 / 오른쪽 창 고르기 칩은
고르기가 아니라 탭이라 그대로
- lib/core/picked_list.dart withPicked: 여럿 고르기 목록 켜고 끄기 (설정 3곳에 같은 코드가 있던 것)
- 시험: jj_chip_contract_test (아이콘 ✓ 바꾸기 · 처음 꼴 · multi · 못 누름 · withPicked)
- 앞 · 뒤 캡처 9 묶음 132장 각 두 번: 앞 · 뒤 0px (보이는 것은 그대로)
- 시험 쪽: monitor_shot 도 고정 폴더 · shotServices (CPU · 메모리 · 디스크 · 대상 디스크)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
modular
parent
7970dba271
commit
d1081f337e
|
|
@ -9,10 +9,10 @@ import 'package:jj_mkvmaker/app/app_controller.dart';
|
|||
import 'package:jj_mkvmaker/app/copy_center.dart';
|
||||
import 'package:jj_mkvmaker/app/live_sync.dart';
|
||||
import 'package:jj_mkvmaker/app/settings.dart';
|
||||
import 'package:jj_mkvmaker/services/platform_services.dart';
|
||||
import 'package:jj_mkvmaker/ui/monitor_page.dart';
|
||||
import 'package:jj_mkvmaker/ui/schedule_editor.dart';
|
||||
import 'package:jj_mkvmaker/ui/theme.dart';
|
||||
import 'support/shot_services.dart';
|
||||
import 'support/temp_dir.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
|
|
@ -24,7 +24,9 @@ void main() {
|
|||
|
||||
testWidgets('모니터링 화면 그림', (tester) async {
|
||||
final out = Platform.environment['JJ_SHOT_DIR'] ?? Directory.systemTemp.path;
|
||||
final tmp = testTempDir('jj_monshot_');
|
||||
// 관리자 지시 (10/24): 폴더 이름 · CPU · 메모리 · 디스크를 정한 값으로 (캡처가 흔들리지 않게 - 시험 쪽에서만).
|
||||
// 진행 줄 (속도 · % · 몇 번째 파일) 은 실제로 복사하는 중의 값이라 찍는 때에 따라 다를 수 있다
|
||||
final tmp = testFixedTempDir('jj_monshot_fixed');
|
||||
final src = p.join(tmp.path, '영상 원본'), dst = p.join(tmp.path, '백업');
|
||||
for (var i = 0; i < 6; i++) {
|
||||
File(p.join(src, i < 3 ? 'A' : 'B', 'clip$i.mkv'))
|
||||
|
|
@ -32,7 +34,7 @@ void main() {
|
|||
..writeAsBytesSync(List.filled(200 * 1024, i));
|
||||
}
|
||||
Directory(dst).createSync();
|
||||
final c = AppController(PlatformServices.create());
|
||||
final c = AppController(shotServices());
|
||||
c.settings.copy.liveSyncPairs = [
|
||||
LiveSyncPair(src, p.join(dst, 'mirror'), schedule: const ['0 9-17 * * 1-5', '0 22-23 * * 0,6'], delete: true),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
/// 약속 (여럿 고르기 목록): [withPicked] 목록 · 항목 · 켬 → 새 목록 (켜면 끝에 하나, 끄면 빼기 - 같은 것이 둘 생기지 않음, 다른 차례는 그대로).
|
||||
/// 원래 목록은 바꾸지 않는다. 오류 없음. 쓰는 곳: 설정의 여럿 고르기 칩 (그림 확장자 · 정리 미리 체크 · 로그인 쿠키 사이트)
|
||||
library;
|
||||
|
||||
List<T> withPicked<T>(List<T> list, T item, bool on) => [
|
||||
for (final x in list)
|
||||
if (x != item) x,
|
||||
if (on) item,
|
||||
];
|
||||
|
|
@ -14,6 +14,7 @@ import '../l10n/tr.dart';
|
|||
import 'app_actions.dart' show isCompact;
|
||||
import 'theme.dart';
|
||||
import 'secret_field.dart';
|
||||
import 'jj_chip.dart';
|
||||
|
||||
/// 86: 동봉한 7-Zip. 없으면 null
|
||||
/// - Windows: 실행 파일 옆 7zip\7z.exe (개발 중에는 third_party 의 것)
|
||||
|
|
@ -290,7 +291,7 @@ Future<String?> extractArchiveFlow(BuildContext context, AppController c, String
|
|||
Widget _placeChoice(BuildContext context, List<(String, String)> choices, String value, void Function(String) onPick) {
|
||||
if (!isCompact(context)) {
|
||||
return Wrap(spacing: 6, runSpacing: 4, children: [
|
||||
for (final (d, label) in choices) ChoiceChip(label: Text(label), selected: value == d, onSelected: (_) => onPick(d)),
|
||||
for (final (d, label) in choices) JjChip(label: label, selected: value == d, onSelected: (_) => onPick(d)),
|
||||
]);
|
||||
}
|
||||
return RadioGroup<String>(
|
||||
|
|
@ -348,7 +349,7 @@ Future<String?> createZipFlow(BuildContext context, AppController c, List<String
|
|||
const SizedBox(height: 10),
|
||||
Wrap(spacing: 6, children: [
|
||||
for (final (v, label) in [(1, tr('빠르게')), (6, tr('보통')), (9, tr('작게'))])
|
||||
ChoiceChip(label: Text(label), selected: level == v, onSelected: (_) => set(() => level = v)),
|
||||
JjChip(label: label, selected: level == v, onSelected: (_) => set(() => level = v)),
|
||||
]),
|
||||
const SizedBox(height: 6),
|
||||
_placeChoice(ctx, choices, where, (d) => set(() => where = d)),
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import '../bookmark_ui.dart';
|
|||
import '../key_hint.dart';
|
||||
import '../theme.dart';
|
||||
import '../jj_header_row.dart';
|
||||
import '../jj_chip.dart';
|
||||
|
||||
// ───────── 즐겨찾기 관리 패널 (Chrome 즐겨찾기 관리자 참고) ─────────
|
||||
|
||||
|
|
@ -87,8 +88,9 @@ class _BookmarkPanelState extends State<BookmarkPanel> {
|
|||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Wrap(spacing: 4, children: [
|
||||
for (final r in bm.tree.roots)
|
||||
ChoiceChip(
|
||||
label: Text(r.title, style: const TextStyle(fontSize: 11)),
|
||||
JjChip(
|
||||
label: r.title,
|
||||
style: const TextStyle(fontSize: 11),
|
||||
selected: _path().first.id == r.id,
|
||||
onSelected: (_) => setState(() => _folderId = r.id),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import '../app/tor_service.dart';
|
|||
import '../l10n/tr.dart';
|
||||
import 'setting_tile.dart';
|
||||
import 'theme.dart';
|
||||
import 'jj_chip.dart';
|
||||
import '../core/picked_list.dart';
|
||||
|
||||
/// 환경 설정 > 파일 탐색기 > 보기: 그림으로 볼 확장자 · ZIP 만화 보기 (이미지 · PDF · ZIP 보기 컴포넌트)
|
||||
class ViewerSettings extends StatelessWidget {
|
||||
|
|
@ -31,13 +33,11 @@ class ViewerSettings extends StatelessWidget {
|
|||
const SizedBox(height: 6),
|
||||
Wrap(spacing: 6, runSpacing: 6, children: [
|
||||
for (final e in _choices)
|
||||
FilterChip(
|
||||
label: Text(e),
|
||||
JjChip(
|
||||
multi: true,
|
||||
label: e,
|
||||
selected: s.reader.imageExts.contains(e),
|
||||
onSelected: (on) => c.updateSettings((x) => x.reader.imageExts = [
|
||||
for (final y in x.reader.imageExts) if (y != e) y,
|
||||
if (on) e,
|
||||
]),
|
||||
onSelected: (on) => c.updateSettings((x) => x.reader.imageExts = withPicked(x.reader.imageExts, e, on)),
|
||||
),
|
||||
]),
|
||||
]),
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import 'schedule_editor.dart';
|
|||
import 'theme.dart';
|
||||
import 'setting_tile.dart';
|
||||
import 'sync_preview_view.dart' show OptionTextFormatter, keepOptionsTogether, optionProblemText;
|
||||
import 'jj_chip.dart';
|
||||
|
||||
/// 환경 설정의 복사 · 동기화 부분.
|
||||
/// - 파일 탐색기 ([rsync] false): 파일 / 폴더 복사 방법 (현재 방식 · robocopy), robocopy 옵션, 속도 제한
|
||||
|
|
@ -335,7 +336,7 @@ class _LiveSyncTile extends StatelessWidget {
|
|||
Wrap(spacing: 6, children: [
|
||||
for (final m in CopyMethod.values)
|
||||
if (copyMethodAvailable(m, c.settings))
|
||||
ChoiceChip(label: Text(tr(m.label)), selected: method == m.name, onSelected: (_) => set(() => method = m.name)),
|
||||
JjChip(label: tr(m.label), selected: method == m.name, onSelected: (_) => set(() => method = m.name)),
|
||||
]),
|
||||
CheckboxListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import '../theme.dart';
|
|||
import 'explorer_buttons.dart';
|
||||
import 'explorer_controller.dart';
|
||||
import 'explorer_pane.dart';
|
||||
import '../jj_chip.dart';
|
||||
|
||||
/// 약속 (E5d-6): 보기를 바꾸는 창 - 정렬 기준 · 즐겨찾기 (Rsync 는 짝) · 내역 · 창 배치 (좌우 ⇆ 위아래 한 번에 · 배치 창) · 버튼 구성.
|
||||
/// 고른 것은 설정에 저장하고 (취소면 그대로), 배치가 바뀌면 보던 폴더가 다시 보이게 한다. [twoPanes]: 지금 창이 둘인지
|
||||
|
|
@ -35,7 +36,7 @@ class ExplorerViewDialogs {
|
|||
('size', tr('크기')),
|
||||
('type', tr('종류 (확장자)')),
|
||||
])
|
||||
ChoiceChip(label: Text(label), selected: by == v, onSelected: (_) => set(() => by = v)),
|
||||
JjChip(label: label, selected: by == v, onSelected: (_) => set(() => by = v)),
|
||||
]),
|
||||
SwitchListTile(value: desc, title: Text(tr('거꾸로')), onChanged: (x) => set(() => desc = x)),
|
||||
if (ctl.c.settings.explorer.sortPerFolder)
|
||||
|
|
@ -131,7 +132,7 @@ class ExplorerViewDialogs {
|
|||
),
|
||||
Wrap(spacing: 6, children: [
|
||||
for (final (v, label) in items)
|
||||
ChoiceChip(label: Text(label), selected: value == v, onSelected: (_) => set(() => on(v))),
|
||||
JjChip(label: label, selected: value == v, onSelected: (_) => set(() => on(v))),
|
||||
]),
|
||||
]);
|
||||
return AlertDialog(
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import '../l10n/tr.dart';
|
|||
import 'app_actions.dart' show isCompact;
|
||||
import 'theme.dart';
|
||||
import 'jj_title_bar.dart';
|
||||
import 'jj_chip.dart';
|
||||
|
||||
/// 79: 찾기 조건 - 이름 (없어도 됨) · 크기 · 바뀐 날 · 종류 ("1GB 넘는 30일 지난 영상")
|
||||
class SearchConditions {
|
||||
|
|
@ -209,8 +210,8 @@ class _ConditionsDialogState extends State<_ConditionsDialog> {
|
|||
// 종류
|
||||
Wrap(spacing: 6, runSpacing: 4, children: [
|
||||
for (final k in const ['all', 'video', 'audio', 'subtitle', 'image', 'custom'])
|
||||
ChoiceChip(
|
||||
label: Text(SearchConditions.kindLabel(k)),
|
||||
JjChip(
|
||||
label: SearchConditions.kindLabel(k),
|
||||
selected: _kind == k,
|
||||
onSelected: (_) => setState(() => _kind = k),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
/// 약속 (W 공용 위젯, 설계 7-2 JjChip): 고르기 칩 하나.
|
||||
/// 입력: 이름 · 고름 · 누름 알림 (null 이면 누를 수 없음) · 아이콘 (있으면) · 여럿 고르기 ([multi]) · 이름 글꼴.
|
||||
/// 출력: 아이콘이 없으면 Material 처음 꼴 (고르면 앞에 ✓). 아이콘이 있으면 고를 때 그 자리에 ✓ 만 (그림 위에 체크가 겹치지 않게 - 62ba78d).
|
||||
/// [multi] 면 FilterChip (여럿 켜고 끄기 - 목록은 core/picked_list 의 withPicked), 아니면 ChoiceChip (하나 고르기). 오류 없음.
|
||||
/// 쓰는 곳: 화면 · 색 보정 · 압축 · 찾기 · 일정 · 저장 공간 · 보기 · 정리 · 받기 설정 · 즐겨찾기 등. 화면은 부르기만 한다
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class JjChip extends StatelessWidget {
|
||||
const JjChip({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.selected,
|
||||
required this.onSelected,
|
||||
this.icon,
|
||||
this.multi = false,
|
||||
this.style,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final bool selected;
|
||||
final ValueChanged<bool>? onSelected;
|
||||
final IconData? icon;
|
||||
final bool multi;
|
||||
final TextStyle? style;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final text = Text(label, style: style);
|
||||
final avatar = icon == null ? null : Icon(selected ? Icons.check : icon, size: 16);
|
||||
final check = icon == null;
|
||||
return multi
|
||||
? FilterChip(label: text, avatar: avatar, showCheckmark: check, selected: selected, onSelected: onSelected)
|
||||
: ChoiceChip(label: text, avatar: avatar, showCheckmark: check, selected: selected, onSelected: onSelected);
|
||||
}
|
||||
}
|
||||
|
|
@ -27,6 +27,7 @@ import '../../core/sync_preview.dart' show optionsRemoveSource;
|
|||
import '../sync_preview_view.dart' show OptionProblemNote, OptionTextFormatter, confirmDeletingRun, effectiveRunText;
|
||||
import '../theme.dart';
|
||||
import 'transfer_widgets.dart';
|
||||
import '../jj_chip.dart';
|
||||
|
||||
class CopyTaskList extends StatelessWidget {
|
||||
final AppController c;
|
||||
|
|
@ -384,7 +385,7 @@ class _CopyTaskCardState extends State<CopyTaskCard> {
|
|||
),
|
||||
),
|
||||
if (t.sources.length > 1 && _method == 'rsync')
|
||||
FilterChip(label: Text(tr('한 번에')), selected: _once, onSelected: (v) => setState(() => _once = v)),
|
||||
JjChip(multi: true, label: tr('한 번에'), selected: _once, onSelected: (v) => setState(() => _once = v)),
|
||||
// 이동 (원본 파일 지우기) 이면: 끝난 뒤 원본의 빈 폴더 · 원본 폴더 정리
|
||||
if (t.move && _method == 'rsync')
|
||||
DropdownButton<String>(
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import '../../app/transfer_job.dart';
|
|||
import '../../core/file_ops.dart';
|
||||
import '../../l10n/tr.dart';
|
||||
import '../theme.dart';
|
||||
import '../jj_chip.dart';
|
||||
|
||||
/// 실행 시각 (ISO) 을 "월/일 시:분" 으로. 날짜가 아니면 빈 글.
|
||||
String formatRunStamp(String iso) {
|
||||
|
|
@ -143,6 +144,6 @@ class PruneChoice extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) => Wrap(spacing: 6, runSpacing: 6, children: [
|
||||
for (final v in [if (allowNone) '', 'keep', 'all'])
|
||||
ChoiceChip(label: Text(pruneLabel(v)), selected: value == v, onSelected: (_) => onChanged(v)),
|
||||
JjChip(label: pruneLabel(v), selected: value == v, onSelected: (_) => onChanged(v)),
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import '../core/cron_window.dart';
|
|||
import '../l10n/tr.dart';
|
||||
import 'theme.dart';
|
||||
import 'jj_field.dart';
|
||||
import 'jj_chip.dart';
|
||||
|
||||
/// 요일 이름 (0 = 일요일)
|
||||
List<String> weekdayNames() => [tr('일'), tr('월'), tr('화'), tr('수'), tr('목'), tr('금'), tr('토')];
|
||||
|
|
@ -85,13 +86,13 @@ class _ScheduleDialogState extends State<_ScheduleDialog> {
|
|||
child: SingleChildScrollView(
|
||||
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Wrap(spacing: 8, children: [
|
||||
ChoiceChip(
|
||||
label: Text(tr('계속')),
|
||||
JjChip(
|
||||
label: tr('계속'),
|
||||
selected: _always,
|
||||
onSelected: (_) => setState(() => _always = true),
|
||||
),
|
||||
ChoiceChip(
|
||||
label: Text(tr('시간 지정 (cron)')),
|
||||
JjChip(
|
||||
label: tr('시간 지정 (cron)'),
|
||||
selected: !_always,
|
||||
onSelected: (_) => setState(() => _always = false),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import '../theme.dart';
|
|||
import '../../l10n/tr.dart';
|
||||
import '../setting_tile.dart';
|
||||
import '../license_texts.dart';
|
||||
import '../jj_chip.dart';
|
||||
import '../../core/picked_list.dart';
|
||||
|
||||
// 약속 (설정 B): 환경 설정의 분야 - 실행 · 종료 · 저장 공간 정리 · 프로그램 정보. 바꾸면 바로 설정에 저장 (c.updateSettings)
|
||||
|
||||
|
|
@ -151,13 +153,12 @@ class _CleanupSettingsSectionState extends State<CleanupSettingsSection> {
|
|||
('update', tr('업데이트 · 설치하고 남은 파일')),
|
||||
('logs', tr('지난 작업 기록')),
|
||||
])
|
||||
FilterChip(
|
||||
label: Text(label),
|
||||
JjChip(
|
||||
multi: true,
|
||||
label: label,
|
||||
selected: s.general.cleanupPrechecked.contains(id),
|
||||
onSelected: (on) => c.updateSettings((x) => x.general.cleanupPrechecked = [
|
||||
for (final y in x.general.cleanupPrechecked) if (y != id) y,
|
||||
if (on) id,
|
||||
]),
|
||||
onSelected: (on) =>
|
||||
c.updateSettings((x) => x.general.cleanupPrechecked = withPicked(x.general.cleanupPrechecked, id, on)),
|
||||
),
|
||||
]),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import '../setting_tile.dart';
|
|||
import '../browser_page.dart' show deleteExportedCookies;
|
||||
import '../toast_status.dart';
|
||||
import 'settings_tiles.dart';
|
||||
import '../jj_chip.dart';
|
||||
import '../../core/picked_list.dart';
|
||||
|
||||
// 약속 (설정 B): 환경 설정의 분야 - Rsync · 다운로드. 바꾸면 바로 설정에 저장 (c.updateSettings)
|
||||
|
||||
|
|
@ -193,13 +195,12 @@ class _DownloadSettingsSectionState extends State<DownloadSettingsSection> {
|
|||
const SizedBox(height: 6),
|
||||
Wrap(spacing: 6, runSpacing: 6, children: [
|
||||
for (final d in loginCookieDomains)
|
||||
FilterChip(
|
||||
label: Text(d),
|
||||
JjChip(
|
||||
multi: true,
|
||||
label: d,
|
||||
selected: s.download.loginCookieSites.contains(d),
|
||||
onSelected: (on) => c.updateSettings((x) => x.download.loginCookieSites = [
|
||||
for (final y in x.download.loginCookieSites) if (y != d) y,
|
||||
if (on) d,
|
||||
]),
|
||||
onSelected: (on) =>
|
||||
c.updateSettings((x) => x.download.loginCookieSites = withPicked(x.download.loginCookieSites, d, on)),
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
icon: const Icon(Icons.cookie_outlined, size: 18),
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import '../l10n/tr.dart';
|
|||
import 'storage_chart.dart';
|
||||
import 'theme.dart';
|
||||
import 'jj_title_bar.dart';
|
||||
import 'jj_chip.dart';
|
||||
|
||||
/// 87: 저장 공간 분석 - 고른 폴더 · 드라이브 아래의 큰 폴더 · 큰 파일 차례.
|
||||
/// 폴더를 누르면 그 안으로 (한 번 훑은 결과로 바로), [그 자리로] 는 탐색기에서 그 자리로 (돌려주는 값).
|
||||
|
|
@ -202,9 +203,9 @@ class _StorageAnalysisPageState extends State<StorageAnalysisPage> {
|
|||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
child: Wrap(spacing: 6, children: [
|
||||
ChoiceChip(label: Text(tr('큰 폴더')), selected: _tab == 0, onSelected: (_) => setState(() => _tab = 0)),
|
||||
ChoiceChip(label: Text(tr('큰 파일')), selected: _tab == 1, onSelected: (_) => setState(() => _tab = 1)),
|
||||
ChoiceChip(label: Text(tr('칸 그림')), selected: _tab == 2, onSelected: (_) => setState(() => _tab = 2)),
|
||||
JjChip(label: tr('큰 폴더'), selected: _tab == 0, onSelected: (_) => setState(() => _tab = 0)),
|
||||
JjChip(label: tr('큰 파일'), selected: _tab == 1, onSelected: (_) => setState(() => _tab = 1)),
|
||||
JjChip(label: tr('칸 그림'), selected: _tab == 2, onSelected: (_) => setState(() => _tab = 2)),
|
||||
]),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import '../core/models.dart';
|
|||
import '../core/subtitle_search.dart';
|
||||
import 'theme.dart';
|
||||
import '../l10n/tr.dart';
|
||||
import 'jj_chip.dart';
|
||||
|
||||
/// 인터넷 자막 찾기 → 목록에서 골라 받기
|
||||
Future<void> showSubtitleSearch(BuildContext context, AppController c, VideoItem v) =>
|
||||
|
|
@ -223,10 +224,12 @@ class _SearchDialogState extends State<_SearchDialog> {
|
|||
),
|
||||
const SizedBox(width: 12),
|
||||
if (_hash != null)
|
||||
FilterChip(
|
||||
JjChip(
|
||||
multi: true,
|
||||
selected: _useHash,
|
||||
onSelected: (v) => setState(() => _useHash = v),
|
||||
label: Text(tr('이 파일에 맞는 자막 우선 (영상 해시)'), style: TextStyle(fontSize: 12)),
|
||||
label: tr('이 파일에 맞는 자막 우선 (영상 해시)'),
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
]),
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import '../l10n/tr.dart';
|
|||
import 'encode_notice.dart';
|
||||
import 'theme.dart';
|
||||
import 'jj_header_row.dart';
|
||||
import 'jj_chip.dart';
|
||||
|
||||
/// 화면 · 색 보정 (파이널 컷의 뷰어 + 인스펙터처럼): 왼쪽은 지금 보고 있는 동영상의 한 장면 미리보기,
|
||||
/// 오른쪽은 화면 비율 · 맞추기 · 회전 · 색 보정. 바꾸면 바로 MKV 만들기 설정에 들어간다.
|
||||
|
|
@ -232,11 +233,10 @@ class _VideoAdjustState extends State<_VideoAdjust> {
|
|||
head(tr('화면 비율')),
|
||||
Wrap(spacing: 6, runSpacing: 6, children: [
|
||||
for (final f in FrameChoice.values)
|
||||
ChoiceChip(
|
||||
// 관리자 지시 (10/21, 28 찾음): 고르면 체크 표가 그림 위에 겹쳐 그려졌다 - 다른 고른 칩처럼 ✓ 만 (그림 자리에)
|
||||
avatar: Icon(s.frame == f ? Icons.check : frameIcons[f], size: 16),
|
||||
showCheckmark: false,
|
||||
label: Text(f.label),
|
||||
// 관리자 지시 (10/21, 28 찾음): 고르면 체크 표가 그림 위에 겹쳐 그려졌다 - 다른 고른 칩처럼 ✓ 만 (그림 자리에, JjChip)
|
||||
JjChip(
|
||||
icon: frameIcons[f],
|
||||
label: f.label,
|
||||
selected: s.frame == f,
|
||||
onSelected: (_) => _set(s.copyWith(frame: f)),
|
||||
),
|
||||
|
|
@ -244,8 +244,8 @@ class _VideoAdjustState extends State<_VideoAdjust> {
|
|||
head(tr('맞추기 (비율이 다를 때)')),
|
||||
Wrap(spacing: 6, runSpacing: 6, children: [
|
||||
for (final f in FitChoice.values)
|
||||
ChoiceChip(
|
||||
label: Text(f.label),
|
||||
JjChip(
|
||||
label: f.label,
|
||||
selected: s.fit == f,
|
||||
onSelected: s.frame == FrameChoice.original ? null : (_) => _set(s.copyWith(fit: f)),
|
||||
),
|
||||
|
|
@ -253,8 +253,8 @@ class _VideoAdjustState extends State<_VideoAdjust> {
|
|||
head(tr('회전')),
|
||||
Wrap(spacing: 6, runSpacing: 6, children: [
|
||||
for (final r in RotateChoice.values)
|
||||
ChoiceChip(
|
||||
label: Text(r.label),
|
||||
JjChip(
|
||||
label: r.label,
|
||||
selected: s.rotate == r,
|
||||
onSelected: (_) => _set(s.copyWith(rotate: r)),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jj_mkvmaker/core/picked_list.dart';
|
||||
import 'package:jj_mkvmaker/ui/jj_chip.dart';
|
||||
|
||||
/// W 공용 위젯 약속 (JjChip · withPicked): 아이콘 칩은 고르면 그 자리에 ✓ 만 (그림 위에 체크가 겹치지 않음 - 62ba78d) ·
|
||||
/// 아이콘 없는 칩은 Material 처음 꼴 · multi 는 켜고 끄기. 위젯 하나에 대해 한 번만 둔다 (설계 7-2)
|
||||
void main() {
|
||||
Future<void> show(WidgetTester t, Widget chip) =>
|
||||
t.pumpWidget(MaterialApp(home: Scaffold(body: Center(child: chip))));
|
||||
|
||||
testWidgets('아이콘 칩: 고르지 않으면 그림, 고르면 그 자리에 ✓ 만 (Material 체크 표는 끔)', (t) async {
|
||||
await show(t, JjChip(label: '세로 9:16', icon: Icons.crop_portrait, selected: false, onSelected: (_) {}));
|
||||
expect(find.byIcon(Icons.crop_portrait), findsOneWidget);
|
||||
expect(t.widget<ChoiceChip>(find.byType(ChoiceChip)).showCheckmark, isFalse);
|
||||
await show(t, JjChip(label: '세로 9:16', icon: Icons.crop_portrait, selected: true, onSelected: (_) {}));
|
||||
expect(find.byIcon(Icons.crop_portrait), findsNothing);
|
||||
expect(find.byIcon(Icons.check), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('아이콘 없는 칩: Material 처음 꼴 (체크 표 켬) · 하나 고르기는 ChoiceChip, 여럿은 FilterChip', (t) async {
|
||||
await show(t, JjChip(label: '큰 폴더', selected: true, onSelected: (_) {}));
|
||||
expect(t.widget<ChoiceChip>(find.byType(ChoiceChip)).showCheckmark, isTrue);
|
||||
bool? got;
|
||||
await show(t, JjChip(multi: true, label: 'mp4', selected: false, onSelected: (v) => got = v));
|
||||
expect(find.byType(FilterChip), findsOneWidget);
|
||||
await t.tap(find.text('mp4'));
|
||||
expect(got, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('누름 알림이 없으면 누를 수 없음', (t) async {
|
||||
await show(t, const JjChip(label: '채우기', selected: false, onSelected: null));
|
||||
expect(t.widget<ChoiceChip>(find.byType(ChoiceChip)).onSelected, isNull);
|
||||
});
|
||||
|
||||
test('withPicked: 켜면 끝에 하나 (이미 있으면 끝으로), 끄면 빼기, 원래 목록은 그대로', () {
|
||||
final list = ['jpg', 'png'];
|
||||
expect(withPicked(list, 'gif', true), ['jpg', 'png', 'gif']);
|
||||
expect(withPicked(list, 'jpg', true), ['png', 'jpg']);
|
||||
expect(withPicked(list, 'jpg', false), ['png']);
|
||||
expect(withPicked(list, 'bmp', false), ['jpg', 'png']);
|
||||
expect(list, ['jpg', 'png']);
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue