W FloatingAction: 떠 있는 단추 자리 판단을 FloatingActions 하나로 + 작업 기록 시계 주입 + 캡처 흔들림 고정
- lib/ui/floating_actions.dart: 좁거나 낮으면 (글자 배율로 나눈 높이 500 미만 - F7) 내용 안 단추 줄, 넓으면 오른쪽 아래에 떠 있고 내용 아래를 단추 높이만큼 비움 (206). MKV 자세히 [이동] 단추가 부르기만 한다 (예전에는 화면 안에 판단 · Stack 이 있었음). 보이는 것 그대로 - MessageText 점검: 문장 잇기는 이미 joinSentences (tr.dart) 하나 - 손으로 빈칸 잇는 곳 없음, 바꾸지 않음 - 주입: PlatformServices.clock → AppLog(now:) (처음 값 실제 시계 - 작업 기록의 [시:분:초]) - 시험: floating_actions_contract_test (떠 있음 / 내용 안 · 비울 높이 · 첫 단추 맨 아래 · floatingInline 4 경우) - 캡처 흔들림 (시험 쪽): mkvdetail (폴더 · 시각 · 기록 시각) · bigtext (JJ_SHOT_TMP 아래 · 드라이브 남은 공간 shotDevice). b3_52 의 받는 중 파일 (.jjpart) 시각은 찍기 전에 고정해 보았으나 복사가 아직 쓰는 중이라 목록이 읽은 시각이 남아 그대로 (까닭을 시험에 적음) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>modular
parent
5a65dbf693
commit
1b09030b80
|
|
@ -28,6 +28,7 @@ PlatformServices shotServices() {
|
||||||
createUpscaler: r.createUpscaler,
|
createUpscaler: r.createUpscaler,
|
||||||
findLanSmb: r.findLanSmb,
|
findLanSmb: r.findLanSmb,
|
||||||
diskSpace: (_) async => (107374182400, 536870912000),
|
diskSpace: (_) async => (107374182400, 536870912000),
|
||||||
|
clock: r.clock,
|
||||||
updater: r.updater,
|
updater: r.updater,
|
||||||
usage: r.usage == null ? null : const FixedUsage(),
|
usage: r.usage == null ? null : const FixedUsage(),
|
||||||
models: r.models,
|
models: r.models,
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,9 @@ void main() {
|
||||||
testWidgets('52: 복사 중에 다른 화면에 갔다 와도 진행 막대', (t) async {
|
testWidgets('52: 복사 중에 다른 화면에 갔다 와도 진행 막대', (t) async {
|
||||||
// 다른 화면에 갔다 와도 아직 복사 중이게 (약 25초)
|
// 다른 화면에 갔다 와도 아직 복사 중이게 (약 25초)
|
||||||
File(p.join(left, 'big.bin')).writeAsBytesSync(List.filled(2 * 1024 * 1024, 7));
|
File(p.join(left, 'big.bin')).writeAsBytesSync(List.filled(2 * 1024 * 1024, 7));
|
||||||
pinTestTimes(tmp); // 오른쪽에 생기는 받는 중 파일 (.jjpart) 의 시각은 실제 복사가 만든 때라 그대로 둔다
|
pinTestTimes(tmp);
|
||||||
|
// 받는 중 파일 (.jjpart) 의 시각은 그대로 둔다: 관리자 지시 (10/25) 로 찍기 바로 전에 고정해 보았으나, 복사가 (80 KB/s 로)
|
||||||
|
// 아직 쓰는 중이라 목록이 읽은 시각이 쓴 때 (지금) 로 남아 2장 (52_back_progress · 52_done_refreshed) 은 실행한 분에 따라 다르다
|
||||||
c.settings.copy.bandwidthKBps = 80;
|
c.settings.copy.bandwidthKBps = 80;
|
||||||
await show(t, ExplorerPage(device: device, c: c));
|
await show(t, ExplorerPage(device: device, c: c));
|
||||||
await selectAndPress(t, 'big.bin', '복사');
|
await selectAndPress(t, 'big.bin', '복사');
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import 'package:jj_mkvmaker/ui/settings_page.dart';
|
||||||
import 'package:jj_mkvmaker/ui/theme.dart';
|
import 'package:jj_mkvmaker/ui/theme.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
import 'support/shot_services.dart';
|
||||||
import 'support/temp_dir.dart';
|
import 'support/temp_dir.dart';
|
||||||
|
|
||||||
/// 253 · 255 · 256 (관리자 지시 10/21): test/large_text_test 의 화면들을 실제 글꼴로 - 글자 1.6배 (412×900 · 900×412 · 1280×800) 와
|
/// 253 · 255 · 256 (관리자 지시 10/21): test/large_text_test 의 화면들을 실제 글꼴로 - 글자 1.6배 (412×900 · 900×412 · 1280×800) 와
|
||||||
|
|
@ -32,8 +33,8 @@ void main() {
|
||||||
final out = Platform.environment['JJ_SHOT_DIR'] ?? '';
|
final out = Platform.environment['JJ_SHOT_DIR'] ?? '';
|
||||||
final pages = <String, Widget Function(AppController, DownloadManager)>{
|
final pages = <String, Widget Function(AppController, DownloadManager)>{
|
||||||
'mkv': (c, d) => HomePage(c: c),
|
'mkv': (c, d) => HomePage(c: c),
|
||||||
'explorer': (c, d) => ExplorerPage(c: c),
|
'explorer': (c, d) => ExplorerPage(c: c, device: shotDevice()),
|
||||||
'rsync': (c, d) => ExplorerPage(c: c, rsync: true),
|
'rsync': (c, d) => ExplorerPage(c: c, rsync: true, device: shotDevice()),
|
||||||
'monitor': (c, d) => MonitorPage(c: c),
|
'monitor': (c, d) => MonitorPage(c: c),
|
||||||
'downloads': (c, d) => DownloadsPage(d: d),
|
'downloads': (c, d) => DownloadsPage(d: d),
|
||||||
'settings': (c, d) => SettingsPage(c: c),
|
'settings': (c, d) => SettingsPage(c: c),
|
||||||
|
|
@ -53,7 +54,9 @@ void main() {
|
||||||
await t.runAsync(() => i18n.apply(lang, save: false));
|
await t.runAsync(() => i18n.apply(lang, save: false));
|
||||||
addTearDown(() => i18n.apply('ko', save: false));
|
addTearDown(() => i18n.apply('ko', save: false));
|
||||||
// 관리자 지시 (10/24): 폴더 이름 · 기록 시각을 실행마다 같게 (캡처가 흔들리지 않게 - 시험 쪽에서만)
|
// 관리자 지시 (10/24): 폴더 이름 · 기록 시각을 실행마다 같게 (캡처가 흔들리지 않게 - 시험 쪽에서만)
|
||||||
final tmp = testFixedTempDir('jj_bigtext_fixed');
|
// 트리에 둘레 폴더도 보여 JJ_SHOT_TMP (캡처용으로 따로 둔 폴더) 아래에 둔다
|
||||||
|
final base = Platform.environment['JJ_SHOT_TMP'] ?? '';
|
||||||
|
final tmp = testFixedTempDir('jj_bigtext_fixed', base: base);
|
||||||
appTempInto(tmp);
|
appTempInto(tmp);
|
||||||
final video = p.join(tmp.path, '아주 긴 이름의 동영상 파일 - 시험용 2026.mp4');
|
final video = p.join(tmp.path, '아주 긴 이름의 동영상 파일 - 시험용 2026.mp4');
|
||||||
File(video).writeAsStringSync('v');
|
File(video).writeAsStringSync('v');
|
||||||
|
|
@ -73,7 +76,7 @@ void main() {
|
||||||
File(f).writeAsStringSync('v');
|
File(f).writeAsStringSync('v');
|
||||||
c.videos.add(VideoItem(f));
|
c.videos.add(VideoItem(f));
|
||||||
}
|
}
|
||||||
pinTestTimes(tmp);
|
pinTestTimes(base.isEmpty ? tmp : Directory(base));
|
||||||
for (var i = 1; i <= 3; i++) {
|
for (var i = 1; i <= 3; i++) {
|
||||||
c.note('시험 기록 $i', at: DateTime(2026, 10, 22, 9, i));
|
c.note('시험 기록 $i', at: DateTime(2026, 10, 22, 9, i));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,12 +61,15 @@ void main() {
|
||||||
addTearDown(t.view.reset);
|
addTearDown(t.view.reset);
|
||||||
await t.runAsync(() => i18n.apply(lang, save: false));
|
await t.runAsync(() => i18n.apply(lang, save: false));
|
||||||
addTearDown(() => i18n.apply('ko', save: false));
|
addTearDown(() => i18n.apply('ko', save: false));
|
||||||
final tmp = testTempDir('jj_mkvdetail_');
|
// 관리자 지시 (10/24): 폴더 이름 · 시각을 실행마다 같게 (캡처가 흔들리지 않게 - 시험 쪽에서만)
|
||||||
|
final tmp = testFixedTempDir('jj_mkvdetail_fixed');
|
||||||
appTempInto(tmp);
|
appTempInto(tmp);
|
||||||
final file = (File(p.join(tmp.path, '[시험] 아주 긴 이름의 동영상 - 1화 고화질.mp4'))..writeAsBytesSync([0])).path;
|
final file = (File(p.join(tmp.path, '[시험] 아주 긴 이름의 동영상 - 1화 고화질.mp4'))..writeAsBytesSync([0])).path;
|
||||||
// 같은 폴더의 외부 자막 (문자셋 고르기까지 든 줄 - F6)
|
// 같은 폴더의 외부 자막 (문자셋 고르기까지 든 줄 - F6)
|
||||||
File(p.join(tmp.path, '[시험] 아주 긴 이름의 동영상 - 1화 고화질.ko.srt')).writeAsStringSync('1\n00:00:01,000 --> 00:00:02,000\n안녕\n');
|
File(p.join(tmp.path, '[시험] 아주 긴 이름의 동영상 - 1화 고화질.ko.srt')).writeAsStringSync('1\n00:00:01,000 --> 00:00:02,000\n안녕\n');
|
||||||
final c = AppController(PlatformServices(mediaTool: _Tool(), storage: DesktopStorageService()));
|
pinTestTimes(tmp);
|
||||||
|
// 작업 기록의 [시:분:초] 도 정한 시각으로 (시험 쪽에서만)
|
||||||
|
final c = AppController(PlatformServices(mediaTool: _Tool(), storage: DesktopStorageService(), clock: () => DateTime(2026, 10, 22, 9)));
|
||||||
await t.runAsync(() async {
|
await t.runAsync(() async {
|
||||||
await c.init();
|
await c.init();
|
||||||
await c.addVideos([file]);
|
await c.addVideos([file]);
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ class AppController extends ChangeNotifier {
|
||||||
|
|
||||||
// ───────── 모듈 ─────────
|
// ───────── 모듈 ─────────
|
||||||
|
|
||||||
final _logBook = AppLog();
|
late final _logBook = AppLog(now: services.clock);
|
||||||
late final _jobs = JobQueue(onChanged: notifyListeners, log: _log);
|
late final _jobs = JobQueue(onChanged: notifyListeners, log: _log);
|
||||||
late final _files = SubtitleFiles(services.storage, _tool);
|
late final _files = SubtitleFiles(services.storage, _tool);
|
||||||
late final _probe = VideoProbe(storage: services.storage, tool: _tool, files: _files, log: _log, fileAccess: () => fileAccess);
|
late final _probe = VideoProbe(storage: services.storage, tool: _tool, files: _files, log: _log, fileAccess: () => fileAccess);
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,17 @@ import 'dart:io';
|
||||||
class AppLog {
|
class AppLog {
|
||||||
final List<String> lines = [];
|
final List<String> lines = [];
|
||||||
|
|
||||||
|
/// 지금 시각 (처음 값 = 실제 시계 - PlatformServices.clock 이 있으면 그것)
|
||||||
|
final DateTime Function() now;
|
||||||
|
AppLog({DateTime Function()? now}) : now = now ?? DateTime.now;
|
||||||
|
|
||||||
/// 작업 기록을 파일에도 남길 곳 (설정 폴더의 app.log)
|
/// 작업 기록을 파일에도 남길 곳 (설정 폴더의 app.log)
|
||||||
String? file;
|
String? file;
|
||||||
|
|
||||||
static const maxLines = 500, maxFileBytes = 2 * 1024 * 1024;
|
static const maxLines = 500, maxFileBytes = 2 * 1024 * 1024;
|
||||||
|
|
||||||
void add(String msg, {DateTime? at}) {
|
void add(String msg, {DateTime? at}) {
|
||||||
final t = at ?? DateTime.now();
|
final t = at ?? now();
|
||||||
String two(int n) => n.toString().padLeft(2, '0');
|
String two(int n) => n.toString().padLeft(2, '0');
|
||||||
lines.add('[${two(t.hour)}:${two(t.minute)}:${two(t.second)}] $msg');
|
lines.add('[${two(t.hour)}:${two(t.minute)}:${two(t.second)}] $msg');
|
||||||
if (lines.length > maxLines) lines.removeAt(0);
|
if (lines.length > maxLines) lines.removeAt(0);
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,9 @@ class PlatformServices {
|
||||||
/// 디스크 남은 · 전체 바이트 재기 (없으면 실제 디스크 - copy_center 의 diskSpace). 작업 현황의 "대상 디스크" 줄
|
/// 디스크 남은 · 전체 바이트 재기 (없으면 실제 디스크 - copy_center 의 diskSpace). 작업 현황의 "대상 디스크" 줄
|
||||||
final Future<(int, int)?> Function(String path)? diskSpace;
|
final Future<(int, int)?> Function(String path)? diskSpace;
|
||||||
|
|
||||||
|
/// 지금 시각 (없으면 실제 시계). 작업 기록의 [시:분:초]
|
||||||
|
final DateTime Function()? clock;
|
||||||
|
|
||||||
/// 이 기기 (Android · Windows · 메모리) - 기기마다 다르게 도는 곳은 이것을 본다 (M1, 처음 값 = 이 기기)
|
/// 이 기기 (Android · Windows · 메모리) - 기기마다 다르게 도는 곳은 이것을 본다 (M1, 처음 값 = 이 기기)
|
||||||
final PlatformInfo platform;
|
final PlatformInfo platform;
|
||||||
|
|
||||||
|
|
@ -93,6 +96,7 @@ class PlatformServices {
|
||||||
this.createUpscaler,
|
this.createUpscaler,
|
||||||
this.findLanSmb,
|
this.findLanSmb,
|
||||||
this.diskSpace,
|
this.diskSpace,
|
||||||
|
this.clock,
|
||||||
this.updater,
|
this.updater,
|
||||||
this.usage,
|
this.usage,
|
||||||
ModelStore? models,
|
ModelStore? models,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
/// 약속 (W 공용 위젯, 설계 7-2 FloatingAction - 떠 있는 단추 묶음): 단추들 · 내용 → 단추가 내용을 가리지 않는 자리.
|
||||||
|
/// 입력: [buttons] (첫 단추가 맨 아래 - 예전 [이동] 자리, 다음은 그 위로) · [content] (안에 넣을 단추 줄 · 아래에 비울 높이를 받아 그림) ·
|
||||||
|
/// [inline] (true 면 떠 있지 않고 내용 안 - 좁은 화면 · 낮은 화면 등, [floatingInline] 이 정해 준다).
|
||||||
|
/// 출력: inline 이면 내용 끝에 오른쪽으로 줄바꿈하는 단추 줄 (아래 여백 24), 아니면 오른쪽 아래에 떠 있고 내용 아래를 단추 높이만큼 비움
|
||||||
|
/// (끝까지 내리면 가리지 않게). 오류 없음. 쓰는 곳: MKV 자세히 [이동] 단추 (206 · F7). 화면은 부르기만 한다
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// 단추 하나의 높이 (FloatingActionButton.extended 56 + 사이 10)
|
||||||
|
const floatingActionRow = 66.0;
|
||||||
|
|
||||||
|
/// 떠 있으면 가리는 화면: 좁거나 ([compact]) 높이가 낮을 때 (글자 배율로 나눠 본다 - 글자가 크면 같은 높이에 들어가는 줄이 적다, F7)
|
||||||
|
bool floatingInline(BuildContext context, {required bool compact, double minHeight = 500}) =>
|
||||||
|
compact || MediaQuery.sizeOf(context).height < minHeight * MediaQuery.textScalerOf(context).scale(1);
|
||||||
|
|
||||||
|
class FloatingActions extends StatelessWidget {
|
||||||
|
const FloatingActions({super.key, required this.buttons, required this.content, required this.inline});
|
||||||
|
|
||||||
|
final List<Widget> buttons;
|
||||||
|
final Widget Function(Widget? inlineRow, double bottomGap) content;
|
||||||
|
final bool inline;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (inline) {
|
||||||
|
final row = Wrap(alignment: WrapAlignment.end, spacing: 10, runSpacing: 10, children: buttons);
|
||||||
|
return content(row, 24);
|
||||||
|
}
|
||||||
|
return Stack(children: [
|
||||||
|
content(null, 16 + buttons.length * floatingActionRow + 16),
|
||||||
|
Positioned(
|
||||||
|
right: 16,
|
||||||
|
bottom: 16,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
for (var i = buttons.length - 1; i >= 0; i--) ...[
|
||||||
|
buttons[i],
|
||||||
|
if (i > 0) const SizedBox(height: 10),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,6 +17,7 @@ import '../../l10n/tr.dart';
|
||||||
import 'move_to_target.dart';
|
import 'move_to_target.dart';
|
||||||
import 'subtitle_row.dart';
|
import 'subtitle_row.dart';
|
||||||
import '../notice_band.dart';
|
import '../notice_band.dart';
|
||||||
|
import '../floating_actions.dart';
|
||||||
|
|
||||||
class VideoDetailPane extends StatelessWidget {
|
class VideoDetailPane extends StatelessWidget {
|
||||||
final AppController c;
|
final AppController c;
|
||||||
|
|
@ -52,35 +53,15 @@ class VideoDetailPane extends StatelessWidget {
|
||||||
);
|
);
|
||||||
// 206: 좁은 화면 (폰 세로) 은 자세히 칸이 낮아 떠 있는 버튼이 칩 (음성 n개 등) 을 가렸다 - 버튼을 칩 줄 아래에 놓는다
|
// 206: 좁은 화면 (폰 세로) 은 자세히 칸이 낮아 떠 있는 버튼이 칩 (음성 n개 등) 을 가렸다 - 버튼을 칩 줄 아래에 놓는다
|
||||||
// F7 (관리자 승인 10/21, 15 찾음): 높이가 낮은 화면 (폰 가로 등) 도 - 떠 있는 버튼이 [자막 파일 추가] 를 반쯤 가렸다.
|
// F7 (관리자 승인 10/21, 15 찾음): 높이가 낮은 화면 (폰 가로 등) 도 - 떠 있는 버튼이 [자막 파일 추가] 를 반쯤 가렸다.
|
||||||
// 높이는 글자 배율로 나눠 본다 (글자가 크면 같은 높이에 들어가는 줄이 적다 - 960×600 · 글자 1.6 에서도 가렸다)
|
// 자리 판단 · 그리기는 공용 FloatingActions (넓은 화면은 떠 있는 버튼 높이만큼 목록 아래를 비운다)
|
||||||
if (isCompact(context) || MediaQuery.sizeOf(context).height < 500 * MediaQuery.textScalerOf(context).scale(1)) {
|
return FloatingActions(
|
||||||
final row = Wrap(alignment: WrapAlignment.end, spacing: 10, runSpacing: 10, children: [
|
inline: floatingInline(context, compact: isCompact(context)),
|
||||||
|
buttons: [
|
||||||
if (buttons.isEmpty) button(null),
|
if (buttons.isEmpty) button(null),
|
||||||
for (final t in buttons) button(t),
|
for (final t in buttons) button(t),
|
||||||
]);
|
],
|
||||||
return _list(context, info, video, audioCount, locked, moveButtons: row, bottom: 24);
|
content: (row, bottom) => _list(context, info, video, audioCount, locked, moveButtons: row, bottom: bottom),
|
||||||
}
|
);
|
||||||
// 넓은 화면: 떠 있는 버튼 높이만큼 목록 아래를 비워 끝까지 내리면 가리지 않게 (버튼이 여러 개면 그만큼)
|
|
||||||
final stacked = buttons.isEmpty ? 1 : buttons.length;
|
|
||||||
return Stack(children: [
|
|
||||||
_list(context, info, video, audioCount, locked, bottom: 16 + stacked * 66.0 + 16),
|
|
||||||
Positioned(
|
|
||||||
right: 16,
|
|
||||||
bottom: 16,
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
if (buttons.isEmpty) button(null),
|
|
||||||
// 첫 번째 버튼이 맨 아래 (예전 [이동] 자리), 다음 버튼은 그 위로
|
|
||||||
for (var i = buttons.length - 1; i >= 0; i--) ...[
|
|
||||||
button(buttons[i]),
|
|
||||||
if (i > 0) const SizedBox(height: 10),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _list(BuildContext context, MediaInfo? info, StreamInfo? video, int audioCount, bool locked,
|
Widget _list(BuildContext context, MediaInfo? info, StreamInfo? video, int audioCount, bool locked,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:jj_mkvmaker/ui/floating_actions.dart';
|
||||||
|
|
||||||
|
/// W 공용 위젯 약속 (FloatingActions): 좁거나 낮은 화면은 내용 안 단추 줄 (떠 있지 않음), 넓으면 오른쪽 아래에 떠 있고
|
||||||
|
/// 내용 아래를 단추 높이만큼 비움 · 첫 단추가 맨 아래. 위젯 하나에 대해 한 번만 둔다 (설계 7-2)
|
||||||
|
void main() {
|
||||||
|
Widget fab(String n) => FloatingActionButton.extended(heroTag: null, onPressed: () {}, icon: const Icon(Icons.drive_file_move_outline), label: Text(n));
|
||||||
|
|
||||||
|
Future<(Widget?, double)> show(WidgetTester t, {required bool inline, Size size = const Size(960, 600), double scale = 1}) async {
|
||||||
|
t.view.physicalSize = size;
|
||||||
|
t.view.devicePixelRatio = 1;
|
||||||
|
addTearDown(t.view.reset);
|
||||||
|
Widget? gotRow;
|
||||||
|
var gotGap = -1.0;
|
||||||
|
await t.pumpWidget(MaterialApp(
|
||||||
|
home: MediaQuery(
|
||||||
|
data: MediaQueryData(size: size, textScaler: TextScaler.linear(scale)),
|
||||||
|
child: Scaffold(
|
||||||
|
body: FloatingActions(
|
||||||
|
inline: inline,
|
||||||
|
buttons: [fab('이동'), fab('NAS')],
|
||||||
|
content: (row, gap) {
|
||||||
|
gotRow = row;
|
||||||
|
gotGap = gap;
|
||||||
|
return ListView(children: [const SizedBox(height: 100, child: Text('내용')), ?row]);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
return (gotRow, gotGap);
|
||||||
|
}
|
||||||
|
|
||||||
|
testWidgets('넓으면 떠 있음: 내용 아래를 단추 높이만큼 비우고, 첫 단추가 맨 아래', (t) async {
|
||||||
|
final (row, gap) = await show(t, inline: false);
|
||||||
|
expect(row, isNull);
|
||||||
|
expect(gap, 16 + 2 * floatingActionRow + 16);
|
||||||
|
expect(t.getRect(find.text('이동')).top, greaterThan(t.getRect(find.text('NAS')).top), reason: '첫 단추가 아래');
|
||||||
|
expect(t.getRect(find.text('이동')).bottom, greaterThan(600 - 60), reason: '오른쪽 아래');
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('좁거나 낮으면 내용 안 단추 줄 (떠 있지 않음) · 아래 여백 24', (t) async {
|
||||||
|
final (row, gap) = await show(t, inline: true, size: const Size(412, 900));
|
||||||
|
expect(row, isNotNull);
|
||||||
|
expect(gap, 24);
|
||||||
|
expect(find.byType(Positioned), findsNothing);
|
||||||
|
expect(t.takeException(), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('floatingInline: 좁음 · 높이 500 미만 · 글자 배율로 나눈 높이 (F7 - 960×600 · 1.6 배)', (t) async {
|
||||||
|
late BuildContext ctx;
|
||||||
|
for (final (size, scale, compact, want) in [
|
||||||
|
(const Size(960, 800), 1.0, false, false),
|
||||||
|
(const Size(960, 800), 1.0, true, true),
|
||||||
|
(const Size(900, 412), 1.0, false, true),
|
||||||
|
(const Size(960, 600), 1.6, false, true),
|
||||||
|
]) {
|
||||||
|
await t.pumpWidget(MediaQuery(
|
||||||
|
data: MediaQueryData(size: size, textScaler: TextScaler.linear(scale)),
|
||||||
|
child: Builder(builder: (c) {
|
||||||
|
ctx = c;
|
||||||
|
return const SizedBox();
|
||||||
|
})));
|
||||||
|
expect(floatingInline(ctx, compact: compact), want, reason: '$size × $scale compact=$compact');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue