diff --git a/assets/l10n/en.json b/assets/l10n/en.json index d0b6747..0dfb35d 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -639,6 +639,7 @@ "가져온 즐겨찾기 ({0})": "Imported bookmarks ({0})", "가져올 즐겨찾기가 없습니다.": "No bookmarks to import.", "간격": "Step", + "갈 곳: {0}": "To: {0}", "감지한 언어: {0} (자동 감지)": "Detected language: {0} (auto-detect)", "감지한 언어: {0} - 틀렸으면 다른 언어로 다시 만드세요": "Detected language: {0} - if it is wrong, make them again with another language", "감지한 원어가 만들 언어와 다를 때만 내려받기 {0}": "Downloaded only if the detected language differs from the subtitle language {0}", @@ -737,6 +738,7 @@ "공용 폴더에 받은 파일 (동영상 · 그림 등)": "Files downloaded to shared folders (videos, images, etc.)", "공유": "Share", "공유 (다른 앱으로 보내기)": "Share (send to another app)", + "공유 안의 폴더 · 비우면 공유의 맨 위": "A folder inside the share · empty = top of the share", "공유 이름": "Share name", "공유 중": "Sharing", "공유 창을 열 수 없습니다.": "Can't open the share sheet.", @@ -852,6 +854,7 @@ "나중에": "Later", "날짜": "Date", "날짜: 바뀐 때": "Date: modified", + "날짜: 찍은 때": "Date: taken", "날짜: 찍은 때 (사진, 없으면 바뀐 때)": "Date: taken (photos; otherwise modified)", "남는 것": "What stays", "남은 {0}장": "{0} left", @@ -1033,6 +1036,7 @@ "덮어쓴 파일은 되돌릴 수 없습니다.": "Overwritten files can't be restored.", "덮어씁니다": "overwrites it", "덴마크어": "Danish", + "도메인 · 작업 그룹": "Domain / workgroup", "도메인 · 작업 그룹 (보통 비움)": "Domain / workgroup (usually empty)", "독일어": "German", "돌아가기": "Go back", @@ -1295,6 +1299,7 @@ "받은 AI 모델 ({0}) 을 Download/JJ_MKVMaker 에 복사해 두었다가 다시 설치한 뒤 되살리기 (잠시 {0} 더 필요)": "Copy downloaded AI models ({0}) to Download/JJ_MKVMaker and restore them after reinstalling (needs {0} more space for a while)", "받은 AI 모델 (그림 · 자막) - 다시 받아야 합니다 (약 {0})": "Downloaded AI models (images · subtitles) - must be downloaded again (about {0})", "받은 AI 자막 모델": "Downloaded AI subtitle models", + "받은 곳에서 알려 준 해시를 붙여넣으세요": "Paste the hash published by the source", "받은 동영상을 편집 목록에 추가: {0}": "Added the received video to the edit list: {0}", "받은 업데이트 · 필수 프로그램 · Chrome 엔진을 설치하고 남은 임시 파일": "Temporary files left from installing updates · required programs · the Chrome engine", "받은 자막을 한국어로 번역해 함께 추가 (한국어 자막이 없을 때)": "Translate received subtitles to Korean and add them too (when there are no Korean subtitles)", @@ -1362,6 +1367,7 @@ "보조 DNS (비워 둘 수 있음)": "Secondary DNS (may be empty)", "보통": "Normal", "보통 (0.25, 기본)": "Normal (0.25, default)", + "보통 비워 둡니다": "Usually left empty", "보통: {0}": "Normal: {0}", "복사": "Copy", "복사 (붙여넣기로)": "Copy (to paste)", @@ -1412,6 +1418,7 @@ "비공개 · 회원 전용 영상입니다.": "This is a private · members-only video.", "비교하지 못했습니다: {0}": "Couldn't compare: {0}", "비교할 값 붙여넣기 (받은 곳의 해시)": "Paste a value to compare (hash from the source)", + "비교할 해시": "Hash to compare", "비밀번호": "Password", "비밀번호 (개인 키만 쓰면 비움)": "Password (leave empty if using only a private key)", "비밀번호 (선택)": "Password (optional)", @@ -1440,7 +1447,9 @@ "비어 있습니다": "Empty", "비어 있음": "Empty", "비우기": "Empty", + "비우면 로그인한 곳 · 예: /volume1/video · 탭의 맨 위가 됩니다": "Empty = where you log in · e.g. /volume1/video · becomes the top of the tab", "비우면 지움": "Empty = remove", + "비우면 홈 폴더 · 예: /volume1/video · 탭의 맨 위가 됩니다": "Empty = home folder · e.g. /volume1/video · becomes the top of the tab", "빈 이름": "Empty name", "빈 폴더 {0}개 지움": "Deleted {0} empty folders", "빈 폴더 그대로": "Leave empty folders", @@ -1615,6 +1624,7 @@ "시작 · 웹 브라우저": "Startup · web browser", "시작 메뉴에 등록": "Add to Start menu", "시작 안 함 (모니터링에서 시작)": "Don't start (start from Monitor)", + "시작 폴더": "Start folder", "시작 폴더 (공유 안, 비우면 맨 위)": "Start folder (inside the share, top if empty)", "시작 폴더 (비우면 로그인한 곳)": "Start folder (empty = where you log in)", "시작 폴더 (비우면 홈 폴더)": "Start folder (home folder if empty)", @@ -2532,6 +2542,7 @@ "탐색기에서 동영상을 열 때": "When opening a video from Explorer", "탐색기에서 연 동영상을 재생할 창": "Window to play videos opened from Explorer", "태국어": "Thai", + "탭에 보입니다 · 비우면 주소": "Shown on the tab · address if empty", "터키어": "Turkish", "토": "Sat", "토렌트 · 마그넷 다운로드": "Torrent · magnet download", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index c66e2be..aa5a7a3 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -639,6 +639,7 @@ "가져온 즐겨찾기 ({0})": "取り込んだブックマーク({0})", "가져올 즐겨찾기가 없습니다.": "取り込むブックマークがありません。", "간격": "間隔", + "갈 곳: {0}": "移動先: {0}", "감지한 언어: {0} (자동 감지)": "検出した言語: {0} (自動検出)", "감지한 언어: {0} - 틀렸으면 다른 언어로 다시 만드세요": "検出した言語: {0} - 違っていれば別の言語で作り直してください", "감지한 원어가 만들 언어와 다를 때만 내려받기 {0}": "検出した言語が作成する言語と異なる場合のみダウンロード {0}", @@ -737,6 +738,7 @@ "공용 폴더에 받은 파일 (동영상 · 그림 등)": "共有フォルダーにダウンロードしたファイル (動画・画像など)", "공유": "共有", "공유 (다른 앱으로 보내기)": "共有 (他のアプリに送る)", + "공유 안의 폴더 · 비우면 공유의 맨 위": "共有内のフォルダー · 空なら共有の一番上", "공유 이름": "共有名", "공유 중": "共有中", "공유 창을 열 수 없습니다.": "共有画面を開けません。", @@ -852,6 +854,7 @@ "나중에": "後で", "날짜": "日付", "날짜: 바뀐 때": "日付: 更新日時", + "날짜: 찍은 때": "日付: 撮影日時", "날짜: 찍은 때 (사진, 없으면 바뀐 때)": "日付: 撮影日時 (写真、なければ更新日時)", "남는 것": "残るもの", "남은 {0}장": "残り {0} 枚", @@ -1033,6 +1036,7 @@ "덮어쓴 파일은 되돌릴 수 없습니다.": "上書きしたファイルは元に戻せません。", "덮어씁니다": "上書きします", "덴마크어": "デンマーク語", + "도메인 · 작업 그룹": "ドメイン・ワークグループ", "도메인 · 작업 그룹 (보통 비움)": "ドメイン・ワークグループ (通常は空欄)", "독일어": "ドイツ語", "돌아가기": "戻る", @@ -1295,6 +1299,7 @@ "받은 AI 모델 ({0}) 을 Download/JJ_MKVMaker 에 복사해 두었다가 다시 설치한 뒤 되살리기 (잠시 {0} 더 필요)": "ダウンロードした AI モデル ({0}) を Download/JJ_MKVMaker にコピーし、再インストール後に復元する (一時的に {0} 余分に必要)", "받은 AI 모델 (그림 · 자막) - 다시 받아야 합니다 (약 {0})": "ダウンロードした AI モデル (画像・字幕) - 再ダウンロードが必要です (約 {0})", "받은 AI 자막 모델": "ダウンロード済みの AI 字幕モデル", + "받은 곳에서 알려 준 해시를 붙여넣으세요": "入手元が示したハッシュを貼り付けてください", "받은 동영상을 편집 목록에 추가: {0}": "受け取った動画を編集リストに追加: {0}", "받은 업데이트 · 필수 프로그램 · Chrome 엔진을 설치하고 남은 임시 파일": "更新・必須プログラム・Chrome エンジンのインストールで残った一時ファイル", "받은 자막을 한국어로 번역해 함께 추가 (한국어 자막이 없을 때)": "受け取った字幕を韓国語に翻訳して一緒に追加(韓国語字幕がないとき)", @@ -1362,6 +1367,7 @@ "보조 DNS (비워 둘 수 있음)": "代替 DNS (空欄可)", "보통": "普通", "보통 (0.25, 기본)": "普通 (0.25、既定)", + "보통 비워 둡니다": "通常は空欄", "보통: {0}": "普通: {0}", "복사": "コピー", "복사 (붙여넣기로)": "コピー (貼り付け用)", @@ -1412,6 +1418,7 @@ "비공개 · 회원 전용 영상입니다.": "非公開・メンバー限定の動画です。", "비교하지 못했습니다: {0}": "比較できませんでした: {0}", "비교할 값 붙여넣기 (받은 곳의 해시)": "比較する値を貼り付け (入手元のハッシュ)", + "비교할 해시": "比較するハッシュ", "비밀번호": "パスワード", "비밀번호 (개인 키만 쓰면 비움)": "パスワード (秘密鍵だけを使う場合は空欄)", "비밀번호 (선택)": "パスワード(任意)", @@ -1440,7 +1447,9 @@ "비어 있습니다": "空です", "비어 있음": "空", "비우기": "空にする", + "비우면 로그인한 곳 · 예: /volume1/video · 탭의 맨 위가 됩니다": "空ならログインした場所 · 例: /volume1/video · タブの一番上になります", "비우면 지움": "空なら削除", + "비우면 홈 폴더 · 예: /volume1/video · 탭의 맨 위가 됩니다": "空ならホームフォルダー · 例: /volume1/video · タブの一番上になります", "빈 이름": "空の名前", "빈 폴더 {0}개 지움": "空フォルダー {0} 個を削除", "빈 폴더 그대로": "空フォルダーはそのまま", @@ -1615,6 +1624,7 @@ "시작 · 웹 브라우저": "起動・ウェブブラウザー", "시작 메뉴에 등록": "スタートメニューに登録", "시작 안 함 (모니터링에서 시작)": "開始しない (モニタリングで開始)", + "시작 폴더": "開始フォルダー", "시작 폴더 (공유 안, 비우면 맨 위)": "開始フォルダー (共有内、空欄なら最上位)", "시작 폴더 (비우면 로그인한 곳)": "開始フォルダー (空欄ならログインした場所)", "시작 폴더 (비우면 홈 폴더)": "開始フォルダー (空欄ならホームフォルダー)", @@ -2532,6 +2542,7 @@ "탐색기에서 동영상을 열 때": "エクスプローラーで動画を開いたとき", "탐색기에서 연 동영상을 재생할 창": "エクスプローラーで開いた動画を再生するウィンドウ", "태국어": "タイ語", + "탭에 보입니다 · 비우면 주소": "タブに表示 · 空ならアドレス", "터키어": "トルコ語", "토": "土", "토렌트 · 마그넷 다운로드": "トレント・マグネットのダウンロード", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index 69a2e5e..958b84e 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -639,6 +639,7 @@ "가져온 즐겨찾기 ({0})": "导入的书签({0})", "가져올 즐겨찾기가 없습니다.": "没有可导入的书签。", "간격": "间隔", + "갈 곳: {0}": "目标: {0}", "감지한 언어: {0} (자동 감지)": "检测到的语言:{0}(自动检测)", "감지한 언어: {0} - 틀렸으면 다른 언어로 다시 만드세요": "检测到的语言:{0} - 如有误,请用其他语言重新生成", "감지한 원어가 만들 언어와 다를 때만 내려받기 {0}": "仅当检测到的语言与要生成的语言不同时下载 {0}", @@ -737,6 +738,7 @@ "공용 폴더에 받은 파일 (동영상 · 그림 등)": "下载到公共文件夹的文件 (视频、图片等)", "공유": "分享", "공유 (다른 앱으로 보내기)": "分享 (发送到其他应用)", + "공유 안의 폴더 · 비우면 공유의 맨 위": "共享内的文件夹 · 为空则为共享的最上层", "공유 이름": "共享名称", "공유 중": "共享中", "공유 창을 열 수 없습니다.": "无法打开分享窗口。", @@ -852,6 +854,7 @@ "나중에": "以后再说", "날짜": "日期", "날짜: 바뀐 때": "日期: 修改时间", + "날짜: 찍은 때": "日期: 拍摄时间", "날짜: 찍은 때 (사진, 없으면 바뀐 때)": "日期: 拍摄时间 (照片; 否则为修改时间)", "남는 것": "保留的内容", "남은 {0}장": "剩余 {0} 张", @@ -1033,6 +1036,7 @@ "덮어쓴 파일은 되돌릴 수 없습니다.": "被覆盖的文件无法恢复。", "덮어씁니다": "覆盖", "덴마크어": "丹麦语", + "도메인 · 작업 그룹": "域 · 工作组", "도메인 · 작업 그룹 (보통 비움)": "域 · 工作组 (通常留空)", "독일어": "德语", "돌아가기": "返回", @@ -1295,6 +1299,7 @@ "받은 AI 모델 ({0}) 을 Download/JJ_MKVMaker 에 복사해 두었다가 다시 설치한 뒤 되살리기 (잠시 {0} 더 필요)": "将已下载的 AI 模型 ({0}) 复制到 Download/JJ_MKVMaker,重新安装后恢复 (暂时需要额外 {0})", "받은 AI 모델 (그림 · 자막) - 다시 받아야 합니다 (약 {0})": "已下载的 AI 模型 (图片 · 字幕) - 需要重新下载 (约 {0})", "받은 AI 자막 모델": "已下载的 AI 字幕模型", + "받은 곳에서 알려 준 해시를 붙여넣으세요": "粘贴来源提供的哈希", "받은 동영상을 편집 목록에 추가: {0}": "已将收到的视频加入编辑列表: {0}", "받은 업데이트 · 필수 프로그램 · Chrome 엔진을 설치하고 남은 임시 파일": "安装更新 · 必需程序 · Chrome 引擎后残留的临时文件", "받은 자막을 한국어로 번역해 함께 추가 (한국어 자막이 없을 때)": "将收到的字幕翻译成韩语并一并添加(没有韩语字幕时)", @@ -1362,6 +1367,7 @@ "보조 DNS (비워 둘 수 있음)": "备用 DNS (可留空)", "보통": "普通", "보통 (0.25, 기본)": "普通 (0.25,默认)", + "보통 비워 둡니다": "通常留空", "보통: {0}": "普通:{0}", "복사": "复制", "복사 (붙여넣기로)": "复制 (用于粘贴)", @@ -1412,6 +1418,7 @@ "비공개 · 회원 전용 영상입니다.": "这是私享 · 会员专享视频。", "비교하지 못했습니다: {0}": "无法比较: {0}", "비교할 값 붙여넣기 (받은 곳의 해시)": "粘贴要比较的值 (来源处的哈希)", + "비교할 해시": "要比较的哈希", "비밀번호": "密码", "비밀번호 (개인 키만 쓰면 비움)": "密码 (仅使用私钥时留空)", "비밀번호 (선택)": "密码(可选)", @@ -1440,7 +1447,9 @@ "비어 있습니다": "空", "비어 있음": "空", "비우기": "清空", + "비우면 로그인한 곳 · 예: /volume1/video · 탭의 맨 위가 됩니다": "为空则为登录位置 · 例: /volume1/video · 成为标签的最上层", "비우면 지움": "留空则删除", + "비우면 홈 폴더 · 예: /volume1/video · 탭의 맨 위가 됩니다": "为空则为主文件夹 · 例: /volume1/video · 成为标签的最上层", "빈 이름": "空名称", "빈 폴더 {0}개 지움": "已删除 {0} 个空文件夹", "빈 폴더 그대로": "保留空文件夹", @@ -1615,6 +1624,7 @@ "시작 · 웹 브라우저": "启动 · 网页浏览器", "시작 메뉴에 등록": "添加到开始菜单", "시작 안 함 (모니터링에서 시작)": "不开始 (在监控中开始)", + "시작 폴더": "起始文件夹", "시작 폴더 (공유 안, 비우면 맨 위)": "起始文件夹 (共享内,留空则为最上层)", "시작 폴더 (비우면 로그인한 곳)": "起始文件夹 (留空则为登录位置)", "시작 폴더 (비우면 홈 폴더)": "起始文件夹 (留空则为主目录)", @@ -2532,6 +2542,7 @@ "탐색기에서 동영상을 열 때": "从资源管理器打开视频时", "탐색기에서 연 동영상을 재생할 창": "播放从资源管理器打开的视频的窗口", "태국어": "泰语", + "탭에 보입니다 · 비우면 주소": "显示在标签上 · 为空则用地址", "터키어": "土耳其语", "토": "六", "토렌트 · 마그넷 다운로드": "种子 · 磁力链接下载", diff --git a/integration_test/windows_trunc_shot_test.dart b/integration_test/windows_trunc_shot_test.dart new file mode 100644 index 0000000..b58bb35 --- /dev/null +++ b/integration_test/windows_trunc_shot_test.dart @@ -0,0 +1,79 @@ +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:integration_test/integration_test.dart'; +import 'package:jj_mkvmaker/app/app_controller.dart'; +import 'package:jj_mkvmaker/app/i18n_controller.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/ftp_settings.dart'; +import 'package:jj_mkvmaker/ui/sftp_settings.dart'; +import 'package:jj_mkvmaker/ui/smb_settings.dart'; +import 'package:jj_mkvmaker/ui/theme.dart'; +import 'package:jj_mkvmaker/ui/webdav_settings.dart'; +import 'package:path/path.dart' as p; + +/// 관리자 지시 (10/18): 412 캡처의 잘림 고침 확인 - FTP · SFTP · SMB · WebDAV 서버 추가 창 (칸 이름은 짧게, 설명은 helper). +/// 크기 412 · 960, 언어 ko · en · ja. 실행: flutter test integration_test\windows_trunc_shot_test.dart -d windows +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + final out = Platform.environment['JJ_SHOT_DIR'] ?? p.join(Directory.systemTemp.path, 'jj_shots_trunc'); + for (final lang in ['ko', 'en', 'ja']) { + for (final (w, h) in [(412, 900), (960, 600)]) { + testWidgets('서버 추가 창 $lang ${w}x$h', (t) async { + t.view.physicalSize = Size(w.toDouble(), h.toDouble()); + t.view.devicePixelRatio = 1; + addTearDown(t.view.reset); + await t.runAsync(() => i18n.apply(lang, save: false)); + addTearDown(() => i18n.apply('ko', save: false)); + final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); + final navKey = GlobalKey(); + final shotKey = GlobalKey(); + await t.pumpWidget(RepaintBoundary( + key: shotKey, + child: MaterialApp( + theme: buildTheme(), debugShowCheckedModeBanner: false, navigatorKey: navKey, home: const Scaffold(body: SizedBox.expand())), + )); + Future settle([int n = 8]) async { + for (var i = 0; i < n; i++) { + await t.runAsync(() => Future.delayed(const Duration(milliseconds: 60))); + await t.pump(); + } + } + + Future shot(String name) async { + await settle(3); + final b = shotKey.currentContext!.findRenderObject()! as RenderRepaintBoundary; + final img = await t.runAsync(() => b.toImage()); + final bytes = await t.runAsync(() => img!.toByteData(format: ui.ImageByteFormat.png)); + Directory(out).createSync(recursive: true); + File(p.join(out, 'trunc_${name}_${lang}_$w.png')).writeAsBytesSync(bytes!.buffer.asUint8List()); + } + + for (final (name, open) in <(String, void Function(BuildContext))>[ + ('ftp', (ctx) => editFtpServer(ctx, c)), + ('sftp', (ctx) => editSftpServer(ctx, c)), + ('smb', (ctx) => editSmbServer(ctx, c)), + ('webdav', (ctx) => editDavServer(ctx, c)), + ]) { + open(navKey.currentContext!); + await settle(); + await shot(name); + // 창 아래쪽 (시작 폴더 칸) 도 + final scroll = find.descendant(of: find.byType(AlertDialog), matching: find.byType(Scrollable)); + if (scroll.evaluate().isNotEmpty) { + await t.drag(scroll.first, const Offset(0, -600)); + await settle(4); + await shot('${name}_bottom'); + } + navKey.currentState!.popUntil((r) => r.isFirst); + await settle(4); + } + }); + } + } +} diff --git a/lib/ui/batch_rename_dialog.dart b/lib/ui/batch_rename_dialog.dart index 7628042..6d7a0f3 100644 --- a/lib/ui/batch_rename_dialog.dart +++ b/lib/ui/batch_rename_dialog.dart @@ -202,6 +202,11 @@ class _BatchRenameDialogState extends State<_BatchRenameDialog> { child: DropdownButton( isExpanded: true, value: _dateSource, + // 412 캡처 (10/18): 고른 뒤 상자에는 짧게 (펼친 목록은 긴 설명 그대로) + selectedItemBuilder: (_) => [ + Text(tr('날짜: 찍은 때'), maxLines: 1, overflow: TextOverflow.ellipsis), + Text(tr('날짜: 바뀐 때'), maxLines: 1, overflow: TextOverflow.ellipsis), + ], items: [ DropdownMenuItem(value: 'photo', child: Text(tr('날짜: 찍은 때 (사진, 없으면 바뀐 때)'), overflow: TextOverflow.ellipsis)), DropdownMenuItem(value: 'modified', child: Text(tr('날짜: 바뀐 때'), overflow: TextOverflow.ellipsis)), diff --git a/lib/ui/explorer_page.dart b/lib/ui/explorer_page.dart index 3c0d04e..637ddad 100644 --- a/lib/ui/explorer_page.dart +++ b/lib/ui/explorer_page.dart @@ -2157,11 +2157,17 @@ class _ExplorerPageState extends State { child: Row(children: [ const Icon(Icons.arrow_forward, size: 18, color: JjColors.accent), const SizedBox(width: 6), + // 412 캡처 (10/18): "갈 곳:" 은 늘 보이고, 폴더 이름이 길면 가운데를 줄인다 (전체 경로는 길게 눌러 · 마우스를 올려) Expanded( child: Tooltip( message: _displayPath(t.current), - child: Text(trf('복사 · 이동 갈 곳: {0}', [_folderName(t.current)]), - maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12.5)), + triggerMode: TooltipTriggerMode.longPress, + child: LayoutBuilder(builder: (context, box) { + const style = TextStyle(fontSize: 12.5); + final label = trf('갈 곳: {0}', ['']); + final room = box.maxWidth - textWidth(context, label, style); + return Text('$label${middleEllipsis(context, _folderName(t.current), style, room)}', maxLines: 1, overflow: TextOverflow.clip, style: style); + }), ), ), TextButton( diff --git a/lib/ui/file_tools.dart b/lib/ui/file_tools.dart index 62a9087..d90cb2f 100644 --- a/lib/ui/file_tools.dart +++ b/lib/ui/file_tools.dart @@ -89,7 +89,7 @@ Future showFileHash(BuildContext context, String path) async { const SizedBox(height: 10), TextField( controller: compare, - decoration: InputDecoration(labelText: tr('비교할 값 붙여넣기 (받은 곳의 해시)'), isDense: true), + decoration: InputDecoration(labelText: tr('비교할 해시'), helperText: tr('받은 곳에서 알려 준 해시를 붙여넣으세요'), helperMaxLines: 3, isDense: true), onChanged: (_) => set(() {}), ), if (match != null) diff --git a/lib/ui/ftp_settings.dart b/lib/ui/ftp_settings.dart index ca1dc2a..7cb9138 100644 --- a/lib/ui/ftp_settings.dart +++ b/lib/ui/ftp_settings.dart @@ -201,7 +201,7 @@ Future editFtpServer(BuildContext context, AppController c, {FtpServ content: SizedBox( width: 520, child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - TextField(controller: name, decoration: InputDecoration(labelText: tr('이름 (탭에 보임, 비우면 주소)'))), + TextField(controller: name, decoration: InputDecoration(labelText: tr('이름'), helperText: tr('탭에 보입니다 · 비우면 주소'), helperMaxLines: 3)), Row(children: [ Expanded( child: TextField( @@ -265,7 +265,7 @@ Future editFtpServer(BuildContext context, AppController c, {FtpServ ), TextField( controller: root, - decoration: InputDecoration(labelText: tr('시작 폴더 (비우면 로그인한 곳)'), helperText: tr('예: /volume1/video · 탭의 맨 위가 됩니다'), helperMaxLines: 3), + decoration: InputDecoration(labelText: tr('시작 폴더'), helperText: tr('비우면 로그인한 곳 · 예: /volume1/video · 탭의 맨 위가 됩니다'), helperMaxLines: 3), ), if (security != 'none') ...[ const SizedBox(height: 6), diff --git a/lib/ui/path_label.dart b/lib/ui/path_label.dart index cd6cb72..c0a6999 100644 --- a/lib/ui/path_label.dart +++ b/lib/ui/path_label.dart @@ -1,5 +1,7 @@ import 'dart:io'; +import 'package:flutter/widgets.dart'; + import '../core/vfs.dart'; import '../l10n/tr.dart'; @@ -25,3 +27,33 @@ String shortPath(String path, {int keep = 3}) { final parts = friendlyPath(path).split(' › '); return parts.length <= keep ? parts.join(' › ') : ['…', ...parts.sublist(parts.length - keep)].join(' › '); } + +/// [text] 를 [style] 로 한 줄에 그렸을 때의 폭 +double textWidth(BuildContext context, String text, TextStyle style) { + final tp = TextPainter( + text: TextSpan(text: text, style: DefaultTextStyle.of(context).style.merge(style)), + textDirection: Directionality.of(context), + textScaler: MediaQuery.textScalerOf(context), + maxLines: 1) + ..layout(); + final w = tp.width; + tp.dispose(); + return w; +} + +/// [text] 가 [maxWidth] 를 넘으면 가운데를 "…" 로 줄인다 (앞 · 끝이 보이게 - 412 캡처 10/18, 갈 곳 막대) +String middleEllipsis(BuildContext context, String text, TextStyle style, double maxWidth) { + if (maxWidth <= 0) return '…'; + if (textWidth(context, text, style) <= maxWidth) return text; + var lo = 0, hi = text.length; + String cut(int n) => '${text.substring(0, (n + 1) ~/ 2)}…${text.substring(text.length - n ~/ 2)}'; + while (lo < hi) { + final mid = (lo + hi + 1) ~/ 2; + if (textWidth(context, cut(mid), style) <= maxWidth) { + lo = mid; + } else { + hi = mid - 1; + } + } + return lo == 0 ? '…' : cut(lo); +} diff --git a/lib/ui/sftp_settings.dart b/lib/ui/sftp_settings.dart index a425b6b..7d042f4 100644 --- a/lib/ui/sftp_settings.dart +++ b/lib/ui/sftp_settings.dart @@ -248,7 +248,7 @@ Future editSftpServer(BuildContext context, AppController c, {SftpS content: SizedBox( width: 520, child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - TextField(controller: name, decoration: InputDecoration(labelText: tr('이름 (탭에 보임, 비우면 주소)'))), + TextField(controller: name, decoration: InputDecoration(labelText: tr('이름'), helperText: tr('탭에 보입니다 · 비우면 주소'), helperMaxLines: 3)), Row(children: [ Expanded( child: TextField( @@ -302,7 +302,7 @@ Future editSftpServer(BuildContext context, AppController c, {SftpS ), TextField( controller: root, - decoration: InputDecoration(labelText: tr('시작 폴더 (비우면 홈 폴더)'), helperText: tr('예: /volume1/video · 탭의 맨 위가 됩니다'), helperMaxLines: 3), + decoration: InputDecoration(labelText: tr('시작 폴더'), helperText: tr('비우면 홈 폴더 · 예: /volume1/video · 탭의 맨 위가 됩니다'), helperMaxLines: 3), ), CheckboxListTile( contentPadding: EdgeInsets.zero, diff --git a/lib/ui/smb_settings.dart b/lib/ui/smb_settings.dart index b7acc82..ead098e 100644 --- a/lib/ui/smb_settings.dart +++ b/lib/ui/smb_settings.dart @@ -169,7 +169,7 @@ Future editSmbServer(BuildContext context, AppController c, {SmbServ content: SizedBox( width: 520, child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - TextField(controller: name, decoration: InputDecoration(labelText: tr('이름 (탭에 보임, 비우면 주소)'))), + TextField(controller: name, decoration: InputDecoration(labelText: tr('이름'), helperText: tr('탭에 보입니다 · 비우면 주소'), helperMaxLines: 3)), Row(children: [ Expanded( child: TextField( @@ -211,7 +211,7 @@ Future editSmbServer(BuildContext context, AppController c, {SmbServ decoration: InputDecoration(labelText: tr('공유 이름'), helperText: tr('예: video · home (\\\\서버\\공유 의 공유)'), helperMaxLines: 3), onChanged: (_) => set(() {}), ), - TextField(controller: root, decoration: InputDecoration(labelText: tr('시작 폴더 (공유 안, 비우면 맨 위)'))), + TextField(controller: root, decoration: InputDecoration(labelText: tr('시작 폴더'), helperText: tr('공유 안의 폴더 · 비우면 공유의 맨 위'), helperMaxLines: 3)), TextField(controller: user, decoration: InputDecoration(labelText: tr('아이디 (비우면 게스트)'))), TextField( controller: pass, @@ -225,7 +225,7 @@ Future editSmbServer(BuildContext context, AppController c, {SmbServ ), ), ), - TextField(controller: domain, decoration: InputDecoration(labelText: tr('도메인 · 작업 그룹 (보통 비움)'))), + TextField(controller: domain, decoration: InputDecoration(labelText: tr('도메인 · 작업 그룹'), helperText: tr('보통 비워 둡니다'), helperMaxLines: 3)), const SizedBox(height: 8), if (testing) const LinearProgressIndicator(), if (result != null) diff --git a/lib/ui/webdav_settings.dart b/lib/ui/webdav_settings.dart index b39b750..7b1c07c 100644 --- a/lib/ui/webdav_settings.dart +++ b/lib/ui/webdav_settings.dart @@ -158,7 +158,7 @@ Future editDavServer(BuildContext context, AppController c, {DavServ content: SizedBox( width: 520, child: Column(mainAxisSize: MainAxisSize.min, children: [ - TextField(controller: name, decoration: InputDecoration(labelText: tr('이름 (탭에 보임, 비우면 주소)'))), + TextField(controller: name, decoration: InputDecoration(labelText: tr('이름'), helperText: tr('탭에 보입니다 · 비우면 주소'), helperMaxLines: 3)), TextField( controller: url, keyboardType: TextInputType.url, diff --git a/test/explorer_test.dart b/test/explorer_test.dart index 8932d9f..cfaba8b 100644 --- a/test/explorer_test.dart +++ b/test/explorer_test.dart @@ -146,7 +146,7 @@ void main() { ..explorerLayout = 'single' ..explorerOnePaneCopy = 'other'; await open(tester); - expect(find.textContaining('복사 · 이동 갈 곳'), findsOneWidget); + expect(find.textContaining('갈 곳: '), findsOneWidget); expect(find.textContaining('갈 곳: right'), findsOneWidget, reason: '숨은 창의 폴더 이름 (전체 경로는 풍선 도움말)'); await act(tester, () => tester.tap(find.text('doc.txt'))); await settle(tester, () => false, rounds: 15); diff --git a/test/explorer_webdav_test.dart b/test/explorer_webdav_test.dart index 9d4267a..99487ad 100644 --- a/test/explorer_webdav_test.dart +++ b/test/explorer_webdav_test.dart @@ -78,7 +78,7 @@ void main() { await act(tester, () => tester.tap(find.text('WebDAV').last)); await calm(tester); expect(find.text('WebDAV 서버 추가'), findsOneWidget); - await tester.enterText(find.widgetWithText(TextField, '이름 (탭에 보임, 비우면 주소)'), '집 NAS'); + await tester.enterText(find.widgetWithText(TextField, '이름'), '집 NAS'); await tester.enterText(find.widgetWithText(TextField, '주소'), server.url); await tester.enterText(find.widgetWithText(TextField, '아이디'), 'user'); await tester.enterText(find.widgetWithText(TextField, '비밀번호'), 'pass');